Core points
In application development, we try to create standalone modules to reuse code in future projects. However, creating completely independent modules that provide useful functionality is difficult; unless their dependencies are properly managed, they can lead to maintenance nightmare. This is where dependency injection comes in handy, as it allows us to inject the dependencies needed for the code to function properly without hard-code them into the module. Pimple is a simple dependency injection container that utilizes PHP's closure to define dependencies in a manageable way. In this article, we will explore the problem of hard-coded dependencies, how dependency injection solves these problems, and how to use Pimple to make code leveraging dependency injection easier to maintain.
Specific dependencies issue
When writing applications, we use many PHP classes. A class may need to call methods of one or more other classes to provide the expected functionality, so we say that the first class depends on other classes. For example:
<?php class A { public function a1() { $b = new B(); $b->b1(); } }
Class A depends on class B. If class B is not available, the above code will not work. Furthermore, every time we hardcode the creation of an object in the class, we will have specific dependencies on the class. Specific dependencies are a barrier to writing testable code. A better approach is to provide an object of class B to class A. These objects can be provided by A's constructor or setter method. Before we further discuss, let's look at a more realistic scenario.
Sharing content on social network sites is very common these days, and most websites directly display their social profile feeds on their websites. Suppose we have a class called SocialFeeds that generates feeds from social sites such as Twitter, Facebook, Google, etc. Create separate classes to handle each of these services. Here we will look at the class TwitterService that interacts with Twitter. The SocialFeeds class uses TwitterService to request Twitter feeds. TwitterService interacts with the database to retrieve specific user tokens that access the API. The token is passed to the OAuth class, which retrieves the feed using the provided token and returns it to the SocialFeeds class.
<?php class A { public function a1() { $b = new B(); $b->b1(); } }
<?php class SocialFeeds { public function getSocialFeeds() { $twService = new TwitterService(); echo $twService->getTweets(); } }
<?php class TwitterService { public function getTweets() { $db = new DB(); $query = "Query to get user token from database"; $token = $db->getQueryResults($query); $oauth = new OAuth(); return $oauth->requestTwitterFeed($token); } }
<?php class OAuth { public function requestTwitterFeed($token) { // Retrieve and return twitter feed using the token } }
It is obvious that SocialFeeds relies on TwitterService. But TwitterService depends on DB and OAuth, so SocialFeeds depend on DB and OAuth indirectly. So what is the problem? SocialFeeds rely on the concrete implementation of three classes, so it is impossible to test SocialFeeds separately without the real implementation of other classes. Or, suppose we want to use a different database or a different OAuth provider. In this case, we have to replace the existing class with the new class throughout the code.
Fix specific dependencies
The solution to these dependencies is simple, i.e. dynamically provide objects when necessary without using concrete implementations. There are two types of techniques that can inject dependencies: constructor-based dependency injection and setter-based injection.
Constructor-based injection
Using constructor-based dependency injection, the dependency object is created externally and passed as a parameter to the constructor of the class. We can assign these objects to class variables and use them anywhere within the class. The constructor-based injection of the SocialFeeds class is as follows:
<?php class DB { public function getQueryResults($query) { // Get results from database and return token } }
An instance of TwitterService is passed as an object to the constructor. SocialFeeds still relies on TwitterService, but now we are free to provide different versions of Twitter service providers, and even mock objects for testing purposes. Regarding TwitterService, DB and OAuth classes are also defined in a similar way.
<?php class SocialFeeds { public $twService; public function __construct($twService) { $this->twService = $twService; } public function getSocialFeeds() { echo $this->twService->getTweets(); } }
Seter-based injection
Using setter-based injection, objects are provided by setter methods instead of constructors. The following is the setter-based dependency injection implementation of the SocialFeeds class:
<?php $db = new DB(); $oauth = new OAuth(); $twService = new TwitterService($db, $oauth); $socialFeeds = new SocialFeeds($twService); $socialFeeds->getSocialFeeds();
The initialization code now includes DB and OAuth looks like this:
<?php class SocialFeeds { public $twService; public function getSocialFeeds() { echo $this->twService->getTweets(); } public function setTwitterService($twService) { $this->twService = $twService; } }
Constructor and Setter Injection
Select constructor-based injection or setter-based injection is up to you. Constructor-based injection is suitable when all dependencies are required to instantiate a class. Setter-based injection is suitable when dependencies are not required every time.
Pros
Disadvantages
After understanding dependency injection and various injection techniques, it's time to see Pimple and how it fits into it.
Pimple's role in DI
When we can already inject dependencies using the aforementioned techniques, you may be wondering why you need Pimple. To answer this question, we need to look at the DRY principle.
Don't repeat yourself (DRY) is a principle in software development that aims to reduce the duplication of various information, which is especially useful in multi-layer architectures. The statement of the DRY principle is that "every fragment of knowledge must have a single, clear, authoritative representation in a system" - Wikipedia
Consider the constructor-based injection example. Every time we want an object of the SocialFeed class, we have to repeat the entire setup process of instantiating and passing its dependencies. According to DRY, such code should be avoided to prevent maintenance problems. Pimple acts as a container that defines such dependencies to avoid duplication. Let's look at a simple example to see how Pimple works.
<?php class A { public function a1() { $b = new B(); $b->b1(); } }
Create an instance of Pimple as a container for storing dependencies. It implements the SPL ArrayAccess interface, so using it is very similar to using arrays. First, we define a key that holds the name of any class we want. We then define a closure to return an instance of the specified class that acts as a service. Note that an instance of the container will be passed to $c, so we can refer to other defined keys as needed; each defined parameter or object can be used in the closure via the $c variable. Now, whenever we want an instance of a class, we can reference the key to retrieve the object. Let's convert the SocialFeeds example to Pimple. The examples on the official Pimple website show constructor-based injection, so here we will illustrate setter-based injection. Remember that in order to use Pimple, we don't need to modify any setter methods or code defined earlier - we just encapsulate the logic.
<?php class SocialFeeds { public function getSocialFeeds() { $twService = new TwitterService(); echo $twService->getTweets(); } }
DB and OAuth classes are both independent modules, so we return their new instances directly within the closure. We then use setter-based injection to add dependencies to the TwitterService class. We have added the DB and OAuth classes to the container, so we can access them directly inside the function using $c['db'] and $c['oauth']. Now, the dependencies are encapsulated within the container as a service. Whenever we want to use a different DB class or a different OAuth service, we just replace the class in the container statement and everything will run perfectly. With Pimple, you just need to add new dependencies in one place.
Advanced Pimple Usage
In the above scenario, Pimple returns a new instance of each class from the closure every time it is requested. In some cases, we need to use the same object without initializing a new instance every time, such as connecting to a database is a perfect example. Pimple provides the ability to return the same instance using a shared object. In doing so, we need to specify the closure through the share() method, as shown below:
<?php class A { public function a1() { $b = new B(); $b->b1(); } }
Also, so far we have defined all dependencies in a single location in the Pimple container. However, consider a case where we need a service that has its dependencies but is configured slightly differently from the original service. For example, suppose we need to access the ORM to implement some functionality of the TwitterService class. We cannot change the existing closure because it forces all existing functions to use ORM. Pimple provides the extend() method to dynamically modify existing closures without affecting the original implementation. Consider the following code:
<?php class SocialFeeds { public function getSocialFeeds() { $twService = new TwitterService(); echo $twService->getTweets(); } }
Now, we are able to use different extension versions of tweet_service in special cases. The first parameter is the name of the service, and the second parameter is a function that can access object instances and containers. In fact, extend() is a powerful way to dynamically add dependencies to suit different situations, but make sure to limit the extended version of the service to a minimum, as it increases the amount of duplicate code.
Summary
Managing dependencies is one of the most important and difficult tasks in web application development. We can use dependency injection of constructors and setter methods to effectively manage them. However, dependency injection itself also has some troubles, and Pimple solves these problems by providing a lightweight container to create and store object dependencies in the DRY way. Feel free to share your experience managing dependencies in your project and your thoughts on Pimple as a dependency injection container in the comments below.
FAQs about Dependency Injection with Pimple (FAQ)
Pimple is a simple PHP dependency injection container that allows you to manage and centralize services in your application. It is used in PHP, making the code more flexible, reusable and easier to test. By using Pimple, you can instantiate objects in one place and inject them into different parts of your application, reducing the need for global state and making your code easier to maintain and test.
Pimple works by storing service definitions in containers. These definitions are callable (function or method) and they return instances of the service. When you access a service from a container, Pimple executes the service definition to create a service object. This allows you to manage services in a centralized manner and share services throughout the application.
Pimple can be installed using Composer (PHP's dependency management tool). You can install Composer globally on your system and then introduce Pimple in your project by running the command composer require pimple/pimple
.
In Pimple, you can define a service by assigning a callable object to a key in a container. The callable object should return an instance of the service. For example, you could define a service for the mail sender class like this:
$container['mailer'] = function ($c) { return new Mailer($c['smtp']); };
In this example, the mail sender service is defined as a new instance of the Mailer class, where the smtp service is injected as a dependency.
You can access services in Pimple using array notation with service keys. For example, you can access the mail sender service like this: $mailer = $container['mailer'];
. When you access a service, Pimple executes the service definition and returns the service object.
By default, Pimple returns a new instance of the service every time it accesses the service. If you want to share the service and return the same instance each time, you can use the share()
method. For example, you can share the mail sender service like this: $container['mailer'] = $container->share(function ($c) { return new Mailer($c['smtp']); });
.
Yes, you can use the extend()
method to extend services in Pimple. This allows you to modify the service after defining it. For example, you can extend the mail sender service like this to add additional configuration:
$container['mailer'] = $container->extend('mailer', function ($mailer, $c) { $mailer->setFrom($c['email.from']); return $mailer; });
In this example, the setFrom()
method is called on the mail sender service using the email.from
service as a parameter.
In Pimple, you can use the protect()
method to protect parameters (which should not be considered as parameters of services). This allows you to store values in a container without treating them as service definitions. For example, you can protect the configuration value like this: $container['config.value'] = $container->protect(function () { return 'value'; });
.
You can use Pimple in a project by creating a new instance of the PimpleContainer class and defining a service there. You can then access the service from the container where you need it in the application. This allows you to manage services in a centralized manner and inject them into different parts of your application.
Pimple provides many benefits for PHP development. It makes your code more flexible because it allows you to manage services in a centralized way. It makes your code easier to reuse because it allows you to share services throughout your application. It makes your code easier to test because it allows you to inject mock services for testing. By using Pimple, you can improve the quality of your code and make it easier to maintain and test.
The above is the detailed content of PHP Master | Dependency Injection with Pimple. For more information, please follow other related articles on the PHP Chinese website!