A Drupal module for overriding services
To generate meaningful CSV export file names, for instance.
Note: get the full source code from Github
Before I give you the specific use-case that lead to this article, let me first show you the module I ended up with. It all boils down to the generic pattern of overriding (parts of) services that exists in other modules, be that core or contributed. One example of services are EventSubscribers.
You can use this module to override as many services as you require. Pretty much any service listed in any module.services.yml file can be altered following this pattern.
I’ve called the module service_overrider. These are the contents of its service_overrider.info.yml file:
name: Service Overrider
type: module
description: 'Override any service in any .services.yml file.'
core: '8.x'
This is the module file structure:
/service_overrider
service_overrider.info.yml
/src
ServiceOverriderServiceProvider.php
/EventSubscriber
AltResourceResponseSubscriber.php
There is no service_overrider.module file. It’s not needed. But there is src/ServiceOverriderServiceProvider.php (contents below). Note the ServiceOverrider prefix in the file name. It’s the CamelCase version of the module name, service_overrider. Stick to this naming convention or it won’t work.
<?phpnamespace Drupal\service_overrider;use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceModifierInterface;class ServiceOverriderServiceProvider implements ServiceModifierInterface { public function alter(ContainerBuilder $container) {
$container
->getDefinition('name_of_service_in.services.yml')
->setClass('Drupal\service_overrider\...');
// Repeat the above for as many services as you like.
}
}
The above function is automatically called by Drupal core, based on it being inside a class by the name of moduleServiceProvider that implements the ServiceModifierInterface. In the function body we tell core to replace the implementation of an existing service with another class. In our case ‘name_of_service_in.services.yml’ was in fact ‘rest.resource_response.subscriber’ and the class reimplementing it, I named Drupal\service_overrider\EventSubscriber\AltResourceResponseSubscriber.
We’ll write the code for that class next. For now, realise that the above sums up the crux of the matter: swap out the old for the new with 2 lines of code.
In our client’s case the specific service was an EvenSubscriber. So I created a subdirectory named EvenSubscriber and put the overriding class AltResourceResponseSubscriber there, in file src/EventSubscriber/AltResourceResponseSubscriber.php.
I named the class AltResourceResponseSubscriber because it constitutes an alternative version of class ResourceResponseSubscriber (from the RESTful Web Services module), which it extends.
The specific use-case that brought all this up, is where you export to a CSV file the results of some REST service.
Let’s assume you’re preparing for a trip to Paris. You found this travel site that allows you to download various types of lists, like …/hotels/Paris, …/museums/Paris, …/cafes/Paris.
If you do this in Drupal, e.g. via the CSV Serialization module, you are likely to end up with 3 file versions Paris.csv, Paris(1).csv and Paris(2).csv. This is because neither the CSV Serialization module nor the core RESTful Web Services module make any attempt to generate a meaningful file name, one that includes (part of) the path, e.g. hotels-Paris.csv, museums-Paris.csv, cafes-Paris.csv.
The class implementation below does just that. It takes the URL path and replaces the slashes by hyphens. It then uses that for the name of the CSV file that is being exported.
<?phpnamespace Drupal\service_overrider\EventSubscriber;use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\SerializerInterface;
use Drupal\rest\EventSubscriber\ResourceResponseSubscriber;
use Drupal\rest\ResourceResponseInterface;/**
* Override to set CSV filename when exported.
*/
class AltResourceResponseSubscriber extends ResourceResponseSubscriber { protected function renderResponseBody(Request $request, ResourceResponseInterface $response,
SerializerInterface $serializer, $format) { // Call the original function.
$result = parent::renderResponseBody(
$request, $response, $serializer, $format); // If CSV, set the file name.
if ($format == 'csv') {
// "/path1/path2/param1" --> "path1-path2-param1".
$filename = str_replace('/', '-',
substr($request->getPathInfo(), 1)); $response->headers->set('Content-Disposition',
'attachment; filename="' . $filename . '.csv"');
}
return $result;
}
}
Ready to override some services?
Remember: 2 lines of code to swap out the old for the new, then a (hopefully) small class that extends the original implementation that you wish to alter.
It’s pretty easy really.