Dependency Injection for Mere Humans

 2 years ago 4,697 views
Presented by Ian Littman (@iansltx)

September 12, 2019

What's the difference between service location and dependency injection? Why is this dependency injection thing such a big deal anyway, and how do you use that tool correctly? I'll answer these questions and more, including real-world examples of refactoring an application toward the more explicit, testable, closer-to-SOLID applications.

About Ian
When he isn't building or maintaining web applications, primarily of the API variety, for a handful of clients, Ian is probably biking between successively later-closing coffee shops in Austin, Texas, or making some sort of comment about transportation or communication infrastructure. He also helps organize the Austin PHP user group and Longhorn PHP Conference.

Transcription (beta):

My name is Ian Littman and this is Nomad PHP, September, 2019. If you'd like to get distracted with the presentation rather than something else happening on the internet, you can go to Ian dot. I M slash Di nomad 19 and follow along. There are several links across, well, a whole ton of links actually across this presentation. And of course you can view the links later as the presentation is online. I also have code samples for some of the minor demos that we'll be providing in a just at ian dot I M slash DI code 19. Now, with all of that out of the way, this presentation was actually inspired by a presentation back four years ago or so when Jeff Carouth was presented on, yeah, basically the same thing also at nomad. So if there's anything good about this talk, he gets credited. If there's anything bad about this talk, it's stuff that I've messed up, but his talk was kind of the seed that got me understanding about dependency injection and learning how to do it in various ways and just getting to be a better, yeah, in that respect.

So shout out to him now. We're going to cover a fair amount of ground this evening. We'll cover various types of dependencies. What had been the C injection is and why you would even want to do it. What does that actually buy you in creating a maintainable laugh that isn't going to SAP your sanity, at least for that reason. We'll look at the types of dependency, injection, constructor injection, cetera, injection property injection and perimeter injection and why you would use one or the other in a given use case. We'll also refactor some basic code from a, a bit of a mess. I can say this because I wrote the sample code to something that is a bit more abstracted, a bit more dependency injected. We'll do that for kind of a generic repository or of operator class as well as a real example using a slim three in one of the route color bowls.

Finally, we'll look at dependency injection containers. Now you don't need one of these in order to do dependency injection, but as an application gets larger, it helps to have some centralized place where you're doing that practice. We'll at a few different examples as well as how to build one from complete scratch. The amount of magic that goes into one those containers, various, but you can actually get away with a very little in the way of magic if you just want to understand the concepts. And finally we'll look through a few different anti-patterns in your journey to a dependency and injected application of things that though they may seem easy to start with, you're probably not going to want to do because they'll box you into a corner later. Now, the big idea behind this talk is that highly coupled code is bad, highly coupled being you have a portion of your code that directly references another portion of your code and directly references [inaudible] your code.

So in the end you have to keep the entire application in your head in order to make a even a minor change. Not only that, but if you want to change some core piece of functionality, you have to change it all over the app. You have to copy paste code and it just gets to be a mess. That said, in order to build an application at all, in order to build something that actually out puts something to a browser or save something to a data, you have to couple that application to the things that actually do the work. Whether that's API clients, whether that's a database engine or what have you. So you want to manage that coupling in a way that is not going to bite you later on. So the reason that decoupled code is good is that you can take a piece of that code.

We'll work on it in isolation and test it in isolation in still actually get a good idea of whether that code is going to work. If you have proper boundaries of abstraction between one piece piece of code and another, then you can just assume the rest of the code works for, for example, unit tests and just work on one thing at a time to give you kind of a zoomed out version of that abstraction. You don't have to understand every line of source code in your database engine or in PHP for that matter in order to use it. The abstractions are reliable enough, at least a high enough percentage of the time that you can take them for granted and constrain the viewpoint, constrain what you're working on and at any given point in time so that you can get what you need to get done faster.

Now, in addition to easy to test, decoupled code is easier to do. Debug. If you have a smaller component where you're not having a 70 or 80 or a hundred or 200 or 500 or a thousand line function or class, then there's less to sift through when something goes wrong. Now you can end up with a Zanaya code where you have 10 layers of abstraction, but you can manage that level of complexity as well. So the types of dependencies that we're talking about today are one of a number of dependency types that you interact with when building an application. Specifically, we're talking about per class dependencies. We're not talking about library level dependencies. That's handled by composer. That's a whole different talk. We're not talking about operating system level dependencies handled by a app to we're young, we're APK, or if you're using Macko S then maybe brew, we're just talking about within an application within a library or maybe across boundaries between libraries, how you get a class, a little dependency into another piece of code.

So with that said, let's go through a couple of common dependencies. First off, you have your database or an [inaudible] abstraction layer on top of it. On the database side, you might be using PDO or you might be using my SQL or the Postgres equivalent or on the ORM side or the database abstraction layer side, you might be using doctrine or maybe you're on the layer of all sides, so you are using the eloquent ORM. You might throw a cash on top of that because the database doesn't return quite as quickly as you would like. Or maybe you have some ephemeral information that you want to calculate once in shove it in the cash. So it dependency would be the a adapter or the client for that caching system. Say read it. Serve memcache. Likewise file storage. Whether that is on a local file system on Amazon, S three on other services that match Amazon's S3 API on other surfaces that don't match Amazon's S3 API or really what you should be using is fly system because it abstracts all that away and you can just kind of go about your day at that point.

Another dependency is notification. It's whether that is sending an email, sending them text message, sending a push notification or in general, any over the network API that you're calling, whether you're calling that directly using HP client like guzzle or curl or whether you're using somebody's software development kit that wraps maybe the ragged edges of that API in a bit cleaner wrapper. Finally, at least for this slide, you've got logging as dependency in so many applications. One of the things it means that you all almost constantly inject into a class is a lager interface because you want to understand when that class or some method in that class does something either successful or something that you might not have expected. So drilling down a little bit further, the current web request is a dependency of your application. The post body contents or the query string or the headers are all a dependency that maybe you should flow through your application rather than the application being built say calling super globals.

That's why as you've seen frameworks proliferate, they've wrapped those super globals in a request object, which, Oh, by the way, makes testing a good bit easier. Likewise environment variables, whether they're feature flags for your entire system or maybe they're secrets for API keys. Those are also dependencies of your app as is the current user. Maybe it's just that you're talking about an admin rather than a standard user or if you're dealing with Santo auth API on top of that, you may only have authorized this particular access token to interact with certain parts of the application. So on top of the fact that a given user has a role, you have scopes on top of that, further limiting their access. Similarly, if you're dealing with more of a browser or a server side rendered application, you might have a session that includes additional data beyond the fact that the a user is Bob Smith.

Finally, and here are a few things that trip folks up. The current time is a dependency. I learned this the hard way when I was building some, a code for a local food delivery startup. And we started enforcing server-side when the market was closed or not. And so you do that work on a Thursday or Tuesday or whenever and then you deploy at Santa's Thursday. Friday rolls around Saturday rolls around, Sunday rolls around in the market is still closed because you had injected the current or not even injected. You had just provided the current date and time into this function. And I had an edge case that only showed itself on Sundays. So I learned the hard way that yes, the time in date currently for a given request is actually a dependency of your application. So you probably want to extract that at our end, make it a little bit easier to fake.

Finally, static method calls may hide dependencies that maybe build a database link or something that is not actually a function that you just, you call the function, you provide inputs. And if you know the inputs, you know the outputs that a static call might hide some, I don't know, creating a database connection, creating a cash connection initializing the API that is now a dependency of your application. Even if you didn't pass an instance of [inaudible] of the object in. Now, some of the static method calls in some frameworks may wrap a a more dynamic implementation. And you might say, well, that's not as big of a deal, but you at least need to know that those static method calls, just like any other dependency, does present a, a liability when you're trying to understand Dan a given block of code. So let's do a quick exercise here. We've got to this order processor that I mentioned earlier and we have a few dependencies here. I'll give you all a couple of seconds to see if you can spot them.

And you know what, let's go ahead and highlight them here. So first off in the constructor, which has no parameters, so you might think, well the order processor doesn't have any dependencies. It's just kind of pulling things from, actually no, it's pulling a, my SQL order repository from thin air, which itself is somehow making a connection to the database. We don't know how, but all we know is that the constructor here doesn't have any parameters. So it's probably in a similar situation to this order process or class in that it is pulling global state into this particular class without any rhyme or reason. Now on the complete order function, we have a little bit of extra logic here. We have a global call to a variable named logger. Okay. Who uses globals a that actually yeah, app make logger. Okay. Actually let, let, let's hope that you'll learn of all people are still doing that.

Okay. Let's okay longer get instance. There we go. So we have this instance of the logger and then we say, okay, the order is completed it at the current time we just went over how that might be a horrible mistake. Then we take that longer that we pulled out of longer get instance, that static method and say, well, the order is complete. And then that order repository that we pulled out of thin air, we save the order to that. And finally we call all this static method on mailer to actually send the work completion email. So so once you have the, once we know what the dependencies are, we can maybe see whether we can abstract these out. But first, the issue with this sort of thing is that if the order processor well we, we, it's hard to find what it actually depends on. We certainly can't look at the instruct a constructor and we basically have to run through the entirety of code and kind of see what dependencies it relies on.

Likewise, if we want to figure out what the what parts of the application use the mailer, so maybe we can replace that or maybe we can remove dead code, then we have to grab the entire code base and look for male or colon, colon and hope that that actually works out. Finally, if we want to actually test complete order and don't want to send emails to the person that we are testing on behalf of, then we better hope that the mailer has some testing functionality built in because as we certainly can't reach into the order process or object and cleaned that up on our own. [inaudible] Yes, the mailer may have some additional tests, unrelated stuff built into it or maybe the a mailer, a calling Colin sinned method is a actually the hiding a dynamic object behind it. But we don't know that at this point.

So with all that said, we haven't really defined what dependency injection is. So dependency injection is taking a dependency of a block of code and, and pushing it into the class rather than that class going out and grabbing the dependency from some external source. Now this is actually different from the dependency in version principal and in solid a single responsibility principle, open, closed, et cetera. We'll get to the actual definition of the dependency in version principal later. It's related, but they aren't really the same thing. So as mentioned earlier, there are three types of dependency injection that we'll actually go over here. Constructor center in property and parameter injection, first constructor injection. Now I was complaining about the fact that we didn't have any idea of what that might [inaudible] [inaudible] repository was doing to get the actual my SQL connection. What we could do in that case is throw the my SQL connection into the constructor for that order repository or in this case like this example, we're taking something that needs a longer and saying let's provide that on construct.

At this point the class no longer needs to figure out how to get that longer. It can just assume that the lager is provided to it and then it's just a property access away for use later. Now if the, if we take this particular approach, the nice thing is if we do this across the board, then if we're asking the question of well where, what dependencies is this class have, all we have to do is look at the constructor. All we have to do is start to tie being new, whatever the name of the class is. And if we're using an idea of some sort, then it's going to say, Hey, look at all the parameters and hopefully if we phone type hints on top of those parameters so we can see exactly what we need to build that entire class. Also, it means that the class is instantiated a little bit more reliably.

We can rely on anything that's constructor injected because well it's it's going to be there and no matter what, so we don't have to, for example, have a, some guard code or worse have our application completely fail. If that logger doesn't actually exist. If the logger doesn't actually exist, then the application will fail at a lot earlier stage where we can catch it rather than later when we call a single method and that method eventually 20 lines down and says, Oh wait, I need to log something. Even worse is if the, Oh, I need to log something is itself on an error. So it tries to call the lager and the lager doesn't exist and now instead of logging the error that was originally the case, you now have an error saying, I couldn't find the logger. Also, as your constructor gets bigger because you have more dependencies, you end up being a little bit more aware of the single responsibility principle, the S in solid, and you start taking a step back and saying, wait a minute, maybe this class is doing too much.

Maybe I need to split it up so that I can actually figure out what is going on in doing that in a reliable manner. Now the flip side of that is if your class really needs to do a bunch of things and then you might end up with a ton of constructor parameters in that might get unwieldy when trying to make sure that the class is actually built out. Also on a larger class, you may have a bunch of dependencies that you had to create ahead of time, but you end up never using because maybe your class has 10 methods. Each of those methods uses two or three dependencies. There's not a whole lot of overlap, so you end up with 10 dependencies for the class. But that also goes to the single responsibility principle where you say, well, maybe this needs to be multiple classes rather than showing a, rather than throwing everything into a single class.

Finally, if you need to swap out dependencies a little bit later in the objects life cycle you know, for, for whatever reason maybe it is a command and you need to delay database connection until the command actually gets run versus when the command is created. For example, in [inaudible], all commands are created. As soon as you run a layer of Austrian line PHB artisan, but their handle method isn't cold until you actually run it. But if you have something heavy in that double underscore construct and it gets called every time that command line starts up, then you could bog down your entire command line run by command that you never actually even run most of the time. So it may be the wrong part of the life cycle and you may want to look at something a little bit different.

So to give you an idea of the constructor bloat that we're talking about, this was a, an actual a action class or controller class in an authentication API that I built a few years back and yes, I need it every single day. One of these dependencies to perform the functions in that class. Now maybe I could have abstracted things a little bit and cleaned this up a bit, but yes, it took, let's see here. One two, three, four, five, six, seven eight yes, nine dependence fees in order to perform the functions that this particular class needed. Now you might say, well that's a huge, a bunch of dependencies. Maybe we can just kind of hide that complexity by throwing everything in a dollar options array and no, don't do that. The reason you don't want to do that is now you're throwing away the type hinting in the visibility that PHP has gotten since Oh 5.2 5.3 somewhere around in there where you can actually understand a bit about the class by looking at at its constructor.

If you hide that behind an options array that at best, now you have to do all that verification logic of checking to see what type each of those options are. And at worst you just kind of go with the flow. You don't actually check for object types on each of the options. And you end up with things failing at runtime in weird ways and then it being difficult to debug. So that ease of debug ability that we were talking about earlier is completely subverted by having this construct with an options are right. There are usually better ways to do this. And then if you, for example, need to a default some of those dependencies so that you're not providing nine framers on a constructor, you can actually use static constructor methods or something to that effect. Now, second form, a dependency injection we're talking about is or injection.

In this case we're looking at optional dependencies or at least mostly optional dependencies where in this case we have a class that kind of wants a lager but it doesn't necessarily need it. So in this case we have a set lager method. We still type hint that. So we know that we have the proper type of instance there and then we just had it to a local perimeter in local, a variable and sorry, local property and then we can use it later. Now if we take this a step further, we could actually do property injection, which is somewhat similar except instead of having a proper Senator with a proper type pin to make sure that you're not injecting garbage into your class, you just say, you know what, we're going to do a public property and we'll see what the user of this class throws in there and hope that it's the right thing.

Now this does get a little bit better in PHP seven for when we get typed properties, in which case you set to lager or something. That's not a logger and it'll actually fail catch is that you still don't have quite the level of specificity in a setting thing, things that you would with a center injector, you could throw additional validation logic here. Whereas on the property injection side, not so much. Now the nice thing about cetera and property injection is that it allows you to load potentially more expensive dependencies later on in the life cycle, maybe only when you need them or only in use cases where when you need them, and that's particularly true when you have larger classes that most of the time you don't need those dependencies and at the last second you want to inject those relatively heavy dependencies because now you know that you'll need them and if the dependencies are optional anyway, if you have guards around those optional dependencies, then maybe it's fine that you don't have a lager for a class or maybe it's fine that you don't have a a mailer associated and if you need to swap out dependencies, then center injection is the way to do that.

Of course if you want to make sure that you can't set or inject something multiple times in a row, then you can add logic in your center to say, Hey, if this is non null, then bail out. Now the catch with this is now instead of looking at your country constructor to figure out what you actually need to build the class, now you have to look at all your set methods and then you're wondering whether the set methods are actually setting dependencies on the class or seeing properties on the class and it just ends up being somewhat of a mess unless you have a relatively low number of senators and they're pretty well documented. Also since setters happen after construction, you need to have some sort of handling code to recover from cases when those particular dependencies are set to know maybe you have that set up as a a get method so you have this arrow get logger rather than referencing the property directly so that that way you can fail safely or maybe return a stub if that lager doesn't actually exist in that class.

Now with that said, you end up with if you don't have that get log or method that somewhat does the same thing or at least provide you a stub, then you end up with code looking kind of like this. Any time you need to check for lager existence, you say, Hey, is this lager, does it exist and is it the right type? If so, then we can safely call it. If not, then you'll want to skip a reference to that object because otherwise this longer log will give you a 500 because the property access on the non object. Now realistically we'll have a typesetter or a type property for this, so maybe you don't need the instance of check. You still end up with this truth in this check on the logger to make sure that it does indeed exist again to avoid your application throwing a fatal error.

Alternately, you could use the Knoll object pattern. Now in this case, what we're doing is we would require a logger upfront or something that actually looks and acts somewhat like a lager, but we could allow that logger to know OB. In this case, we have this no longer class which implements the lager interface and [inaudible] implements the pattern that they call a null object pattern. And instead of having to do a Knoll check on whether the property has set itself, we assume no, this was constructor injected or this is injected 100% of the time. And if we call it then it just doesn't do anything. That means that you have a lot fewer if blocks in your code and the code ends up being a lot more legible and pushes those abstractions of where the little log or actually does anything are not out to probably where it belongs, which is in the or class itself.

That said, the longer example is a pretty bad one because you don't want to build an old longer class. There is one built into the PSR log interface which that's PSR three. That's what monologue implements. If you're using that and a few other loggers do as well. So just use that one. Don't reinvent the wheel on logging. Now, one cool thing that you can do with cetera injection, and this is more on maybe having a required instance of a class being set in and relying on your dependency injection container to handle that is, yeah, you've got a couple of steps to this. First off, you take that. For example, set lager method. You pull that into a trait, then you take that same method signature, you add it to an interface. You important the trade-ins class, you import the interface where you you say that the class implements.

Does that interface because thanks to the trait it does. And then you tell the DEI container that you're using, Hey, anytime you see an object that conforms to this interface, inject this particular lager call, this set log or method, and make sure that that lager always exists. But that way if you have a bunch of classes that have a common dependency, whether that's a log or whether that's your database entity manager, your dependency, injecting all of them via container, and you don't want to clutter up your constructor of that class, then this is one way to do that. Assuming of course that you have a dependency injection container that understands how to [inaudible] to make this process work in, not all of them do. Unfortunately. You can't just say, well, I'm going to declare the interface right on the trait. So all I have to do is the single use line in all of a sudden I have a lager everywhere.

I need it. PHB doesn't support that. You have to declare the interface separately and then implement the interface in the class just like you use the trait D on different lines. Now our final dependency injection type here is parameter injection. And you'll see, see this more on say, console commands or on a route called balls for for your web frameworks but can really use this anywhere. The idea here is that the caller is responsible for making sure that the objects that are given function, needs, perform performance actions are there or at least some of them. So that means that in this example, the request and response objects for this this is a slim of three. A route column will here flow through the application rather than being kind of static things that are as soon too be there when the application starts.

The request and response example in particular is nice for if you wanted to serve multiple requests per application instantiation, say a in a synchronous context or something like that, but the fact remains that instead of throwing them in a set or a constructor, you're sprung them in exactly where you need them. And we could do the same thing say with a order action, you would see this something like Laravel where you've got a a controller class that has multiple actions. Not all of those actions we need, need every dependency. But this order action needs both requests, which is just going to be passed in as a matter of course. And this order service, which level kind of bootstrapping and container is intelligent enough to build out for you using a concept called auto wiring, which we'll get to later. Now the nice thing about perimeter injection is you're not having to set dependencies that you don't necessarily need for other methods.

The catch with it is that now your caller needs to find those dependencies on its own. And the use case for that is usually more in a framework where that wiring will be done for you. Now you can use multiple of these methods simultaneously, a mix of constructor and cetera injection and even perimeter injection to get where you need to go. And it's actually quite common as I adult stuff out to use a mix where say in a controller, I am constructor injecting the things that I need absolutely across the board. Then perimeter injecting other pieces as I am routing for a given request. And in other cases I'll use that kind of a trait interface set or injection pattern in order to quickly set across the board, say the fact that I need a lager or the fact that I need a mailer.

Now I told you that I would talk through the dependency in version principle and while this goes hand in hand with dependency injection, it's not actually the same thing. It depends. The in version principle is just the idea that you want everything depending on abstractions rather than concretions. You want to depend on the fact that you can go to a generic implementation of the dentist interface and provide and call a examined mouth on them with a mouth and then you get back an examination result rather than having to go with a concrete implementation of the dentist and take an hour and a half journey in each direction in order to make that happen. Not saying that, that, Oh, wait, that. Yep, that happened this morning. Point being that details would depend on obstructions and you shouldn't flip that to where you have to know for example, which say version of an API you're using.

If the API, the API wrapper is actually properly abstracted. Now the, this goes hand in hand with use of interfaces rather than typing ting against a concrete classes. And one cool thing that you can do with interfaces that you can't do with classes and PHP is you can do multiple inheritance. So if you want to type in against say an interface that that does actions a, B, C, and [inaudible], then you can build an interface that extends all of the sub interfaces, implement that superset interface on your class. And then any of those four interfaces or the superset you can type into against those in the rest of your application. And that implementation class will conform to that interface. So that way you only have a couple methods or maybe one method in each interface, depending on what a given class needs as a pendency at any given time, which means that it's easier to at a glance, see what you might need to mock out or stub out when testing that class.

Now you might get into an argument on whether you actually need the interface, a string in the name of your interface and well really it doesn't matter or whether you call whether you call your interface SMS provider or SMS provider interface as long as you're consistent it pick one and stick with it. Now similarly, we want to make sure that the abstractions that we are saying we're following don't leak, don't expose implementation details that we really shouldn't care about. I shouldn't care whether a, a car has a combustion engine or is running off of batteries. When I press the gas pedal, it should just go. Likewise, if I'm building a user repository or if I'm using a user repository, I shouldn't really care at the application level, at the level where you're using this dependency, whether a, a user repository is pulling users from my SQL post crest Reddis, [inaudible] elastic search or some fancy GRPC API.

In all cases we're getting back something that conforms to the interface of a user or maybe we're getting back Knoll or maybe we're getting back a user, no object to take your pick. And then performing actions on that user hack of the user might actually be lazy loaded so that the heavier portions of that user object aren't even populated until you ask for them. The implementation of whatever is calling this user repository get by ID method should not care about how that transaction takes place. Now, when interesting, a corollary of this is that you usually do not put constructors for an object in the interface of the object because that's kind of the one place where if an object has dependencies that that you're going to kind of want to vary those dependencies. There's nothing stopping you from, I'm including the constructor on the interface, but I really haven't seen that pattern out in the wild.

So with that base level of knowledge, let's [inaudible] factor the code snippet that I showed earlier so we know where our dependencies are. So let's start by working on the constructor and as well taking this lager instance in cleaning that up as well. So let's pull those into construction parameters and Oh, by the way, as, as we're doing that, we're going to say, well, the my school order repository implements order repository interface. So we'll type it against the order repository interface called a repository rather than the concrete version. That way if we decide to say read orders from a flat file or pull them off of a Kafka queue or something, we don't care for this order process, the sort of things just continue working. We've also pulled the lager up into the constructor here because both the order repository and a logger, we assume that we're going to use those all the time.

So let's get those into the constructor and then them available in reference them via the property accessor's rather than pulling them out in air. So let's continue along that path. We've cleaned up the order repository and lager here. Let's also clean up the mailer and the current time. So one week we could do that is pulling both of them into the constructor of the order processor. So we say the order process or needs the current time and we're going to construct or inject that and just set that in wherever we're configuring things. And likewise with a mailer where we appear to need that all the time. So we're just going to make sure that's instantiated before the order processor is now one catched with this is maybe we're reusing the order processed or over time in maybe a request actually takes a few seconds for an API call to process or a what have you prior to getting down to setting this completed at timestamp.

So maybe setting the date time interface on construct is not the best idea. Maybe a better idea is pulling that into a perimeter dependency here, in which case we always will need to provide both the order and the current time into the complete order function, which the caller can decide, you know what, this is just going to be a new date, time immutable and call it a day or it can have some more complex logic or if we're in a testing scenario, then we can now call a complete order multiple times with different values of the current time. Even set the current time to a specific value for each of those calls so that we can suss out edge cases so that you're not ending up with a some weird closed behavior when you're when you're supposed to be open. So been talking about dependency injection containers and saying, Hey, we're going to inject these dependencies somehow.

A dependency injection container manages getting those constructors filled with the proper parameters. So those setters of those properties depending on what type of container you're using and that container is going to be instantiating your objects. Yeah. At least most of them rather than you using the new keyword everywhere. Now you can still use the new keyword for example. Yeah. Creating a daytime instance, which a daytime immutable instance would be a value object where everything that that you need in order to to build out an instance of that object you should just be able to get got me immediately and say if you wanted to have a value object for a $10 bill, you could do a, a new dollar bill in parentheses 10. Those cases you don't necessarily need a dependency injection container to figure out what all else you need to inject.

You already have that wherever you are in the application. Likewise, if you need multiple instances of say a, a model object or something to that effect, then either you directly or maybe your framework or your ORM is using a factory pattern under the hood where it might have a [inaudible] based set of dependencies that you're providing it ahead of time. But then it's adding a few more in right before it creates a unique instance of an object. Also dependency injection injection containers are not actually required to do the practice of dependency injection. You can new up objects in, in pass constructor parameters or past set or parameters. Without them it's just maybe a little bit less centralized in a little bit more difficult. Finally, don't use a dependency injection container as a rule, as a service locator. Well, what's a service locator? So to give me an example of that, we've got this container interface.

It's an interface, so that's cool, right? But we're pulling things out of that container within the constructor. For this class we're saying, Hey, we need an order repository. We're going to get the order repository, we're going to get a log, or we're going to get the mailer and Hey, we're using class names, we're all good, right? Well what you're doing here is you're saying the order, your processor needs a container and some stuff needs to be in that container and it's not clear what stuff that is. It's just stuff. So now you have to look in the constructor in order to figure out what stuff that is. And even then, it may not be clear what exactly instance wise or type hint wise, those order repository at log or in mail or might be, and you also end up with a temptation to do this.

We were saying, you know what, I'm just going to pull the entire container into this class, pull dependencies out of it whenever I need them, and then go about my day. Take it from somebody who's actually done this in the past and it's relatively difficult to unwind and it means that it's quite difficult to figure around what exactly a cost needs when you actually get around to testing it. This is the sort of thing that will paint you into a corner despite the fact that you say, Oh, well I'm injecting all my dependencies. See, it's right there in the constructor. If you don't acid a container interface and then the class doesn't build. Now some service containers, some dependency injection containers have a fair amount of magic, but that doesn't necessarily need to be the case. Okay? Yes, we're using magic methods here, but they're pretty self explanatory.

Yes. This container built by no other than Fabiana [inaudible], CA back several years ago now is all you really need to build out a dependency injection container. You can set a dependencies into it, you can get dependencies out. It actually use the magic, uses the magic center and get her methods to do that. So it's just, it feels like property accesses and it'll actually set your dependency is lazy so that you're not instantiating every single object everywhere. Now let's see this in practice. So we need a longer, the lager has a constructor with the log or is it dependency? So we knew up in the container,


We say, okay, the the loggers property on that actually is not a property. We have the magic setter method going on there, but we set it as a callable so that that way we get the lazy loading that we're expecting. Now, we then set this another property called my service in this case because we're feeling fine about it. And in that case we can set it to a callable that takes the entire container as a parameter. So he can reach back into the container. Grab the instance of the lager that we said earlier and return it now. Well, yes, I just said avoid the anti-pattern of service location. In your actual classes. You're going to use that service location pattern inside your dependency injection config. And as long as it stays only in that configuration, then you're fine. Now we use the magic guitar on this very last, I've already, I'm blind to get back out of this, the service that we've set. Now this will invoke the


Get our method, which will in turn invoke the guitar method again when it realizes that it needs to find a logger. So it's just calling callable pulls in. You end up with a, an a, a instance of the needs a logger with the lager dependency dropped right in. And really that's all there is to it. Now PSR 11, formerly known as container inter-op, all it does is provide a couple of inter or a a an interface with a couple of methods on it to say, Hey, let's all agree on some really minor conventions around how you can pull things out of a container. So that way you can swap dependency injection containers as needed. And things will continue to work. Say if you're using a microphone work and went to switch from container a to container B. So if we want to actually implement that container interface on a Twitty, then we go from something that fits into 140 character tweet into something that fits into a two and 80 character one.

And this is what that looks like. All we did was added, get in, has methods in and implements tag there. And so what we'll do now is actually use that to dependency. Inject everybody's favorite programming test, namely fizzbuzz. In this case we're saying we're injecting definitions of fizz in buzz in the behavior we're injecting is what happens when a double underscore two string is called. In this case, we return fizz buzz. So that's what gets printed out. We inject them into our fizbos class and then on invoke we use that magic two string method to to provide results back. So what this, what we need to do to configure this is we'll new up a container and since we're using Twitty, we throw a Coldwell here, a colorable there, and then sets the fizzbuzz to pull a fizz in, bus back out of the container, new up the class, and now we can use that fancy PSR 11 get method over and over again as we go from one to 15 printout, one to fizz, four buys, et cetera.

Now that said, we probably don't want to use Twitty and a production application because every time you grab a dependency, it's going through that entire Kabul calling process. And there are a lot of solid feature filled dependency injection containers out there. Each major framework has one service manager dependency injection components, eliminate container. And I'm actually doing a talk on that eliminate container in a few months in Bulgaria. And the link here is to an older version of that talk that I did back in March. Alternately, there's no shortage of a standalone DII containers, whether that is pimple, PHP, PDI, et cetera. We'll actually take a look at three of those this evening. We're running a little short on time, so I'll pick up the pace for those. Now. Pimple as of a while back is PSR 11 compliant and is mature but really hasn't been worked on in a while probably because it really does what it needs to do and does it reliably.

So in the case of Pymble, instead of using the magic setter method, what we do is do use array access. Here. We set the dependencies onto that particular key in the array and and then pimple goes through and instantiates the class based on that Coldwell same pattern as we use with Twitty. One difference here is that by default, pimple will actually keep that instance of the class around. And that way you're not having to reinstate a log or 15 times if you use a lager in 15 classes in your application over the course of a request. Now if you want to go back to the behavior of having a brand new instance each time, which by the way is the default for say, a [inaudible] Dai container, then you can use [inaudible] the factory method on pimple to do that. You can also, if you want to shove a callable as is into the container rather than having it resolve based on the dependency in any unit or the return value, you can call a container or protect and provide it with a callable, which will then return as is.

Or if you forget to do that and you want to return the either a bare callable or, or the code that actually generates a dependency rather than the dependency itself, then you can use the raw method on the pimple container. Not a whole lot of magic here, not a whole lot of code here. It's still plenty quick. And as a result it was the D vault container for a some framework version three and you can actually use it in slim for as well. Although the default project is not use that one, it uses us something else. Now speaking of slim three, let's see, take a century route and refactor it to be dependency injection. So weave or use dependency injection container rather, we've actually already gotten this user repository to be dependency injected. It requires a PDO as a constructor parameter, so you're not guessing at what dependencies the user repository has, but we're having to figure out to build the connection string and that sort of thing.

Anywhere where this route is, which probably means we have to pull in variables from the global scope and it's just kind of a messy word. Maybe use super globals in a underscore in. So instead let's take that user or actually let's first take the PDO dependency in, pull that into the container, let's call it a DB, for lack of a better term. In that case, we'll still need to figure out how it's to put environment variables in there. But now that's in the dependency injection configuration. And for the other 45 routes that we have, we can reuse the PDO instance rather than having to hunt for those connections string framers every single time. And Oh, by the way, this means that all of the PDO instances will actually be the same one and we'll get, we'll get some performance improvements as a result of that.

So let's take this one step further and and say, okay, well we, we've taken the use, we've taken the database, pull that into a dependency, we're using the container back in this a request. Let's do the same thing with the user repository. So now we pull the user of pository. We've already taken the database out, we asked for the database back inside the code to instantiate through user repository. And then slim action. He binds the container get method to this inside these route cobbles. So we'll actually use that to pull the user repository back out of the container. So now we've got this dependency injection and that's all well and good, but we're still kind of pulling dependencies out on the fly. If we pull this into an action class, which if you were around earlier for the ADR talk you probably had a, an idea of how those action classes look.

Then we could take the entire class in, inject, frame it as like this. You'd say, well, all we need for this particular action is a user repository. So constructor inject that. And then on invocation slim we'll provide the request response and optionally route parameters to that particular route callable to get the job done. So in this case, we use a property access or for use repository. And to get this actually built into the container config, we just do this. We say that's called the dependency get users build the functionality to use user repository class. And then on the route definition, all you have to do is provide the name of the dependency. Insulin will build the dependency out thanks to pimple and I call a double scoring, double underscoring vocal on it. Now that is slim. Three that is a pimple. We'll fly through these a little bit more quickly.

PHP DEI is another kind of third party. It depends the injection container, but one kind of interesting thing that it does is auto wiring is turned on by default. So assuming we have the same longer classes that we mentioned earlier, all we need to do is say, Hey, the logger interface maps to an actual or instance and then PHP DEI does the rest. Now the way that it does this is needs a logger that we mentioned that we mentioned earlier, has a logger interface typing it in the construct. Okay. These PDI turns on auto wiring. When you ask for an instance of a class, it will look up via PHPs reflection API. What constructor parameters are there. It will then look into its own dependency injection config and see, okay, can I resolve an instance of that class? Whether that's an interface or whether that is the concrete thing.

It'll traverse that graph back auto wiring as needed. And hopefully you have enough information based on a combination of auto wiring and the DEI config to build out the entire class. If it does, then we'll return it to you. Now the PHP reflection API is a little bit slow compared with just calling calls. As a result, auto wiring containers will tend to either by default or at least provide you the option to cash. The calculations that they need to make to auto wire and PHP DEI is no different, so in order to help it along though, you need to actually specify, Hey auto wire this class so that on its build step where it spits out that cash file, it actually knows which classes to look for. Here's what that looks like. You're actually adding depth clinicians here and we've switched from Newing up the container directly, so using this container builder because we're doing some configuration ahead of time and that the preferred way of interacting with PHP DUI is by using this ad definitions method and providing a mapping of classes to effectively instructions for PHP BI to follow.

### Transcription trimmed due to size ###


PHP Tutorials and Videos


Showing 1 to 1 of 1 comments.
aplehm - 2 years ago
Excellent talk! A really good explanation of DI!


PHP Tutorials and Videos