Drupal added oEmbed support to the Media module in Drupal 8.6.0. See the Drupal.org change record for more details. While having oEmbed support in the Drupal core is great, we discovered some limitations that, as of writing this blog post, haven’t been resolved.
RELATED READ
Top 5 Things You Need To Know To Speak DrupalWhat is oEmbed?
According to oembed.com:
What does that mean? To put it simply, it allows a website's content to be embedded into another page. You have likely seen many examples without even realizing. Twitter, Instagram, YouTube, Vimeo, and far too many others to list, all support the oEmbed standard.
RELATED READ
Supercharged SEO with Drupal 8What’s wrong with the Drupal 8 implementation?
As of writing this post, Drupal does not support the ability to extend or alter some of the functionality. This is where the problem lies. Drupal only supports oEmbed providers listed in the oEmbed providers database at https://oembed.com/providers.json. While that sounds adequate, what if you want to add a custom provider? Or a provider not listed in providers JSON? There is no hook or plugin type to do so.
The other issue that I encountered is that not all oEmbed providers adhere to the specification. Height and/or width parameters are provided as a null value instead of a value in pixels. Drupal requires both a height and width value. If an oEmbed provider returns a null value for the height or width an error will be thrown: "The dimensions must be numbers greater than zero." There is no way to get around this or provide a default value.
What did I need?
Twitter supports oEmbed for Tweets, Timelines and Moments. The first obstacle is that the oEmbed providers database does not include support for Timelines or Moments. I needed to add Twitter Moments to the list of providers in order for them to be supported by the Drupal Media module.
The second obstacle is that Twitter is providing a null value for the height and/or width of the resources. For Timelines, the height and width are both null. For Moments, the width is defined, but the height is null.
Based on the above, in its current state Drupal does not, and can not support both Twitter Timelines and Moments. Fortunately, existing services can be altered.
Altering the core services
To alter/extend a service you need to create a class that extends the ServiceProviderBase
class that implements the alter()
method. In order for the service alteration to be discovered automatically, the class must be in the top-level namespace of your module. The name of the class must be a camel case version of your module's machine name directly followed by ServiceProvider
. For example, in my case, our module's name is savas_labs
. The class namespace will be Drupal\savas_labs
and the class name will be SavasLabsServiceProvider
. For more information about altering services and service decoration, see the documentation on Drupal.org.
Knowing that I'm able to alter services, the two services that I had to make changes to are:
- Service:
media.oembed.provider_repository
Class:\Drupal\media\OEmbed\ProviderRepository
This service is responsible for fetching the list of providers from oembed.org. We need to invoke an alter hook after the providers have been fetched. - Service:
media.oembed.resource_fetcher
Class:\Drupal\media\OEmbed\ResourceFetcher
This service is responsible for fetching and caching the oEmbed resources. We need to be able to alter the resource to add a static value for the height.
<?php
namespace Drupal\savas_labs;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
use Symfony\Component\DependencyInjection\Reference;
/**
* Class SavasLabsServiceProvider.
*
* @package Drupal\savas_labs
*/
class SavasLabsServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
// Modify the provider repository so we can alter the list of providers.
$definition = $container->getDefinition('media.oembed.provider_repository');
$definition->setClass('Drupal\savas_labs\OEmbed\SavasLabsProviderRepository')
->addArgument(new Reference('module_handler'));
// Modify the resource fetcher so we can alter the oembed data.
$definition = $container->getDefinition('media.oembed.resource_fetcher');
$definition->setClass('Drupal\savas_labs\OEmbed\SavasLabsResourceFetcher')
->addArgument(new Reference('module_handler'));
}
}
The above example replaces the \Drupal\media\OEmbed\ProviderRepository
class with \Drupal\savas_labs\OEmbed\SavasLabsProviderRepository
and the \Drupal\media\OEmbed\ResourceFetcher
class with \Drupal\savas_labs\OEmbed\SavasLabsResourceFetcher
.
We’re also adding a new argument to each service. The module_handler
will allow us to invoke the necessarily alter hooks.
The SavasLabsProviderRepository
class will extend the \Drupal\media\OEmbed\ProviderRepository
class. Be sure to update the class constructor (adding the module_handler
service as a function parameter and setting the variable). Next, I needed to alter the providers after they had been fetched, but before they were instantiated as \Drupal\media\OEmbed\Provider
classes and cached. Unfortunately, the best way to do so is to completely override the ProviderRepository::getAll()
method. I copied the entire method and pasted it in the new SavasLabsProviderRepository
class. I added:
// Allow other modules to alter the list of providers.
$providers = $this->alterProviders($providers);
Directly after the following code.
$providers = Json::decode((string) $response->getBody());
if (!is_array($providers) || empty($providers)) {
throw new ProviderException('Remote oEmbed providers database returned invalid or empty list.');
}
The SavasLabsProviderRepository::alterProviders()
method is below.
/**
* Allow modules to alter the list of providers.
*
* @param array $providers
* An array of providers.
*
* @return array
* The updated array of providers.
*/
protected function alterProviders(array $providers) {
$keyed_providers = [];
foreach ($providers as $provider) {
$name = (string) $provider['provider_name'];
$keyed_providers[$name] = $provider;
}
$this->moduleHandler->alter('savas_labs_oembed_providers', $keyed_providers);
return $keyed_providers;
I then implemented this new hook (hook_savas_labs_oembed_providers_alter
) in our savas_labs
module:
/**
* Implements hook_savas_labs_oembed_providers_alter().
*/
function savas_labs_savas_labs_oembed_providers_alter(&$providers) {
foreach ($providers['Twitter']['endpoints'] as &$endpoint) {
if ('https://publish.twitter.com/oembed' == $endpoint['url']) {
$endpoint['schemes'][] = 'https://twitter.com/i/moments/*';
$endpoint['schemes'][] = 'https://*.twitter.com/i/moments/*';
}
}
The above code adds support for Twitter Moments by adding the URLs to the supported URL schemes for Twitter.
Now to fix the next issue: the null value for the resource height. As I did with the SavasLabsProviderRepository
class, the SavasLabsResourceFetcher
class extended the \Drupal\media\OEmbed\ResourceFetcher
class. The class constructor was updated (adding the module_handler
service) here as well. In this case, I need to update the resource after it has been fetched, but before it’s cached. Again, the best way to do so was to override the ResourceFetcher::fetchResource()
method in the SavasLabsResourceFetcher
class. I added:
// Allow other modules to alter the data.
$provider = $data['provider_name'] ? $this->providers->get($data['provider_name']) : NULL;
$this->moduleHandler->alter('savas_labs_oembed_resource_data', $data, $provider);
Just before:
$this->cacheSet($cache_id, $data);
One thing to note with this new hook (hook_savas_labs_oembed_resource_data_alter
) is that a variable is passed for the provider \Drupal\media\OEmbed\Provider $provider
if we can match it. That way in our module I can check to see if the provider is Twitter before modifying the height. I then implemented the hook in our module:
/**
* Implements hook_savas_labs_oembed_resource_data_alter().
*/
function savas_labs_savas_labs_oembed_resource_data_alter(&$data, Provider $provider = NULL) {
if ($provider && 'Twitter' == $provider->getName()) {
if (empty($data['height'])) {
$data['height'] = 440;
}
}
}
Our use case is quite simple. Check to see if the provider is Twitter. If yes and there is no height, set it to 440px. More complex requirements could certainly be added here.
That’s it! With those two new hooks (hook_savas_labs_oembed_providers_alter
and hook_savas_labs_oembed_resource_data_alter
) I was able to solve our problems.
Looking forward
Both of these issues have been discussed within the Drupal community and there are efforts to add similar functionality to the Drupal core. I’ve linked the relevant Drupal issues below.
Provide hook_oembed_providers_alter()
https://www.drupal.org/project/drupal/issues/3008119
The oembed Resource value object should be more permissive for NULL dimensions
https://www.drupal.org/project/drupal/issues/3071682