This course is archived!
This tutorial is built using Drupal 8.0. The fundamental concepts of Drupal 8 - like services & routing - are still valid, but newer versions of Drupal *do* have major differences.
Service Arguments
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribePretend like the ROAR string calculation takes a really long time - like 2 seconds:
// ... lines 1 - 6 | |
class RoarGenerator | |
{ | |
// ... lines 9 - 15 | |
public function getRoar($length) | |
{ | |
// ... lines 18 - 24 | |
sleep(2); | |
// ... lines 26 - 29 | |
return $string; | |
} | |
} |
If that were true, we wouldn't want to generate it more than once: we'd want to cache it in the key value store.
We already know how to access services from our controller: just use the create()
function workflow to pass more arguments to __construct()
. And hey, sometimes, it's even easier because there are shortcut methods that help do the common stuff.
But how can we get access to a service - like keyvalue
from inside of another service, like RoarGenerator
? If you're thinking that we could make RoarGenerator
extend ControllerBase
.... well, you're clever. But nope, sorry! That only works for your controller.
Accessing a Service inside a Service
Instead: here's the rule: as soon as you need access to a service from within a service, we need to create a __construct()
method and pass it as an argument. Run container:debug
and grep it for keyvalue
:
drupal container:debug | grep keyvalue
This tells me that the keyvalue
service is an instance of KeyValueFactory
. Create the public function __construct()
and type-hint its first argument with this class. Woh, but wait! Like before, there is a concrete class and a KeyValueFactoryInterface
that it implements. You can use either: interfaces are technically more correct and much more hipster, but really, it doesn't matter. Name the argument $keyValueFactory
and open the method. I'll use another shortcut, alt enter on a mac, to initialize the field:
// ... lines 1 - 4 | |
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; | |
// ... line 6 | |
class RoarGenerator | |
{ | |
private $keyValueFactory; | |
public function __construct(KeyValueFactoryInterface $keyValueFactory) | |
{ | |
$this->keyValueFactory = $keyValueFactory; | |
} | |
// ... lines 15 - 31 | |
} |
That doesn't do anything special: it just creates this private property and sets it.
Ok, step back for a second. This is really similar to what we did in our controller when we needed the dino_roar.roar_generator
service. We're saying that whoever creates the RoarGenerator
will be forced to pass in an object that implements KeyValueFactoryInterface
. Who does that or how they do that, well, that's not our problem. But once they do, we store it on a property so we can use it.
And use it we shall! First, create a cache $key
called roar_
and then the $length
:
// ... lines 1 - 6 | |
class RoarGenerator | |
{ | |
// ... lines 9 - 15 | |
public function getRoar($length) | |
{ | |
// ... line 18 | |
$key = 'roar_'.$length; | |
// ... lines 20 - 30 | |
} | |
} |
That'll give us a different cache key for each.
Next, grab the key-value store itself with $store = $this->keyValueFactory->get()
and then the name of our store: dino
. If the store has the key, return $store->get($key)
and save us from the long, slow 2 second sleep:
// ... lines 1 - 15 | |
public function getRoar($length) | |
{ | |
$store = $this->keyValueFactory->get('dino'); | |
$key = 'roar_'.$length; | |
if ($store->has($key)) { | |
return $store->get($key); | |
} | |
// ... lines 24 - 29 | |
return $string; | |
} | |
// ... lines 32 - 33 |
At the bottom, set the string to a variable and then store it with $store->set($key, $string)
. And don't forget to return $string
:
// ... lines 1 - 15 | |
public function getRoar($length) | |
{ | |
$store = $this->keyValueFactory->get('dino'); | |
$key = 'roar_'.$length; | |
// ... lines 20 - 24 | |
sleep(2); | |
$string = 'R'.str_repeat('O', $length).'AR!'; | |
$store->set($key, $string); | |
return $string; | |
} | |
// ... lines 32 - 33 |
That's a perfect cache setup.
Let's give this cacheable RoarGenerator a try. Back in the controller, undo everything so we're using that service again:
// ... lines 1 - 10 | |
class RoarController extends ControllerBase | |
{ | |
// ... lines 13 - 35 | |
public function roar($count) | |
{ | |
$roar = $this->roarGenerator->getRoar($count); | |
// ... lines 39 - 41 | |
return new Response($roar); | |
} | |
} |
Ok, refresh!
Configure Service Arguments
Ah, error!
Call to a member function get*() on null, RoarGenerator line 22
Go check that out. Huh. Somehow, the $keyValueFactory
is not set: it wasn't passed into the __construct()
method.
But wait. Who is instantiating the RoarGenerator
anyways? The container is! We registered it as a service, and Drupal says new RoarGenerator()
as soon as we ask for it. But it doesn't pass it any constructor arguments.
Somehow, we need to teach Drupal's container that "Hey, when you instantiate RoarGenerator
, it has a constructor argument. I need you to pass in the keyvalue
service.". To do that, add an arguments
key:
services: | |
dino_roar.roar_generator: | |
class: Drupal\dino_roar\Jurassic\RoarGenerator | |
arguments: | |
// ... lines 5 - 6 |
This is an array, so I can hit enter and indent four spaces, or two spaces. Two spaces is the Drupal standard. If I put the string keyvalue
, it will literally pass the string keyvalue
as the first argument. That's not what we want! We want the container to pass in the service called keyvalue
.
The secret way to do that is with the @
symbol:
services: | |
dino_roar.roar_generator: | |
class: Drupal\dino_roar\Jurassic\RoarGenerator | |
arguments: | |
- '@keyvalue' |
Ok, we just made a configuration change, so rebuild the Drupal cache:
drupal cache:rebuild
Refresh! Ok, super slow - it's sleeeeping. Shhh... let it sleep. There it is! But next time, it's super quick! Try 50. Slow.......... then fast the second time!
Maybe you didn't realize it, but we just had another big Eureka, buzzword-esque moment. Yes! And that is: when you are inside a service - like RoarGenerator
- and you need access to another service or configuration value, you need to add a __construct()
argument for it and update your service's arguments
to pass that in.
So if tomorrow, we need to log something from inside RoarGenerator
, what are we going to do? PANIC... is not the correct answer. No, we're going to calmly add a second argument to the __construct()
method then update dino_roar.services.yml
to configure this new argument.
A cool side-effect of this stuff is that even though we had to change how RoarGenerator
is created, we didn't need to change any of our code that uses it. In the controller, we just ask for dyno_roar.roar_generator
:
// ... lines 1 - 35 | |
public function roar($count) | |
{ | |
$roar = $this->roarGenerator->getRoar($count); | |
// ... lines 39 - 42 | |
} | |
// ... lines 44 - 45 |
The container looks to see if the keyvalue
service is already created. If it isn't, it creates it first and then passes it to the RoarGenerator
. No matter how complex creating RoarGenerator
might become, all we need to do is ask the container for it. All the ugly complications are hidden.
q: when you set @keyvalue argument you add quotes '@keyvalue'. Is that necessary? or it's a good practice?
Thanks.