ASHRAF ABED:
Hey, (INAUDIBLE). (INAUDIBLE) is bad. Alright, so hello. We're going to be talking about... It's really a sort of a case study from a project I worked on where we had some significant performance issues and I dug into the code the developers wrote, refactored it a little bit, and utilizing services and dependency injection really significantly, you know, addressed all the performance issues that we were focused on at the time. So my name is Ashraf. I am... I put Triple+ as a joke, but I am a Triple Acquia-certified Drupal expert. And the... Let's see, I worked at Acquia a while ago as a technical architect. I left to focus on Debug Academy full time, and at Debug Academy we do all sorts of training. A lot of these slides are copy-pasted from some of our other training courses. If you're interested in this talk, you'd probably be most interested in - we have an architect series. It's five classes. Each class is 2.5 hours long. You know, we know that people at this level tend to not have a lot of time for training, so we condense it and we go into how to use object-oriented code to make your code more maintainable.
How do you make code more performance? How do you manage automated testing? And whatnot. Each one of those topics is one day in The Architect series. We also have an advanced module development class and various other classes as well. Today we're going to talk about services, dependency injection, and then get into a case study to show how it had a big impact performance-wise, using it in a specific way. So first of all, I will just give a quick intro to Services and dependency injection. I like to think of Services as tools. So, you know we have PHP objects in Drupal. We have entity objects, nodes, blocks, etc., but we also have various services. There's entity-type manager, logger, factory, etc. One thing that the services all seem to have in common is, again, they essentially act as tools. Whereas for nodes, for a node object, you might have node one, node two, node three, a bunch of different objects. For services, it's not the same. You're going to have an entity-type manager object whose job it is to manage entities, but it's a tool that can be used again and again.
So when I say service objects remain pure, what I really mean by that is you're not loading a bunch of different instances of the same service. You're just loading it once and using it again and again. Each service has a specific job or tasks that it can be used for. And one of the reasons we use services instead of just a plain-old PHP object is because it's swappable. It gives you the ability to override it and replace it with your own service. So if, you know, Drupal gives you the various logger tools, you've got the DB logger, the system log, etc. You have the ability to add your own logger and it can pick that up and use it without you patching Drupal core. So some examples of services you've probably encountered in the wild, again, the logger, which is actually a service collector, but it's not the topic for today. And there's - what did I do? Two monitors. I tried to go backwards on this one, but yes. Logger, which is a service collector, it picks up many different logging services.
The database service, so you're never explicitly calling MySQL or MariaDB. You just interact with the database service and it does that for you. Entity type manager, you're not calling node load anymore. Now you're using the entity type manager service and using that to load and manage entities. Now, why do we use services? Well, services are inherited from Symfony. So Drupal 8 was rewritten on top of Symfony. And what Symfony has is a service architecture which comes with something called a service container. And the service container exists for every request. And what the service container does is, think of it as like a bucket for all the services that have been called upon in the current request. So if you have a page request and the current page request logs a message or logs something to the database and it also prints out a message and runs the database query, the service container is going to lazy-load those. So upon request, as soon as someone says I want to log something, Symfony will retrieve the logger service, instantiate the object, you know, PAFYs that would be something like New Logger.
It would create that object and it would reference it from the service container. So it basically keeps a link to it. So that happens the very first time somebody asks for the logger in the current page request. But in a single request, you're going to most likely use the database service many times, the logger service multiple times. So the second and third and fourth time that you reference those services, you're going to see this has already been instantiated in the service container and Symfony is going to retrieve it for you and it's going to give you the same service object that was used earlier in the request. So it's not reloading or re-instantiating these objects again and again. So that's one of the main benefits of using services. And they're lazy-loaded, meaning if the current request doesn't need the logger service, it will not be instantiated. So it's not instantiating all the services. Now, how do you access these services that have already been instantiated? One of the ways is through dependency injection.
So if you create your own service, as we'll see the code a little bit later, but if you create your own service, you would typically list the other services that your service depends on. So... So by doing that, Symfony will basically say, OK, I'm instantiating this service, this custom service you created. It relies on the logger service, so let me go to the service container. Let me get that object and let me pass it as an argument to your custom services constructor. So that's really how dependency injection works. It takes it from the service container, it takes the instantiated object, and it passes it as an argument to the constructor of your service. Again, we'll see the code here in just a few seconds. But the reason for this, one of the reasons for this is, it allows you to focus on what your service is really meant to do as opposed to, My service depends on the logger. Let me look up how the logger works. Let me look up how to instantiate it. You don't need to do that. Additionally, you don't need to worry about which logger is in use.
So if you're creating your custom service, you don't have to say, Let me get the DB log because that's what my site is using. You just say, Let me get the logger factory and it will say, OK, these are all the logger services that are relevant and it brings them all automatically. So you don't have to worry about those implementation details. Speaking of implementation details, let's just take a quick look at the code and then we'll move on to the performance example because I know in these classes, not everybody has necessarily written services before. So if you want to create a new service you would, in your own custom module, create a file, your module's name.services.yml. You can look at core.services.yml for a lot of examples of services provided by Drupal Core. The code ends up looking something like this. You put the name of your service. I'll get my little laser pointer. You get a - you put a name for your service, and whatever you put here, that's what other people have to type in when they want to call on your service.
So you put a name for your service and you put the class representing your service. And that class is essentially what will be used to instantiate your service object. And in that class, you would create a Construct function, a Construct method, and in your construct method, Drupal will automatically pass whatever services are listed here. So if you list again logger factory and database, then whenever somebody loads your custom service, Drupal will automatically retrieve these from the service container and pass both of them into your classes constructor as arguments in that order. Any questions about this? OK. And if you wanted to overwrite an existing service, you can just create it the same way as usual. The only difference is the name of the service. Make sure you put the same name as what you're overwriting. So if you want it to overwrite the database service, create a service called database in your custom module. Yes?
SPEAKER:
Do you have a quick (INAUDIBLE) syntax? What's the difference between using just the name and the (INAUDIBLE) in your arguments versus using that (INAUDIBLE)?
ASHRAF ABED:
If you don't use the R-sign, I believe it will be treated as a string and it'll just pass it in as a string. It won't actually get the service. Alright, yes?
SPEAKER:
So if two different modules try to overwrite the same service, which one wins?
ASHRAF ABED:
It's unknown if you use this technique. (LAUGHS). If you use this technique, you have more control, I believe. You can specify a weight. So that's why this is listed here. But yeah, it's even in the documentation that it's unknown if you just use the simpler way of overwriting it. OK. And this.... Honestly, the ability to overwrite services has been a game-changer in the cases I've needed to use it. There was one project where, you know, something must have been done wrong by the developer to cause this scenario. But we had a scenario where they were using the messenger service to print out confirmation messages after forms were submitted. But this project also had a very aggressive caching setup, and what ended up happening was someone would submit the form, the message would display, and it would get cached by the CDN, and the next person who came would see that message. And like I said, this project had a lot going on to it, probably a little too gung ho with the way the caching was set up.
So instead of refactoring the whole project caching, I actually was able to overwrite the messenger service and I made it so it was client side instead of server side. So I put the message in the cookie and then I use JavaScript to take the message out of the cookie and print it out, and it was just like a full sitewide solution. No more caching problems with messages. You know, there's downsides to that approach, putting messages, you know, cookies are not really designed for all the characters that could be in a message. But nonetheless, the fact that services and dependency injection and whatnot is available gave us the power to make those bad decisions. (LAUGHS). Let us fix the problems all at once. OK, and... Again, just wrapping up the code sample. So I showed you how to create a service and overwrite one. If you want to actually use a service, so if someone wanted to use the service you just created, they could call Drupal Service and then the name of your service that you created, and that will instantiate your service object or retrieve it from the service container if it was already instantiated.
If there's a more specific class, Drupal documentation recommends you use that. So instead of writing Drupal service database, you should just use the function. Now, this is how you would use it in a .module file or a .theme file where the code is global and it's not in a class. If you wanted to use it inside of a class, you would use dependency injection. We saw the mymodule.services.yml file where you list the services you depend on. Now what you'll see is in your service class, you would create a Construct function and Symfony would automatically pass all of those arguments you listed as arguments into the construct method. That's all it does. That's the end of dependency injection. It passes them in as arguments and then it doesn't do anything else. So if you don't create this function, it's like you don't have access to the service. If you do create it, most likely you would do this. You would create a property on your current object and store a reference to the service that they gave you so that you can use it elsewhere in that object.
If you don't do that, it gets passed in and you don't do anything with it. So normally that's what people do with dependencies that are injected. They store them all. And this should not be an equal sign. This should be an arrow just like that. Any questions here? This is specifically how you would do it in your service. You would receive injected dependencies in your service. And just last slide about the code. If you were doing this in a form or some other class, then you wouldn't have mymodule.services.yml to list your arguments. So how can you say which services you want to be passed? To do that, you first would implement this interface - container injection interface - in your class, and you would make this Create function, public static create. It's always the same code, and it should return new static container, Get the name of the service you want. So if you want many services to be injected into your classes construct method, you would just separate them by commas. Container Get this service, comma.
Container Get that service... Yeah, this Return new static syntax, static refers to the class that we're currently in. So Return new static basically means - let's say this were the node class. It basically means new node. And as the arguments in that node's constructor, it would be the services retrieved from the service container. Alright. Any questions about the implementation of services or dependency injection? Yes.
SPEAKER:
I've seen another approach where they use like inside the public static function Create, they create like a (INAUDIBLE) method. So for example, like where they will put it on the property on the class, so I was wondering like words like... Like, what the difference is or what is the advantage of using one way or another.
ASHRAF ABED:
I think this is the most standard way that I've always seen. But yeah, when you call return new static, that's going to create the object. There's nothing stopping you from - instead of returning it, you can just make a variable that says My Object equals new static, so-and-so. And then you could get these dependencies and attach to them as properties and then you can return the objects already prepared and then you might be able to get away without creating the construct if you do that. I don't know that one's really better than the other, but here at least you can make sure that the type is right. You can use the type hinting. Yes.
SPEAKER:
Any of the methods does describe how you deal with your class or your service extends some (INAUDIBLE) class, that requires arguments. So you ask whatever it needs, ask your (INAUDIBLE), and you (INAUDIBLE) in the case that the parent class changes, you're still good to go. (INAUDIBLE).
ASHRAF ABED:
OK. So we're going to take a look at the scenario that's highlighted in the (INAUDIBLE) title, how we use services to resolve a performance bottleneck on our project. So on this project, the performance for authenticated traffic was very poor. And again, I joined the project very late. It was built in a very complicated way, but we had some custom user access and intricate caching. It was set up in such a way that... It's hard to explain, but it's almost like all users of the same role shared one account instead of everybody getting their own account. So you would log in, it would create a hash, it would figure out which account you belong to, it would pass that as context to the CDN, and the CDN would give you the cached page for that authenticated role. And that way, we basically had authenticated traffic where the authentication was really served by the CDN fully. So we had reasons for that. It was a very high-traffic, heavily authenticated site. But with that, the user access logic ended up being very complicated and we were launching very soon and we went to our load testing phase and it was just failing the load test.
So first, we had to figure out where - what's the real root of the performance issues. So we used XHProf. It's a tool, it's a freely available, open-source tool. And what you do is, you can load a page, you can trigger a request however you like, and XHProf will log basically all of the function calls, how much time each function call took, how many times each function was called. It's really very thorough and very, very useful in determining bottlenecks. And you can let it run for your whole request or you can narrow it down. So maybe within this one function, start at the beginning and at the end of the function. So for us, what we ended up finding out was, get user access tiers. This - at the time global function was... It was taking like 50% of the page load time, which was really shocking. So the first thing I did, I was tasked with solving this. So the first thing I did, I converted it to a service. Just because in Drupal 8a and above, if you have a tool that's going to just be used repeatedly and it usually should be created as a service.
So this was essentially a tool amongst other tools we had. So I created a place for it, a service for it, and called the service this name - user_access_tier_manager, and inside of it I created that function getTierforuser. And basically, initially, I let it be the same function, the same code. I just copy pasted the code, made sure I didn't break anything. That was fine, but it didn't solve the performance problem. So at this point, the service itself only loaded once per request, but that wasn't really the problem earlier. The problem was that the function itself was called many, many times on each page. It was like for every single block, in some cases, for each field, it would call and check and it would bubble the cache tags and all that good stuff. But it was called many times and it would do queries and it would do joins between many tables to figure out what this user should and should not have access to. So the next thing I did to improve performance is, I switched from using the entity query to querying the tables directly.
So I would use - I'm still using the database abstraction layer, but I was using more of a raw query and going straight to the one table that I needed. So we actually didn't need all the joints to do the logic. That's not... That shouldn't be your default. You can take that approach if you're having a performance problem with all the joins and you really don't need the joins and, you know, you really know what you're doing when it comes to the query. You can do that, especially if you're just reading data and not writing it. You really want to avoid writing data in that way. That did help. It probably cut the load time by 15%-20%, but it still wasn't quite good enough. So this function was being called many times. The computation was being performed repeatedly. The output of the function was different per user per request. But per request, within the request, the output was always the same. It's always the same user loading the whole page. And light bulb moment - services are instantiated once per request.
So we have something that lives for the whole request and we have a function call whose output is the same for the whole request. And so, we have been talking about caching solutions. I was thinking, can we cache the output for the request? And at the time the team was thinking, yeah, we just need more caching and more caching. And there were all these caching layers. Some of them we never interact with really, but Memcache, Varnish, CDN, all these caching layers where we can store the output of that function. And, you know, Memcache would have been OK. We could have used Memcache, but Memcache is not meant to be cached from the beginning of the request to the end. It would have persisted afterwards. There are ways around... There are ways to deal with that where maybe you prepend the user ID, but really we didn't want it to be cached more than one request because these users could lose access on the next page load. It was just a complicated access setup. And it hit me, you know, why are we overcomplicating this?
We have an object that exists exactly for the request from the beginning to the end. So within the function, what I did was, I essentially took the original function. I didn't refactor it anymore, but I took the output of the original function and I stored it as a property on the service object itself. So this user tier for the current user ID is equal to whatever the function return. And before calling all of that access checking logic, next time I checked, Have I done this before? Have I already computed this? And so, what ended up happening was instead of this being called 100 times, it was called once. You would get into this function, is it empty the first time? Yes, it's empty. You call the function, get the output and store it on the service object, which is attached to the service container and persists for the entire request. it would return what they asked for. The next time this is called, is this empty? No, we're part of the same request. The property is still on that object because the object persists in the service container.
And so, we skip the function call and we just return what they asked for. And essentially, that acted as a single request cache. Yes.
SPEAKER:
Quick question. Do you use the protected property in other areas of the code?
ASHRAF ABED:
No.
SPEAKER:
So what you could have done - this is just an alternative - is use a static variable within this function. That's kind of a little bit older-school way of doing that. Then that variable would have persisted and effectively persisted across objects.
ASHRAF ABED:
Yeah, we could use a global variable, (CROSSTALK).
SPEAKER:
But if you defined static within, get to your for user like static user tier, then that variable will persist within that function within this object. And then the next time you use that function, it would actually... It would have a value. And you see that a lot in (CROSSTALK) files and V7, the static variable cache, and that methodology could be used here too unless you're using it elsewhere.
ASHRAF ABED:
Yeah. Yeah. Thanks. I noted that. I'm going to look into it more. Yeah, cool. Thanks. Yeah, so for this scenario, at least it did cut down the vast majority of the computation work, you know, because again, services exist for the whole request and... We call the function repeatedly but, you know, only the expensive parts once. And it's - the injection made sure that it's the same object throughout the system no matter how many times or where it's called from. And that addressed it. In the end, it was simple but very effective. And when I explained this to the team, they actually went and looked for every function that they had created that required the user ID as an argument and they updated all of them to use this. They moved them all into this user whatever manager. They did the same thing for all of them and, you know, our performance after doing the database query fix plus this on all of the functions that they came across, it dropped, you know, in improved by 60% at least. And you know, we were ready to launch.
(LAUGHS). Yeah, I mean, I told my colleague, I said, I'm targeting 30 minutes for this hour talk because people don't need things dragged out. But if you have questions, I'm happy. Questions or ideas you'd like to add on, please chime in. We also do have a number of classes scheduled over the next month, so May and June, so if you want to take some Drupal classes before DrupalCon, you know, feel free to take a look. And yeah, if you have any questions or comments or anything, I'm all ears. Alright. Thank you. That's it. Yes, go ahead.
SPEAKER:
If you talk about overwriting services, there's also a notion of decorating. (CROSSTALK). Is there like a smart way to... when to do it?
ASHRAF ABED:
Yeah. Yeah. Yeah. I had slides on decorators and service collectors and a couple of other things and I took it out. But yeah, so for the decorator approach, you would want that if you want the original implementation of the service to also persist if you still want it to be there and you want to add to it or modify it, whereas when you're overriding, you're losing the original implementation of the service. So that's really the trade-off. Do I still want the original functionality to also happen? If so, service decorator. If not, override. Any other questions or thoughts? Yes.
SPEAKER:
We're not quite (INAUDIBLE), but we were able (INAUDIBLE) the proper thing to do would be to use the static (INAUDIBLE). And that's subject to cache validation (INAUDIBLE).
ASHRAF ABED:
Thank you. I like learning here too. (LAUGHS). Thank you. Yeah, I mean, from these things that are scheduled, and in case you're curious, without going to the website, this is five 2.5-hour classes. We have Acquia front-end certification prep. That's four classes spread out over four weeks, plus a lot of written material. Everybody who has ever worked at my company is Acquia Certified, junior-senior developers, and I've trained them all personally and compiled the notes and turned it into a course that really covers everything. Building sites with Drupal, this is a one-day class really for project managers or beginners. You get some hands-on site-building, hands-on experience building a site with Drupal, mostly through the UI. Object-oriented PHP with Symfony, again, this is a one-day class, a full-day class for people who really are comfortable with PHP but have not looked into Symfony much at all. So sort of introduction with Symfony, we build a simple site with Symfony and we also dive into object-oriented programming.
So that would be good if you are not comfortable with object-oriented programming. Yes.
SPEAKER:
I worked on (INAUDIBLE) building site with Drupal.
ASHRAF ABED:
That has no prerequisites. Just comfortable using a computer pretty much. And then the advanced Drupal 9 module development class, we sort of build a somewhat ambitious module in one full day, seven to eight hours. Services, dependency injection, plug-ins... And yeah, that one is ambitious. That one is one where you should be somewhat comfortable with object-oriented programming. You should be very comfortable with PHP and, you know, know at least the basics of module development. But it's a whole class full of writing PHP code. Yeah, and I think that's basically it for me. Oh, sorry. Yes?
SPEAKER:
I just wanted to share something that I think is cool that if you start doing a lot of work with services and dependency injection, you can end up with some classes that have a lot of (INAUDIBLE) code for all those properties that you need at the top. And then in your constructors, setting all those to the services that are being injected, there's a new feature. I don't know if it's in PHP 8.0 or 8.1, which is constructor property promotion, which is very cool. And you get rid of all or a huge part of that boilerplate property creation code and you can essentially declare your properties within the constructor. And that might be a reason - I'm sorry I don't know your name, but when you were talking about the alternative ways to set it up with just the Create method as opposed to Create the constructor, for this, I think you need the (INAUDIBLE) where the constructor is beneficial as you can ditch all of many lines of code of all your property declarations and declare your properties in the arguments of the constructor.
So once you get to the point on your project where you're able to support PHP 8.0 or 8.1, you get a lot of (INAUDIBLE) play code.
ASHRAF ABED:
Yeah.
SPEAKER:
I think it's 8.2. Yeah, I think so. I think you're right, yeah.
ASHRAF ABED:
Yeah, definitely. Yeah, PHP 8 has a lot of features to reduce the amount of code you write so you can get rid of this protected declaration.
SPEAKER:
It doesn't look like a lot here, but if you have a lot of (INAUDIBLE). Yeah, I guess. So are there any costs to adding the service to your module? Should we just take all of our global functions and... Methods and surfaces.
ASHRAF ABED:
It's sort of the default to do that. But if you know it's only going to be called once then, you know, you probably incur a minor cost. It also depends on what you're comparing it to. Are you comparing it to it being in a class but not a service, or it being in a global function but not a class global function? I mean, with a global function, that's going to sort of get loaded on every request. It's not going to be executed, of course. I think it's a very minimal cost. I think with the services, the service is lazy-loaded whereas global functions are present. So you also get that benefit. But I haven't run a test to see what the true trade-off is. It should be some little performance gain in either direction or performance loss in either direction, but I don't know which is bigger. Basically, I'd say it's somewhat minuscule because the service container will exist regardless and it's lazy-loaded, so it's not like it's going to load it when you're not using it. Alright. Anything else from anyone?
OK. Thank you. (APPLAUSE) Thank you very much. Thank you.