• Blog >
  • Sharing content part 2

We’re back for Part 2 of this content and search sharing project, so let’s have a quick recap:

In Part 1, we set up migrations to pull in regional content to the national site and configured a Solr core to index them for location searches. Then we altered the Solr queries, so that the default behaviour only shows content from the current site.

The remaining step is to render the search results that come from other sites. The main problem here is that the default Drupal/Solr/Views integration uses Solr to find the content before rendering it through Drupal’s rendering engine. However, the regional sites are unable to render content from the national site... at least not through the usual process.

 

Give me what I want

Once again, we can use Drupal 8’s class structure to our benefit. Views uses the Drupal\search_api\Plugin\views\row\SearchApiRow class to render Search API results.

We extended this with a custom class, called RemoteSearchApiRow. This class is far more complex than the previous classes we have discussed; it also contains some project-specific details, so I won’t recreate it all here. However, I will briefly mention the principles behind the general concept.

In the overridden render() method, we first check whether the content exists on the current site:

$uuid = $row->_item->getExtraData('search_api_solr_document')->getFields()['ss_uuid'];
if ($uuid) {
 $loaded_entity = $this->getEntityTypeManager()
   ->getStorage('node')
   ->loadByProperties(['uuid' => $uuid]);
}

If so, we simply call it parent::render($row) and let the normal SearchApiRow class handle the rendering. If not, we attempt to fetch the rendered content via a custom service

The final piece of the puzzle is to create the API for the service to access on the national site. Here, we fall back on the always reliable Views module, now part of Drupal 8 core.

The View in question uses the REST Export display plugin, the Fields row formatter and a contextual filter, to accept the UUID as part of the URL. The View then renders the output in the desired view mode (as content types and view modes are shared across all of the sites). Back in our RemoteSearchApiRow class, we simply return this parsed output in a render array to display it to the user.

 

Service? What service?

Okay, there was a big jump in that description. However, the custom service we created was not particularly complicated. Firstly, a custom service was used so that the code accessing this second API could be reused in multiple places, in a decoupled manner.

Additionally, this allowed us to take advantage of dependency injection to get an HTTP client. The code accessing this API doesn’t care how the HTTP request is performed, only that it can perform the request and receive the output. Dependency injection also means that we can replace this HTTP request with a much simpler, mocked version for testing.


Defining the service looks like this in wildlife_search.services.yml:

services:
 wildlife_search.localapi_field_values:
   class: Drupal\wildlife_search\WildlifeSearchLocalApiFieldValues
   arguments: ['@http_client']

The class itself was in a file called WildlifeSearchLocalApiFieldValues.php and contained only one property and two methods. The $httpClient property keeps a reference to the HTTP client passed to the constructor.

 /**
  * HTTP Client.
  *
  * @var \GuzzleHttp\ClientInterface $httpClient
  */
 protected $httpClient;

/**
  * WildlifeSearchLocalApiFieldValues constructor.
  *
  * @param \GuzzleHttp\ClientInterface $http_client
  */
 public function __construct(ClientInterface $http_client) {
   $this->httpClient = $http_client;
 }


The main method is called getFields() and is, essentially, a wrapper for an HTTP client that is compatible with \GuzzleHttp\ClientInterface. Using the domain and UUID passed to the method, it constructs a URL and uses the client to request it. It then does some very simple parsing and error handling.

 /**
  * Get the remote fields for a given UUID.
  *
  * @param $uuid
  *
  * @return array|null
  */
 public function getFields($uuid, $domain = NULL) {
   $remote_field_values = [];

   if ($domain) {
     $config_domains = [
       ['url' => $domain]
     ];
   }
   else {
     $config_domains = \Drupal::config('wildlife_sharing.settings')->get('domains');
   }

   foreach ($config_domains as $config_domain) {
     try {
       $file_contents = $this->httpClient->get($config_domain['url'] . '/localapi/' . $uuid);
     }
     catch (\Exception $e) {
       // If anything goes wrong with the request, assume it doesn't exist on
       // that domain and try the next one.
       continue;
     }

     // If we received a valid response, stop searching.
     if ($file_contents->getStatusCode() == '200') {
       // If we didn't find a valid file then stop processing.
       if (!empty($file_contents)) {
         $parsed = \GuzzleHttp\json_decode($file_contents->getBody());

         if (!empty($parsed)) {
           $remote_field_values['used_domain'] = $config_domain['url'];
           break;
         }
       }
     }
   }

   // If we didn't find a valid file then stop processing.
   if (empty($parsed)) {
     return NULL;
   }

   // Get the site language.
   $language = \Drupal::languageManager()->getCurrentLanguage()->getName();

   // Loop through each of the returned item translations.
   foreach ($parsed as $key => $translation) {
     // If a translation exists for the current language, use it.
     if ($translation->langcode == $language) {
       $item = $parsed[$key];
     }
   }

   // If there is no translation for the given item and language, just return
   // the first parsed item.
   if (!isset($item)) {
     $item = reset($parsed);
   }

   // If the parsed result is empty then the entity doesn't exist on
   // the remote site so return NULL.
   if (empty($item)) {
     return NULL;
   }

   $remote_field_values['item'] = $item;

   return $remote_field_values;
 }
}

And there we have it: content shared between a central national site and multiple regional sites, that can be searched from any of the sites and linked back to the original site.

By using a combination of Drupal and custom services, we were able to tackle the management of a vast quantity of content, without risking duplication or unnecessary storage. The Wildlife Trusts national site is now live and, with the staggered launches of the regional sites, the content sharing system is fully functioning.

Read more about our work on this project in the full Wildlife Trusts Case Study.

 

● ● ●