Integrating remote data into Drupal 7 and exposing it to Views
Drupal's strength as a content management framework is in its ability to effectively manage and display structured content through its Web user interface. However, the out-of-the-box system assumes all data is local (stored in the database). This can present challenges when attempting to integrate remote data stored in other systems. You cannot, by default, display non-local records as pages. While setting this up is in itself a challenge, it is an even bigger challenge to manipulate, aggregate and display this data through Views.
I've split this article into the following sections and subsections. Click on any of these to jump directly to the one of them.
- Introduction
- What's Changed
- Architecture
- Views support
- Alternatives
- References
This exposition is effectively a follow-up to some excellent articles from years past:
- Larry Garfield's Remote Data in Drupal: Museums and the Web (2009)
- Florian Loretan's Remote entities in Drupal 7 (2012)
I'd recommend reading them for background information.
The first article (written in the Drupal 6 days) describes a "Wipe/rebuild import" method (Method 3) to bring remote data into Drupal. That's basically what we'll be discussing here, but there is now a standard method for doing so. What's interesting is that future plans mentioned there included per-field storage engines (with some being remote). The idea never made it very far. This is most likely because grabbing field data from multiple locations is far too inefficient (multiple Web-service calls) compared to fetching an entire record from a single location.
Taking a look at the second article, you can now see that Drupal 7 is dominant, and we have more tools at our disposal, but at the time this one was written, we still didn't have all of them in place. We did, however, have the following APIs for dealing with entities.
We now have another API, the Remote Entity API, which was inspired by Florian's article. As you can imagine, this API is dependent on the Entity API which is in turn dependent on the Drupal Core's entity functionality.
I recently added support for this new API to EntityFieldQuery Views Backend, the module allowing Views to work with data stored outside of the local SQL database. Previously, it supported non-SQL data, but still assumed that this data was local. Tying these two components together gives us what we need to achieve our goal.
So we really need to take advantage of the three (3) entity APIs to load and display individual remote records.
- The entity API in Drupal Core
- The Entity API contributed module
- The Remote Entity API contributed module
The first provides basic entity functionality in Drupal. The second adds enhanced functionality for custom entities. The third and final API adds additional handling mechanisms for working with any remote data.
We'll need the following contributed modules to make all of this work.
- Chaos tool suite (ctools)
- Entity API
- Web Service Clients
- Remote Entity API
- Views
- EntityFieldQuery Views Backend
In addition to the above, a new custom module is necessary. I recommend something like siteshortname_entities_remote for the machine name. You can have another one, siteshortname_entities_local, for local entities without all of the remote code if necessary. In the .info file, add remote_entity (the Remote Entity API) as a dependency.
You'll want to divide your module file into at least three (3) parts:
- Entity APIs: Code for defining remote entities through any of the above entity APIs. (Part I)
- Drupal Core: Code for implementing Drupal Core hooks. This is basically a hook_menu() implementation with some helper functions to get your entity instances to show up at specific paths based on the remote entity IDs. (Part II)
- Web Service Clients: Code for implementing what's necessary for the Web Service Clients module, a prerequisite for the Remote Entity API. It's essentially the external communications component for accessing your remote data. (Part III)
Most of the code will be in PHP class files you'll want in a classes subdirectory (autoloaded by defining these in your .info file), but you'll still need some code in your main module file.
We'll be adding only one new entity in this exercise, but the code is extensible enough to allow for more. Once one of these is set up, adding more is (in most cases) trivial.
Your basic remote entity definitions will exist in the Entity APIs section of your module file, Part I. Within the hook_entity_info() implementation, you'll see that different properties within the definition will be used by different layers, the three APIs.
For the following examples, let's assume we have a remote event data type.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/****************************************************************************<br> ** Entity APIs<br> ****************************************************************************/<br><br>/**<br> * Implements hook_entity_info().<br> *<br> * @todo Add 'bundles' for different types of remote content.<br> * @todo Add 'entity keys' => 'needs remote save' if remote saving required.<br> * @todo Remove 'static cache' and 'field cache' settings after development.<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">siteshortname_entities_remote_entity_info</span><span style="color: #007700">() {<br> </span><span style="color: #0000BB">$entities</span><span style="color: #007700">[</span><span style="color: #DD0000">'siteshortname_entities_remote_event'</span><span style="color: #007700">] = array(<br><br> </span><span style="color: #FF8000">// Core properties.<br> </span><span style="color: #DD0000">'label' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Event'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'controller class' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'RemoteEntityAPIDefaultController'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'base table' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'siteshortname_entities_remote_events'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'uri callback' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'entity_class_uri'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'label callback' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'remote_entity_entity_label'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'fieldable' </span><span style="color: #007700">=> </span><span style="color: #0000BB">FALSE</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'entity keys' </span><span style="color: #007700">=> array(<br> </span><span style="color: #DD0000">'id' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'eid'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'label' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'event_name'</span><span style="color: #007700">,<br> ),<br> </span><span style="color: #DD0000">'view modes' </span><span style="color: #007700">=> array(<br> </span><span style="color: #DD0000">'full' </span><span style="color: #007700">=> array(<br> </span><span style="color: #DD0000">'label' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Full content'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'custom settings' </span><span style="color: #007700">=> </span><span style="color: #0000BB">FALSE</span><span style="color: #007700">,<br> ),<br> ),<br> </span><span style="color: #DD0000">'static cache' </span><span style="color: #007700">=> </span><span style="color: #0000BB">FALSE</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'field cache' </span><span style="color: #007700">=> </span><span style="color: #0000BB">FALSE</span><span style="color: #007700">,<br><br> </span><span style="color: #FF8000">// Entity API properties.<br> </span><span style="color: #DD0000">'entity class' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'SiteshortnameEvent'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'module' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'siteshortname_entities_remote'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'metadata controller class' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'RemoteEntityAPIDefaultMetadataController'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'views controller class' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'EntityDefaultViewsController'</span><span style="color: #007700">,<br><br> </span><span style="color: #FF8000">// Remote Entity API properties.<br> </span><span style="color: #DD0000">'remote base table' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'siteshortname_entities_remote_events'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'remote entity keys' </span><span style="color: #007700">=> array(<br> </span><span style="color: #DD0000">'remote id' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'event_id'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'label' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'event_name'</span><span style="color: #007700">,<br> ),<br> </span><span style="color: #DD0000">'expiry' </span><span style="color: #007700">=> array(<br> </span><span style="color: #FF8000">// Number of seconds before a locally cached instance must be refreshed<br> // from the remote source.<br> </span><span style="color: #DD0000">'expiry time' </span><span style="color: #007700">=> </span><span style="color: #0000BB">600</span><span style="color: #007700">,<br> </span><span style="color: #FF8000">// A boolean indicating whether or not to delete expired local entities<br> // on cron.<br> </span><span style="color: #DD0000">'purge' </span><span style="color: #007700">=> </span><span style="color: #0000BB">FALSE</span><span style="color: #007700">,<br> ),<br> );<br><br> </span><span style="color: #FF8000">// Get the property map data.<br> </span><span style="color: #0000BB">$remote_properties </span><span style="color: #007700">= </span><span style="color: #0000BB">siteshortname_entities_remote_get_remote_properties</span><span style="color: #007700">();<br><br> </span><span style="color: #FF8000">// Assign each map to its corresponding entity.<br> </span><span style="color: #007700">foreach (</span><span style="color: #0000BB">$entities </span><span style="color: #007700">as </span><span style="color: #0000BB">$key </span><span style="color: #007700">=> </span><span style="color: #0000BB">$einfo</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$entities</span><span style="color: #007700">[</span><span style="color: #0000BB">$key</span><span style="color: #007700">][</span><span style="color: #DD0000">'property map'</span><span style="color: #007700">] =<br> </span><span style="color: #0000BB">drupal_map_assoc</span><span style="color: #007700">(</span><span style="color: #0000BB">array_keys</span><span style="color: #007700">(</span><span style="color: #0000BB">$remote_properties</span><span style="color: #007700">[</span><span style="color: #0000BB">$key</span><span style="color: #007700">]));<br> }<br><br> </span><span style="color: #FF8000">// Return all of the entity information.<br> </span><span style="color: #007700">return </span><span style="color: #0000BB">$entities</span><span style="color: #007700">;<br>}<br></span><span style="color: #0000BB">?></span></span>
Notes
- Just like the entity type node, which is subdivided into content types (generically referred to as bundles in Drupal-speak), we can subdivide remote entities into their own bundles. In this case, we could have a "High-school event" bundle and a "College event" bundle that vary slightly, but instances of both would still be members of the entity type Event. We won't be setting this up here though.
- In this article, we won't be covering remote saving (only remote loading), but it is possible through the remote API.
- Make sure to adjust the cache settings properly once development is complete.
- Detailed documentation on the APIs is available for the Core entity API, the Entity API, and the the Remote Entity API.
As we're not using the Field API to attach information to our entities, we need to do it with properties. The code below exposes the data we'll define shortly.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/**<br> * Implements hook_entity_property_info_alter().<br> *<br> * This is needed to use wrappers to access the remote entity<br> * data in the entity_data property of remote entities.<br> *<br> * @see: Page 107 of the Programming Drupal 7 Entities book. The code below is<br> * a variation on it.<br> * @todo: Remove whenever this gets added to the remote_entity module.<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">siteshortname_entities_remote_entity_property_info_alter</span><span style="color: #007700">(&</span><span style="color: #0000BB">$info</span><span style="color: #007700">) {<br><br> </span><span style="color: #FF8000">// Set the entity types and get their properties.<br> </span><span style="color: #0000BB">$entity_types </span><span style="color: #007700">= array(<br> </span><span style="color: #DD0000">'siteshortname_entities_remote_event'</span><span style="color: #007700">,<br> );<br><br> </span><span style="color: #0000BB">$remote_properties </span><span style="color: #007700">= </span><span style="color: #0000BB">siteshortname_entities_remote_get_remote_properties</span><span style="color: #007700">();<br><br> </span><span style="color: #FF8000">// Assign the property data to each entity.<br> </span><span style="color: #007700">foreach (</span><span style="color: #0000BB">$entity_types </span><span style="color: #007700">as </span><span style="color: #0000BB">$entity_type</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$properties </span><span style="color: #007700">= &</span><span style="color: #0000BB">$info</span><span style="color: #007700">[</span><span style="color: #0000BB">$entity_type</span><span style="color: #007700">][</span><span style="color: #DD0000">'properties'</span><span style="color: #007700">];<br> </span><span style="color: #0000BB">$entity_data </span><span style="color: #007700">= &</span><span style="color: #0000BB">$properties</span><span style="color: #007700">[</span><span style="color: #DD0000">'entity_data'</span><span style="color: #007700">];<br> </span><span style="color: #0000BB">$pp </span><span style="color: #007700">= &</span><span style="color: #0000BB">$remote_properties</span><span style="color: #007700">[</span><span style="color: #0000BB">$entity_type</span><span style="color: #007700">];<br> </span><span style="color: #0000BB">$entity_data</span><span style="color: #007700">[</span><span style="color: #DD0000">'type'</span><span style="color: #007700">] = </span><span style="color: #DD0000">'remote_entity_' </span><span style="color: #007700">. </span><span style="color: #0000BB">$entity_type</span><span style="color: #007700">;<br><br> </span><span style="color: #FF8000">// Set the default getter callback for each property.<br> </span><span style="color: #007700">foreach (</span><span style="color: #0000BB">$pp </span><span style="color: #007700">as </span><span style="color: #0000BB">$key </span><span style="color: #007700">=> </span><span style="color: #0000BB">$pinfo</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$pp</span><span style="color: #007700">[</span><span style="color: #0000BB">$key</span><span style="color: #007700">][</span><span style="color: #DD0000">'getter callback'</span><span style="color: #007700">] = </span><span style="color: #DD0000">'entity_property_verbatim_get'</span><span style="color: #007700">;<br> }<br><br> </span><span style="color: #FF8000">// Assign the updated property info to the entity info.<br> </span><span style="color: #0000BB">$entity_data</span><span style="color: #007700">[</span><span style="color: #DD0000">'property info'</span><span style="color: #007700">] = </span><span style="color: #0000BB">$pp</span><span style="color: #007700">;<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
Remote property definition
This is where we define the field (or in this case property) information, the data attached to each entity, that we exposed above.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/**<br> * Get remote property information for remote entities.<br> *<br> * @return<br> * An array of property information keyed by entity type.<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">siteshortname_entities_remote_get_remote_properties</span><span style="color: #007700">() {<br><br> </span><span style="color: #FF8000">// Initialize a list of entity properties.<br> </span><span style="color: #0000BB">$properties </span><span style="color: #007700">= array();<br><br> </span><span style="color: #FF8000">// Define properties for the entity type.<br> </span><span style="color: #0000BB">$properties</span><span style="color: #007700">[</span><span style="color: #DD0000">'siteshortname_entities_remote_event'</span><span style="color: #007700">] = array(<br><br> </span><span style="color: #FF8000">// Event information.<br> </span><span style="color: #DD0000">'event_id' </span><span style="color: #007700">=> array(<br> </span><span style="color: #DD0000">'label' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Remote Event ID'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'integer'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'description' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'The remote attribute "id".'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'views' </span><span style="color: #007700">=> array(<br> </span><span style="color: #DD0000">'filter' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'siteshortname_entities_remote_views_handler_filter_event_id'</span><span style="color: #007700">,<br> ),<br> ),<br> </span><span style="color: #DD0000">'event_date' </span><span style="color: #007700">=> array(<br> </span><span style="color: #DD0000">'label' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Date'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'date'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'description' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'The remote attribute "date".'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'views' </span><span style="color: #007700">=> array(<br> </span><span style="color: #DD0000">'filter' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'siteshortname_entities_remote_views_handler_filter_event_date'</span><span style="color: #007700">,<br> ),<br> ),<br> </span><span style="color: #DD0000">'event_details' </span><span style="color: #007700">=> array(<br> </span><span style="color: #DD0000">'label' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Details'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'text'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'description' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'The remote attribute "details".'</span><span style="color: #007700">,<br> ),<br> );<br><br> </span><span style="color: #FF8000">// Return all of the defined property info.<br> </span><span style="color: #007700">return </span><span style="color: #0000BB">$properties</span><span style="color: #007700">;<br>}<br></span><span style="color: #0000BB">?></span></span>
Notes
- Try to remember the distinction between local and remote entity IDs. At the moment, we're only interested in remote properties so we don't don't need to worry about local IDs just yet.
- Don't worry too much about the Views filters. These are Views filter handler classes. They're only necessary if you'd like custom filters for the respective properties.
This starts the Core Hooks section of the module file, Part II. In this section, we're providing each remote data instance as a Web page just like standard local content within Drupal via nodes.
The hook_menu() implementation responds to hits to the event/EVENT_ID path, loads the object, themes all of the data, and then returns it for display as a page. We're assuming all of your HTML output will be in a template in the includes/siteshortname_entities_remote.theme.inc file in your module's directory.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/****************************************************************************<br> ** Drupal Core<br> ****************************************************************************/<br><br>/**<br> * Implements hook_menu().<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">siteshortname_entities_remote_menu</span><span style="color: #007700">() {<br> </span><span style="color: #0000BB">$items </span><span style="color: #007700">= array();<br><br> </span><span style="color: #0000BB">$items</span><span style="color: #007700">[</span><span style="color: #DD0000">'event/%siteshortname_entities_remote_event'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'title' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Remote Event'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'page callback' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'siteshortname_entities_remote_event_view'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'page arguments' </span><span style="color: #007700">=> array(</span><span style="color: #0000BB">1</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'access arguments' </span><span style="color: #007700">=> array(</span><span style="color: #DD0000">'access content'</span><span style="color: #007700">),<br> );<br><br> return </span><span style="color: #0000BB">$items</span><span style="color: #007700">;<br>}<br><br></span><span style="color: #FF8000">/**<br> * Menu autoloader wildcard for path 'event/REMOTE_ID'.<br> *<br> * @see hook_menu() documentation.<br> * @param $remote_id<br> * The remote ID of the record to load.<br> * @return<br> * The loaded object, or FALSE on failure.<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">siteshortname_entities_remote_event_load</span><span style="color: #007700">(</span><span style="color: #0000BB">$remote_id</span><span style="color: #007700">) {<br> return </span><span style="color: #0000BB">remote_entity_load_by_remote_id</span><span style="color: #007700">(</span><span style="color: #DD0000">'siteshortname_entities_remote_event'</span><span style="color: #007700">, </span><span style="color: #0000BB">$remote_id</span><span style="color: #007700">);<br>}<br><br></span><span style="color: #FF8000">/**<br> * Page callback for path 'event/%remote_id'.<br> *<br> * @param $event<br> * The auto-loaded object.<br> * @return<br> * The themed output for the event page.<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">siteshortname_entities_remote_event_view</span><span style="color: #007700">(</span><span style="color: #0000BB">$event</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$fullname </span><span style="color: #007700">= </span><span style="color: #0000BB">$event</span><span style="color: #007700">-></span><span style="color: #0000BB">name</span><span style="color: #007700">;<br> </span><span style="color: #0000BB">drupal_set_title</span><span style="color: #007700">(</span><span style="color: #0000BB">$fullname</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$event_output </span><span style="color: #007700">= </span><span style="color: #0000BB">theme</span><span style="color: #007700">(</span><span style="color: #DD0000">'siteshortname_entities_remote_event'</span><span style="color: #007700">, array(<br> </span><span style="color: #DD0000">'event' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$event</span><span style="color: #007700">,<br> ));<br> return </span><span style="color: #0000BB">$event_output</span><span style="color: #007700">;<br>}<br><br></span><span style="color: #FF8000">/**<br> * Implements hook_theme().<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">siteshortname_entities_remote_theme</span><span style="color: #007700">() {<br> return array(<br> </span><span style="color: #DD0000">'siteshortname_entities_remote_event' </span><span style="color: #007700">=> array(<br> </span><span style="color: #DD0000">'variables' </span><span style="color: #007700">=> array(</span><span style="color: #DD0000">'event' </span><span style="color: #007700">=> </span><span style="color: #0000BB">NULL</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'file' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'includes/siteshortname_entities_remote.theme.inc'</span><span style="color: #007700">,<br> ),<br> );<br>}<br></span><span style="color: #0000BB">?></span></span>
There's one more thing to do here. In our hook_entity_info() implementation, we stated the following:
<span style="color: #000000"><span style="color: #0000BB"><?php<br> </span><span style="color: #DD0000">'entity class' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'SiteshortnameEvent'</span><span style="color: #007700">,<br></span><span style="color: #0000BB">?></span></span>
We could have used Entity here instead of SiteshortnameEvent, but we want a custom class here so that we can override the URL path for these entities. So add the following class:
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">class </span><span style="color: #0000BB">SiteshortnameEvent </span><span style="color: #007700">extends </span><span style="color: #0000BB">Entity </span><span style="color: #007700">{<br> </span><span style="color: #FF8000">/**<br> * Override defaultUri().<br> */<br> </span><span style="color: #007700">protected function </span><span style="color: #0000BB">defaultUri</span><span style="color: #007700">() {<br> return array(</span><span style="color: #DD0000">'path' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'event/' </span><span style="color: #007700">. </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">remote_id</span><span style="color: #007700">);<br> }<br>} <br></span><span style="color: #0000BB">?></span></span>
Web services integration
We're now onto Part III, setting up Web-service endpoints and associating remote resources with entities. This is done through the implementation of a few Web Service Clients hooks.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/****************************************************************************<br> ** Web Service Clients<br> ****************************************************************************/<br><br>/**<br> * Implements hook_clients_connection_type_info().<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">siteshortname_entities_remote_clients_connection_type_info</span><span style="color: #007700">() {<br> return array(<br> </span><span style="color: #DD0000">'our_rest' </span><span style="color: #007700">=> array(<br> </span><span style="color: #DD0000">'label' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'REST Data Services'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'description' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Connects to our data service using REST endpoints.'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'tests' </span><span style="color: #007700">=> array(<br> </span><span style="color: #DD0000">'event_retrieve_raw' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'SiteshortnameEntitiesRemoteConnectionTestEventRetrieveRaw'</span><span style="color: #007700">,<br> ),<br> </span><span style="color: #DD0000">'interfaces' </span><span style="color: #007700">=> array(<br> </span><span style="color: #DD0000">'ClientsRemoteEntityInterface'</span><span style="color: #007700">,<br> ),<br> ),<br> );<br>}<br><br></span><span style="color: #FF8000">/**<br> * Implements hook_clients_default_connections().<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">siteshortname_entities_remote_clients_default_connections</span><span style="color: #007700">() {<br><br> </span><span style="color: #0000BB">$connections</span><span style="color: #007700">[</span><span style="color: #DD0000">'my_rest_connection'</span><span style="color: #007700">] = new </span><span style="color: #0000BB">clients_connection_our_rest</span><span style="color: #007700">(array(<br> </span><span style="color: #DD0000">'endpoint' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'https://data.example.com'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'configuration' </span><span style="color: #007700">=> array(<br> </span><span style="color: #DD0000">'username' </span><span style="color: #007700">=> </span><span style="color: #DD0000">''</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'password' </span><span style="color: #007700">=> </span><span style="color: #DD0000">''</span><span style="color: #007700">,<br> ),<br> </span><span style="color: #DD0000">'label' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Our REST Service'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'our_rest'</span><span style="color: #007700">,<br> ), </span><span style="color: #DD0000">'clients_connection'</span><span style="color: #007700">);<br><br> return </span><span style="color: #0000BB">$connections</span><span style="color: #007700">;<br>}<br><br></span><span style="color: #FF8000">/**<br> * Implements hook_clients_default_resources().<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">siteshortname_entities_remote_clients_default_resources</span><span style="color: #007700">() {<br> </span><span style="color: #0000BB">$resources</span><span style="color: #007700">[</span><span style="color: #DD0000">'siteshortname_entities_remote_event'</span><span style="color: #007700">] = new </span><span style="color: #0000BB">clients_resource_remote_entity</span><span style="color: #007700">(array(<br> </span><span style="color: #DD0000">'component' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'siteshortname_entities_remote_event'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'connection' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'my_rest_connection'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'label' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Resource for remote events'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'remote_entity'</span><span style="color: #007700">,<br> ), </span><span style="color: #DD0000">'clients_resource'</span><span style="color: #007700">);<br><br> return </span><span style="color: #0000BB">$resources</span><span style="color: #007700">;<br>}<br></span><span style="color: #0000BB">?></span></span>
In the first function, we're adding metadata for the connection. In the second one, we're setting the endpoint and its credentials. The third function is what ties our remote entity, defined earlier, with the remote resource. There's some information on this documentation page, but there's more in the README file.
We'll need to store the remote data in a local table as a non-authoritative cache. The frequency with which it gets refreshed is up to you, as described earlier in this article. We'll need one table per entity. The good news is that we don't need to worry about the details; this is handled by the Remote Entity API. It provides a function returning the default schema. If you want to do anything different here, you are welcome to define your own.
The argument provided in the call is used for the table description as "The base table for [whatever you provide]". This will go in your siteshortname_entities_remote.install file.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/**<br> * Implementation of hook_schema().<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">siteshortname_entities_remote_schema</span><span style="color: #007700">() {<br> </span><span style="color: #0000BB">$schema </span><span style="color: #007700">= array(<br> </span><span style="color: #DD0000">'siteshortname_entities_remote_events' </span><span style="color: #007700">=> </span><span style="color: #0000BB">remote_entity_schema_table</span><span style="color: #007700">(</span><span style="color: #DD0000">'our remote event entity type'</span><span style="color: #007700">),<br> );<br><br> return </span><span style="color: #0000BB">$schema</span><span style="color: #007700">;<br>}<br></span><span style="color: #0000BB">?></span></span>
If you don't actually want to save one or more of your remote entities locally (say because you have private data you'd rather not have stored on your publicly-accessible Web servers), you can alter this default behaviour by defining your own controller which overrides the save() method.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/**<br> * Entity controller extending RemoteEntityAPIDefaultController<br> *<br> * For most of our cases the default controller is fine, but we can use<br> * this one for entities we don't want stored locally. Override the save<br> * behaviour and do not keep a local cached copy.<br> */<br></span><span style="color: #007700">class </span><span style="color: #0000BB">SiteshortnameEntitiesRemoteNoLocalAPIController </span><span style="color: #007700">extends </span><span style="color: #0000BB">RemoteEntityAPIDefaultController </span><span style="color: #007700">{<br><br> </span><span style="color: #FF8000">/**<br> * Don't actually save anything.<br> */<br> </span><span style="color: #007700">public function </span><span style="color: #0000BB">save</span><span style="color: #007700">(</span><span style="color: #0000BB">$entity</span><span style="color: #007700">, </span><span style="color: #0000BB">DatabaseTransaction $transaction </span><span style="color: #007700">= </span><span style="color: #0000BB">NULL</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$entity</span><span style="color: #007700">-></span><span style="color: #0000BB">eid </span><span style="color: #007700">= </span><span style="color: #0000BB">uniqid</span><span style="color: #007700">();<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
Implementing the remote connection class
Create a file for the connection class.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/**<br> * @file<br> * Contains the clients_connection_our_rest class.<br> */<br><br>/**<br> * Set up a client connection to our REST services.<br> *<br> * @todo Make private functions private once development is done.<br> */<br></span><span style="color: #007700">class </span><span style="color: #0000BB">clients_connection_our_rest </span><span style="color: #007700">extends </span><span style="color: #0000BB">clients_connection_base<br> </span><span style="color: #007700">implements </span><span style="color: #0000BB">ClientsConnectionAdminUIInterface</span><span style="color: #007700">, </span><span style="color: #0000BB">ClientsRemoteEntityInterface </span><span style="color: #007700">{<br><br>}<br></span><span style="color: #0000BB">?></span></span>
We'll now divide the contents of said file into three (3) sections, ClientsRemoteEntityInterface implementations, clients_connection_base overrides and local methods.
ClientsRemoteEntityInterface implementations
As you can see below, we've got three (3) methods here.
- remote_entity_load() will load a remote entity with the provided remote ID.
- entity_property_type_map() is supposedly required to map remote properties to local ones, but it wasn't clear to me how this gets used.
- getRemoteEntityQuery() returns a query object, either a "select", "insert" or "update" based on whichever one was requested.
<span style="color: #000000"><span style="color: #0000BB"><?php<br> </span><span style="color: #FF8000">/**************************************************************************<br> * ClientsRemoteEntityInterface implementations.<br> **************************************************************************/<br><br> /**<br> * Load a remote entity.<br> *<br> * @param $entity_type<br> * The entity type to load.<br> * @param $id<br> * The (remote) ID of the entity.<br> *<br> * @return<br> * An entity object.<br> */<br> </span><span style="color: #007700">function </span><span style="color: #0000BB">remote_entity_load</span><span style="color: #007700">(</span><span style="color: #0000BB">$entity_type</span><span style="color: #007700">, </span><span style="color: #0000BB">$id</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$query </span><span style="color: #007700">= </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">getRemoteEntityQuery</span><span style="color: #007700">(</span><span style="color: #DD0000">'select'</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$query</span><span style="color: #007700">-></span><span style="color: #0000BB">base</span><span style="color: #007700">(</span><span style="color: #0000BB">$entity_type</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$query</span><span style="color: #007700">-></span><span style="color: #0000BB">entityCondition</span><span style="color: #007700">(</span><span style="color: #DD0000">'entity_id'</span><span style="color: #007700">, </span><span style="color: #0000BB">$id</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$result </span><span style="color: #007700">= </span><span style="color: #0000BB">$query</span><span style="color: #007700">-></span><span style="color: #0000BB">execute</span><span style="color: #007700">();<br><br> </span><span style="color: #FF8000">// There's only one. Same pattern as entity_load_single().<br> </span><span style="color: #007700">return </span><span style="color: #0000BB">reset</span><span style="color: #007700">(</span><span style="color: #0000BB">$result</span><span style="color: #007700">);<br> }<br><br> </span><span style="color: #FF8000">/**<br> * Provide a map of remote property types to Drupal types.<br> *<br> * Roughly analogous to _entity_metadata_convert_schema_type().<br> *<br> * @return<br> * An array whose keys are remote property types as used as types for fields<br> * in hook_remote_entity_query_table_info(), and whose values are types<br> * recognized by the Entity Metadata API (as listed in the documentation for<br> * hook_entity_property_info()).<br> * If a remote property type is not listed here, it will be mapped to 'text'<br> * by default.<br> */<br> </span><span style="color: #007700">function </span><span style="color: #0000BB">entity_property_type_map</span><span style="color: #007700">() {<br> return array(<br> </span><span style="color: #DD0000">'EntityCollection' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'list<string>'</span><span style="color: #007700">,<br> );<br> }<br><br> </span><span style="color: #FF8000">/**<br> * Get a new RemoteEntityQuery object appropriate for the connection.<br> *<br> * @param $query_type<br> * (optional) The type of the query. Defaults to 'select'.<br> *<br> * @return<br> * A remote query object of the type appropriate to the query type.<br> */<br> </span><span style="color: #007700">function </span><span style="color: #0000BB">getRemoteEntityQuery</span><span style="color: #007700">(</span><span style="color: #0000BB">$query_type </span><span style="color: #007700">= </span><span style="color: #DD0000">'select'</span><span style="color: #007700">) {<br> switch (</span><span style="color: #0000BB">$query_type</span><span style="color: #007700">) {<br> case </span><span style="color: #DD0000">'select'</span><span style="color: #007700">:<br> return new </span><span style="color: #0000BB">OurRestRemoteSelectQuery</span><span style="color: #007700">(</span><span style="color: #0000BB">$this</span><span style="color: #007700">);<br> case </span><span style="color: #DD0000">'insert'</span><span style="color: #007700">:<br> return new </span><span style="color: #0000BB">OurRestRemoteInsertQuery</span><span style="color: #007700">(</span><span style="color: #0000BB">$this</span><span style="color: #007700">);<br> case </span><span style="color: #DD0000">'update'</span><span style="color: #007700">:<br> return new </span><span style="color: #0000BB">OurRestRemoteUpdateQuery</span><span style="color: #007700">(</span><span style="color: #0000BB">$this</span><span style="color: #007700">);<br> }<br> }<br></span><span style="color: #0000BB">?></span></span>
Parent overrides
The only method we need to worry about here is callMethodArray(). Basically, it sets up the remote call.
<span style="color: #000000"><span style="color: #0000BB"><?php<br> </span><span style="color: #FF8000">/**************************************************************************<br> * clients_connection_base overrides<br> **************************************************************************/<br><br> /**<br> * Call a remote method with an array of parameters.<br> *<br> * This is intended for internal use from callMethod() and<br> * clients_connection_call().<br> * If you need to call a method on given connection object, use callMethod<br> * which has a nicer form.<br> *<br> * Subclasses do not necessarily have to override this method if their<br> * connection type does not make sense with this.<br> *<br> * @param $method<br> * The name of the remote method to call.<br> * @param $method_params<br> * An array of parameters to passed to the remote method.<br> *<br> * @return<br> * Whatever is returned from the remote site.<br> *<br> * @throws Exception on error from the remote site.<br> * It's up to subclasses to implement this, as the test for an error and<br> * the way to get information about it varies according to service type.<br> */<br> </span><span style="color: #007700">function </span><span style="color: #0000BB">callMethodArray</span><span style="color: #007700">(</span><span style="color: #0000BB">$method</span><span style="color: #007700">, </span><span style="color: #0000BB">$method_params </span><span style="color: #007700">= array()) {<br><br> switch (</span><span style="color: #0000BB">$method</span><span style="color: #007700">) {<br> case </span><span style="color: #DD0000">'makeRequest'</span><span style="color: #007700">:<br><br> </span><span style="color: #FF8000">// Set the parameters.<br> </span><span style="color: #0000BB">$resource_path </span><span style="color: #007700">= </span><span style="color: #0000BB">$method_params</span><span style="color: #007700">[</span><span style="color: #0000BB">0</span><span style="color: #007700">];<br> </span><span style="color: #0000BB">$http_method </span><span style="color: #007700">= </span><span style="color: #0000BB">$method_params</span><span style="color: #007700">[</span><span style="color: #0000BB">1</span><span style="color: #007700">];<br> </span><span style="color: #0000BB">$data </span><span style="color: #007700">= isset(</span><span style="color: #0000BB">$method_params</span><span style="color: #007700">[</span><span style="color: #0000BB">2</span><span style="color: #007700">]) ? </span><span style="color: #0000BB">$method_params</span><span style="color: #007700">[</span><span style="color: #0000BB">2</span><span style="color: #007700">] : array();<br><br> </span><span style="color: #FF8000">// Make the request.<br> </span><span style="color: #0000BB">$results </span><span style="color: #007700">= </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">makeRequest</span><span style="color: #007700">(</span><span style="color: #0000BB">$resource_path</span><span style="color: #007700">, </span><span style="color: #0000BB">$http_method</span><span style="color: #007700">, </span><span style="color: #0000BB">$data</span><span style="color: #007700">);<br> break;<br> }<br><br> return </span><span style="color: #0000BB">$results</span><span style="color: #007700">;<br> }<br></span><span style="color: #0000BB">?></span></span>
Local methods
We're assuming REST here, but you can use any protocol.
We have a makeRequest() method, which actually performs the remote call, and handleRestError() which deals with any errors which are returned.
<span style="color: #000000"><span style="color: #0000BB"><?php<br> </span><span style="color: #FF8000">/**************************************************************************<br> * Local methods<br> **************************************************************************/<br><br> /**<br> * Make a REST request.<br> *<br> * Originally from clients_connection_drupal_services_rest_7->makeRequest().<br> * Examples:<br> * Retrieve an event:<br> * makeRequest('event?eventId=ID', 'GET');<br> * Update a node:<br> * makeRequest('node/NID', 'POST', $data);<br> *<br> * @param $resource_path<br> * The path of the resource. Eg, 'node', 'node/1', etc.<br> * @param $http_method<br> * The HTTP method. One of 'GET', 'POST', 'PUT', 'DELETE'. For an explanation<br> * of how the HTTP method affects the resource request, see the Services<br> * documentation at http://drupal.org/node/783254.<br> * @param $data = array()<br> * (Optional) An array of data to pass to the request.<br> * @param boolean $data_as_headers<br> * Data will be sent in the headers if this is set to TRUE.<br> *<br> * @return<br> * The data from the request response.<br> *<br> * @todo Update the first two test classes to not assume a SimpleXMLElement.<br> */<br> </span><span style="color: #007700">function </span><span style="color: #0000BB">makeRequest</span><span style="color: #007700">(</span><span style="color: #0000BB">$resource_path</span><span style="color: #007700">, </span><span style="color: #0000BB">$http_method</span><span style="color: #007700">, </span><span style="color: #0000BB">$data </span><span style="color: #007700">= array(), </span><span style="color: #0000BB">$data_as_headers </span><span style="color: #007700">= </span><span style="color: #0000BB">FALSE</span><span style="color: #007700">) {<br><br> </span><span style="color: #FF8000">// Tap into this function's cache if there is one.<br> </span><span style="color: #0000BB">$request_cache_map </span><span style="color: #007700">= &</span><span style="color: #0000BB">drupal_static</span><span style="color: #007700">(</span><span style="color: #0000BB">__FUNCTION__</span><span style="color: #007700">);<br><br> </span><span style="color: #FF8000">// Set the options.<br> </span><span style="color: #0000BB">$options </span><span style="color: #007700">= array(<br> </span><span style="color: #DD0000">'headers' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">getHeaders</span><span style="color: #007700">(), </span><span style="color: #FF8000">// Define if you need it.<br> </span><span style="color: #DD0000">'method' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$http_method</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'data' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$data</span><span style="color: #007700">,<br> );<br><br> </span><span style="color: #FF8000">// If cached, we have already issued this request during this page request so<br> // just use the cached value.<br> </span><span style="color: #0000BB">$request_path </span><span style="color: #007700">= </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">endpoint </span><span style="color: #007700">. </span><span style="color: #0000BB">$context_path </span><span style="color: #007700">. </span><span style="color: #DD0000">'/' </span><span style="color: #007700">. </span><span style="color: #0000BB">$resource_path</span><span style="color: #007700">;<br><br> </span><span style="color: #FF8000">// Either get the data from the cache or send a request for it.<br> </span><span style="color: #007700">if (isset(</span><span style="color: #0000BB">$request_cache_map</span><span style="color: #007700">[</span><span style="color: #0000BB">$request_path</span><span style="color: #007700">])) {<br> </span><span style="color: #FF8000">// Use the cached copy.<br> </span><span style="color: #0000BB">$response </span><span style="color: #007700">= </span><span style="color: #0000BB">$request_cache_map</span><span style="color: #007700">[</span><span style="color: #0000BB">$request_path</span><span style="color: #007700">];<br> } else {<br> </span><span style="color: #FF8000">// Not cached yet so fire off the request.<br> </span><span style="color: #0000BB">$response </span><span style="color: #007700">= </span><span style="color: #0000BB">drupal_http_request</span><span style="color: #007700">(</span><span style="color: #0000BB">$request_path</span><span style="color: #007700">, </span><span style="color: #0000BB">$options</span><span style="color: #007700">);<br><br> </span><span style="color: #FF8000">// And then cache to avoid duplicate calls within the page request.<br> </span><span style="color: #0000BB">$request_cache_map</span><span style="color: #007700">[</span><span style="color: #0000BB">$request_path</span><span style="color: #007700">] = </span><span style="color: #0000BB">$response</span><span style="color: #007700">;<br> }<br><br> </span><span style="color: #FF8000">// Handle any errors and then return the response.<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">handleRestError</span><span style="color: #007700">(</span><span style="color: #0000BB">$request_path</span><span style="color: #007700">, </span><span style="color: #0000BB">$response</span><span style="color: #007700">);<br> return </span><span style="color: #0000BB">$response</span><span style="color: #007700">;<br> }<br><br> </span><span style="color: #FF8000">/**<br> * Common helper for reacting to an error from a REST call.<br> *<br> * Originally from clients_connection_drupal_services_rest_7->handleRestError().<br> * Gets the error from the response, logs the error message,<br> * and throws an exception, which should be caught by the module making use<br> * of the Clients connection API.<br> *<br> * @param $response<br> * The REST response data, decoded.<br> *<br> * @throws Exception<br> */<br> </span><span style="color: #007700">function </span><span style="color: #0000BB">handleRestError</span><span style="color: #007700">(</span><span style="color: #0000BB">$request</span><span style="color: #007700">, </span><span style="color: #0000BB">$response</span><span style="color: #007700">) {<br><br> </span><span style="color: #FF8000">// Report and throw an error if we get anything unexpected.<br> </span><span style="color: #007700">if (!</span><span style="color: #0000BB">in_array</span><span style="color: #007700">(</span><span style="color: #0000BB">$response</span><span style="color: #007700">-></span><span style="color: #0000BB">code</span><span style="color: #007700">, array(</span><span style="color: #0000BB">200</span><span style="color: #007700">, </span><span style="color: #0000BB">201</span><span style="color: #007700">, </span><span style="color: #0000BB">202</span><span style="color: #007700">, </span><span style="color: #0000BB">204</span><span style="color: #007700">, </span><span style="color: #0000BB">404</span><span style="color: #007700">))) {<br><br> </span><span style="color: #FF8000">// Report error to the logs.<br> </span><span style="color: #0000BB">watchdog</span><span style="color: #007700">(</span><span style="color: #DD0000">'clients'</span><span style="color: #007700">, </span><span style="color: #DD0000">'Error with REST request (@req). Error was code @code with error "@error" and message "@message".'</span><span style="color: #007700">, array(<br> </span><span style="color: #DD0000">'@req' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$request</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'@code' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$response</span><span style="color: #007700">-></span><span style="color: #0000BB">code</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'@error' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$response</span><span style="color: #007700">-></span><span style="color: #0000BB">error</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'@message' </span><span style="color: #007700">=> isset(</span><span style="color: #0000BB">$response</span><span style="color: #007700">-></span><span style="color: #0000BB">status_message</span><span style="color: #007700">) ? </span><span style="color: #0000BB">$response</span><span style="color: #007700">-></span><span style="color: #0000BB">status_message </span><span style="color: #007700">: </span><span style="color: #DD0000">'(no message)'</span><span style="color: #007700">,<br> ), </span><span style="color: #0000BB">WATCHDOG_ERROR</span><span style="color: #007700">);<br><br> </span><span style="color: #FF8000">// Throw an error with which callers must deal.<br> </span><span style="color: #007700">throw new </span><span style="color: #0000BB">Exception</span><span style="color: #007700">(</span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">"Clients connection error, got message '@message'."</span><span style="color: #007700">, array(<br> </span><span style="color: #DD0000">'@message' </span><span style="color: #007700">=> isset(</span><span style="color: #0000BB">$response</span><span style="color: #007700">-></span><span style="color: #0000BB">status_message</span><span style="color: #007700">) ? </span><span style="color: #0000BB">$response</span><span style="color: #007700">-></span><span style="color: #0000BB">status_message </span><span style="color: #007700">: </span><span style="color: #0000BB">$response</span><span style="color: #007700">-></span><span style="color: #0000BB">error</span><span style="color: #007700">,<br> )), </span><span style="color: #0000BB">$response</span><span style="color: #007700">-></span><span style="color: #0000BB">code</span><span style="color: #007700">);<br> }<br> }<br></span><span style="color: #0000BB">?></span></span>
Implementing the remote query class
This is where the magic happens. We need a new class file, OurRestRemoteSelectQuery.class.php, that will assemble the select query and execute it based on any set conditions.
Class variables and constructor
First, let's define the class, its variables and its constructor. It's a subclass of the RemoteEntityQuery class. Most of the standard conditions would be added to the $conditions array, but conditions handled in a special way (say those dealing with metadata) can be set up as variables themselves. In the example below, the constructor sets the active user as it can affect which data is returned. You can, however, set whatever you need to initialize your subclass, or leave it out entirely.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/**<br> * @file<br> * Contains the OurRestRemoteSelectQuery class.<br> */<br><br>/**<br> * Select query for our remote data.<br> *<br> * @todo Make vars protected once no longer developing.<br> */<br></span><span style="color: #007700">class </span><span style="color: #0000BB">OurRestRemoteSelectQuery </span><span style="color: #007700">extends </span><span style="color: #0000BB">RemoteEntityQuery </span><span style="color: #007700">{<br><br> </span><span style="color: #FF8000">/**<br> * Determines whether the query is RetrieveMultiple or Retrieve.<br> *<br> * The query is Multiple by default, until an ID condition causes it to be<br> * single.<br> */<br> </span><span style="color: #007700">public </span><span style="color: #0000BB">$retrieve_multiple </span><span style="color: #007700">= </span><span style="color: #0000BB">TRUE</span><span style="color: #007700">;<br><br> </span><span style="color: #FF8000">/**<br> * An array of conditions on the query. These are grouped by the table they<br> * are on.<br> */<br> </span><span style="color: #007700">public </span><span style="color: #0000BB">$conditions </span><span style="color: #007700">= array();<br><br> </span><span style="color: #FF8000">/**<br> * The from date filter for event searches<br> */<br> </span><span style="color: #007700">public </span><span style="color: #0000BB">$from_date </span><span style="color: #007700">= </span><span style="color: #0000BB">NULL</span><span style="color: #007700">;<br><br> </span><span style="color: #FF8000">/**<br> * The to date filter for event searches<br> */<br> </span><span style="color: #007700">public </span><span style="color: #0000BB">$to_date </span><span style="color: #007700">= </span><span style="color: #0000BB">NULL</span><span style="color: #007700">;<br><br> </span><span style="color: #FF8000">/**<br> * The user id.<br> */<br> </span><span style="color: #007700">public </span><span style="color: #0000BB">$user_id </span><span style="color: #007700">= </span><span style="color: #0000BB">NULL</span><span style="color: #007700">;<br><br> </span><span style="color: #FF8000">/**<br> * Constructor to generically set up the user id condition if<br> * there is a current user.<br> *<br> * @param $connection<br> */<br> </span><span style="color: #007700">function </span><span style="color: #0000BB">__construct</span><span style="color: #007700">(</span><span style="color: #0000BB">$connection</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">parent</span><span style="color: #007700">::</span><span style="color: #0000BB">__construct</span><span style="color: #007700">(</span><span style="color: #0000BB">$connection</span><span style="color: #007700">);<br> if (</span><span style="color: #0000BB">user_is_logged_in</span><span style="color: #007700">()) {<br> global </span><span style="color: #0000BB">$user</span><span style="color: #007700">;<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">useridCondition</span><span style="color: #007700">(</span><span style="color: #0000BB">$user</span><span style="color: #007700">-></span><span style="color: #0000BB">name</span><span style="color: #007700">);<br> }<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
Setting conditions
We have three (3) methods which set conditions within the query. entityCondition() sets conditions affecting entities in general. (The only entity condition supported here is the entity ID.) propertyCondition() sets conditions related to properties specific to the type of data. For example, this could be a location filter for one or more events. Finally, we have useridCondition() which sets the query to act on behalf of a specific user. Here we simply record the current Drupal user.
<span style="color: #000000"><span style="color: #0000BB"><?php<br> </span><span style="color: #FF8000">/**<br> * Add a condition to the query.<br> *<br> * Originally based on the entityCondition() method in EntityFieldQuery, but<br> * largely from USDARemoteSelectQuery (Programming Drupal 7 Entities) and<br> * MSDynamicsSoapSelectQuery.<br> *<br> * @param $name<br> * The name of the entity property.<br> */<br> </span><span style="color: #007700">function </span><span style="color: #0000BB">entityCondition</span><span style="color: #007700">(</span><span style="color: #0000BB">$name</span><span style="color: #007700">, </span><span style="color: #0000BB">$value</span><span style="color: #007700">, </span><span style="color: #0000BB">$operator </span><span style="color: #007700">= </span><span style="color: #0000BB">NULL</span><span style="color: #007700">) {<br><br> </span><span style="color: #FF8000">// We only support the entity ID for now.<br> </span><span style="color: #007700">if (</span><span style="color: #0000BB">$name </span><span style="color: #007700">== </span><span style="color: #DD0000">'entity_id'</span><span style="color: #007700">) {<br><br> </span><span style="color: #FF8000">// Get the remote field name of the entity ID.<br> </span><span style="color: #0000BB">$field </span><span style="color: #007700">= </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">entity_info</span><span style="color: #007700">[</span><span style="color: #DD0000">'remote entity keys'</span><span style="color: #007700">][</span><span style="color: #DD0000">'remote id'</span><span style="color: #007700">];<br><br> </span><span style="color: #FF8000">// Set the remote ID field to the passed value.<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">conditions</span><span style="color: #007700">[</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">remote_base</span><span style="color: #007700">][] = array(<br> </span><span style="color: #DD0000">'field' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$field</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'value' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$value</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'operator' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$operator</span><span style="color: #007700">,<br> );<br><br> </span><span style="color: #FF8000">// Record that we'll only be retrieving a single item.<br> </span><span style="color: #007700">if (</span><span style="color: #0000BB">is_null</span><span style="color: #007700">(</span><span style="color: #0000BB">$operator</span><span style="color: #007700">) || (</span><span style="color: #0000BB">$operator </span><span style="color: #007700">== </span><span style="color: #DD0000">'='</span><span style="color: #007700">)) {<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">retrieve_multiple </span><span style="color: #007700">= </span><span style="color: #0000BB">FALSE</span><span style="color: #007700">;<br> }<br> }<br> else {<br><br> </span><span style="color: #FF8000">// Report an invalid entity condition.<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">throwException</span><span style="color: #007700">(<br> </span><span style="color: #DD0000">'OURRESTREMOTESELECTQUERY_INVALID_ENTITY_CONDITION'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'The query object can only accept the \'entity_id\' condition.'<br> </span><span style="color: #007700">);<br> }<br> }<br><br> </span><span style="color: #FF8000">/**<br> * Add a condition to the query, using local property keys.<br> *<br> * Based on MSDynamicsSoapSelectQuery::propertyCondition().<br> *<br> * @param $property_name<br> * A local property. Ie, a key in the $entity_info 'property map' array.<br> */<br> </span><span style="color: #007700">function </span><span style="color: #0000BB">propertyCondition</span><span style="color: #007700">(</span><span style="color: #0000BB">$property_name</span><span style="color: #007700">, </span><span style="color: #0000BB">$value</span><span style="color: #007700">, </span><span style="color: #0000BB">$operator </span><span style="color: #007700">= </span><span style="color: #0000BB">NULL</span><span style="color: #007700">) {<br><br> </span><span style="color: #FF8000">// Make sure the entity base has been set up.<br> </span><span style="color: #007700">if (!isset(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">entity_info</span><span style="color: #007700">)) {<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">throwException</span><span style="color: #007700">(<br> </span><span style="color: #DD0000">'OURRESTREMOTESELECTQUERY_ENTITY_BASE_NOT_SET'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'The query object was not set with an entity type.'<br> </span><span style="color: #007700">);<br> }<br><br> </span><span style="color: #FF8000">// Make sure that the provided property is valid.<br> </span><span style="color: #007700">if (!isset(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">entity_info</span><span style="color: #007700">[</span><span style="color: #DD0000">'property map'</span><span style="color: #007700">][</span><span style="color: #0000BB">$property_name</span><span style="color: #007700">])) {<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">throwException</span><span style="color: #007700">(<br> </span><span style="color: #DD0000">'OURRESTREMOTESELECTQUERY_INVALID_PROPERY'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'The query object cannot set a non-existent property.'<br> </span><span style="color: #007700">);<br> }<br><br> </span><span style="color: #FF8000">// Adding a field condition (probably) automatically makes this a multiple.<br> // TODO: figure this out for sure!<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">retrieve_multiple </span><span style="color: #007700">= </span><span style="color: #0000BB">TRUE</span><span style="color: #007700">;<br><br> </span><span style="color: #FF8000">// Use the property map to determine the remote field name.<br> </span><span style="color: #0000BB">$remote_field_name </span><span style="color: #007700">= </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">entity_info</span><span style="color: #007700">[</span><span style="color: #DD0000">'property map'</span><span style="color: #007700">][</span><span style="color: #0000BB">$property_name</span><span style="color: #007700">];<br><br> </span><span style="color: #FF8000">// Set the condition for use during execution.<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">conditions</span><span style="color: #007700">[</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">remote_base</span><span style="color: #007700">][] = array(<br> </span><span style="color: #DD0000">'field' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$remote_field_name</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'value' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$value</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'operator' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$operator</span><span style="color: #007700">,<br> );<br> }<br><br> </span><span style="color: #FF8000">/**<br> * Add a user id condition to the query.<br> *<br> * @param $user_id<br> * The user to search for appointments.<br> */<br> </span><span style="color: #007700">function </span><span style="color: #0000BB">useridCondition</span><span style="color: #007700">(</span><span style="color: #0000BB">$user_id</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">user_id </span><span style="color: #007700">= </span><span style="color: #0000BB">$user_id</span><span style="color: #007700">;<br> }<br></span><span style="color: #0000BB">?></span></span>
Executing the remote query
The execute() method marshals all of the conditions, passes the built request to the connection's makeRequest() that we saw earlier, calls parseEventResponse() (which we'll investigate below) and then returns the list of remote entities that can now be used by Drupal.
Feel free to ignore the authentication code if it's not required for your implementation. I left it in as an extended example of how this could be done.
<span style="color: #000000"><span style="color: #0000BB"><?php<br> </span><span style="color: #FF8000">/**<br> * Run the query and return a result.<br> *<br> * @return<br> * Remote entity objects as retrieved from the remote connection.<br> */<br> </span><span style="color: #007700">function </span><span style="color: #0000BB">execute</span><span style="color: #007700">() {<br><br> </span><span style="color: #FF8000">// If there are any validation errors, don't perform a search.<br> </span><span style="color: #007700">if (</span><span style="color: #0000BB">form_set_error</span><span style="color: #007700">()) {<br> return array();<br> }<br><br> </span><span style="color: #0000BB">$querystring </span><span style="color: #007700">= array();<br><br> </span><span style="color: #0000BB">$path </span><span style="color: #007700">= </span><span style="color: #0000BB">variable_get</span><span style="color: #007700">(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">base_entity_type </span><span style="color: #007700">. </span><span style="color: #DD0000">'_resource_name'</span><span style="color: #007700">, </span><span style="color: #DD0000">''</span><span style="color: #007700">);<br><br> </span><span style="color: #FF8000">// Iterate through all of the conditions and add them to the query.<br> </span><span style="color: #007700">if (isset(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">conditions</span><span style="color: #007700">[</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">remote_base</span><span style="color: #007700">])) {<br> foreach (</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">conditions</span><span style="color: #007700">[</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">remote_base</span><span style="color: #007700">] as </span><span style="color: #0000BB">$condition</span><span style="color: #007700">) {<br> switch (</span><span style="color: #0000BB">$condition</span><span style="color: #007700">[</span><span style="color: #DD0000">'field'</span><span style="color: #007700">]) {<br> case </span><span style="color: #DD0000">'event_id'</span><span style="color: #007700">:<br> </span><span style="color: #0000BB">$querystring</span><span style="color: #007700">[</span><span style="color: #DD0000">'eventId'</span><span style="color: #007700">] = </span><span style="color: #0000BB">$condition</span><span style="color: #007700">[</span><span style="color: #DD0000">'value'</span><span style="color: #007700">];<br> break;<br> case </span><span style="color: #DD0000">'login_id'</span><span style="color: #007700">:<br> </span><span style="color: #0000BB">$querystring</span><span style="color: #007700">[</span><span style="color: #DD0000">'userId'</span><span style="color: #007700">] = </span><span style="color: #0000BB">$condition</span><span style="color: #007700">[</span><span style="color: #DD0000">'value'</span><span style="color: #007700">];<br> break;<br> }<br> }<br> }<br><br> </span><span style="color: #FF8000">// "From date" parameter.<br> </span><span style="color: #007700">if (isset(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">from_date</span><span style="color: #007700">)) {<br> </span><span style="color: #0000BB">$querystring</span><span style="color: #007700">[</span><span style="color: #DD0000">'startDate'</span><span style="color: #007700">] = </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">from_date</span><span style="color: #007700">;<br> }<br><br> </span><span style="color: #FF8000">// "To date" parameter.<br> </span><span style="color: #007700">if (isset(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">to_date</span><span style="color: #007700">)) {<br> </span><span style="color: #0000BB">$querystring</span><span style="color: #007700">[</span><span style="color: #DD0000">'endDate'</span><span style="color: #007700">] = </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">to_date</span><span style="color: #007700">;<br> }<br><br> </span><span style="color: #FF8000">// Add user id based filter if present.<br> </span><span style="color: #007700">if (isset(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">user_id</span><span style="color: #007700">)) {<br> </span><span style="color: #0000BB">$querystring</span><span style="color: #007700">[</span><span style="color: #DD0000">'userId'</span><span style="color: #007700">] = </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">user_id</span><span style="color: #007700">;<br> }<br><br> </span><span style="color: #FF8000">// Assemble all of the query parameters.<br> </span><span style="color: #007700">if (</span><span style="color: #0000BB">count</span><span style="color: #007700">(</span><span style="color: #0000BB">$querystring</span><span style="color: #007700">)) {<br> </span><span style="color: #0000BB">$path </span><span style="color: #007700">.= </span><span style="color: #DD0000">'?' </span><span style="color: #007700">. </span><span style="color: #0000BB">drupal_http_build_query</span><span style="color: #007700">(</span><span style="color: #0000BB">$querystring</span><span style="color: #007700">);<br> }<br><br> </span><span style="color: #FF8000">// Make the request.<br> </span><span style="color: #007700">try {<br> </span><span style="color: #0000BB">$response </span><span style="color: #007700">= </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">connection</span><span style="color: #007700">-></span><span style="color: #0000BB">makeRequest</span><span style="color: #007700">(</span><span style="color: #0000BB">$path</span><span style="color: #007700">, </span><span style="color: #DD0000">'GET'</span><span style="color: #007700">);<br> } catch (</span><span style="color: #0000BB">Exception $e</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">$e</span><span style="color: #007700">-></span><span style="color: #0000BB">getCode</span><span style="color: #007700">() == </span><span style="color: #0000BB">OUR_REST_LOGIN_REQUIRED_NO_SESSION</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">drupal_set_message</span><span style="color: #007700">(</span><span style="color: #0000BB">$e</span><span style="color: #007700">-></span><span style="color: #0000BB">getMessage</span><span style="color: #007700">());<br> </span><span style="color: #0000BB">drupal_goto</span><span style="color: #007700">(</span><span style="color: #DD0000">'user/login'</span><span style="color: #007700">, array(</span><span style="color: #DD0000">'query' </span><span style="color: #007700">=> </span><span style="color: #0000BB">drupal_get_destination</span><span style="color: #007700">()));<br> }<br> elseif (</span><span style="color: #0000BB">$e</span><span style="color: #007700">-></span><span style="color: #0000BB">getCode</span><span style="color: #007700">() == </span><span style="color: #0000BB">OUR_REST_LOGIN_REQUIRED_TOKEN_EXPIRED</span><span style="color: #007700">) {<br><br> </span><span style="color: #FF8000">// Logout<br> </span><span style="color: #007700">global </span><span style="color: #0000BB">$user</span><span style="color: #007700">;<br> </span><span style="color: #0000BB">module_invoke_all</span><span style="color: #007700">(</span><span style="color: #DD0000">'user_logout'</span><span style="color: #007700">, </span><span style="color: #0000BB">$user</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">session_destroy</span><span style="color: #007700">();<br><br> </span><span style="color: #FF8000">// Redirect<br> </span><span style="color: #0000BB">drupal_set_message</span><span style="color: #007700">(</span><span style="color: #0000BB">$e</span><span style="color: #007700">-></span><span style="color: #0000BB">getMessage</span><span style="color: #007700">());<br> </span><span style="color: #0000BB">drupal_goto</span><span style="color: #007700">(</span><span style="color: #DD0000">'user/login'</span><span style="color: #007700">, array(</span><span style="color: #DD0000">'query' </span><span style="color: #007700">=> </span><span style="color: #0000BB">drupal_get_destination</span><span style="color: #007700">()));<br> }<br> }<br><br> switch(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">base_entity_type</span><span style="color: #007700">) {<br> case </span><span style="color: #DD0000">'siteshortname_entities_remote_event' </span><span style="color: #007700">:<br> </span><span style="color: #0000BB">$entities </span><span style="color: #007700">= </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">parseEventResponse</span><span style="color: #007700">(</span><span style="color: #0000BB">$response</span><span style="color: #007700">);<br> break;<br> }<br><br> </span><span style="color: #FF8000">// Return the list of results.<br> </span><span style="color: #007700">return </span><span style="color: #0000BB">$entities</span><span style="color: #007700">;<br> }<br></span><span style="color: #0000BB">?></span></span>
Unmarshalling the response data and returning it
Here, in the parseEventResponse method, we decode the response data (if there is any), and do any additional work required to get each entity's data into an object. They're all returned as a single list (array) of entity objects. If the response provides information on the format (XML, JSON, etc.), you can unmarshal the data differently based on what the server returned.
<span style="color: #000000"><span style="color: #0000BB"><?php<br> </span><span style="color: #FF8000">/**<br> * Helper for execute() which parses the JSON response for event entities.<br> *<br> * May also set the $total_record_count property on the query, if applicable.<br> *<br> * @param $response<br> * The JSON/XML/whatever response from the REST server.<br> *<br> * @return<br> * An list of entity objects, keyed numerically.<br> * An empty array is returned if the response contains no entities.<br> *<br> * @throws<br> * Exception if a fault is received when the REST call was made.<br> */<br> </span><span style="color: #007700">function </span><span style="color: #0000BB">parseEventResponse</span><span style="color: #007700">(</span><span style="color: #0000BB">$response</span><span style="color: #007700">) {<br><br> </span><span style="color: #FF8000">// Fetch the list of events.<br> </span><span style="color: #007700">if (</span><span style="color: #0000BB">$response</span><span style="color: #007700">-></span><span style="color: #0000BB">code </span><span style="color: #007700">== </span><span style="color: #0000BB">404</span><span style="color: #007700">) {<br> </span><span style="color: #FF8000">// No data was returned so let's provide an empty list.<br> </span><span style="color: #0000BB">$events </span><span style="color: #007700">= array();<br> }<br> else </span><span style="color: #FF8000">/* we have response data */ </span><span style="color: #007700">{<br><br> </span><span style="color: #FF8000">// Convert the JSON (assuming that's what we're getting) into a PHP array.<br> // Do any unmarshalling to convert the response data into a PHP array.<br> </span><span style="color: #0000BB">$events </span><span style="color: #007700">= </span><span style="color: #0000BB">json_decode</span><span style="color: #007700">(</span><span style="color: #0000BB">$response</span><span style="color: #007700">-></span><span style="color: #0000BB">data</span><span style="color: #007700">, </span><span style="color: #0000BB">TRUE</span><span style="color: #007700">);<br> }<br><br> </span><span style="color: #FF8000">// Initialize an empty list of entities for returning.<br> </span><span style="color: #0000BB">$entities </span><span style="color: #007700">= array();<br><br> </span><span style="color: #FF8000">// Iterate through each event.<br> </span><span style="color: #007700">foreach (</span><span style="color: #0000BB">$events </span><span style="color: #007700">as </span><span style="color: #0000BB">$event</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$entities</span><span style="color: #007700">[] = (object) array(<br><br> </span><span style="color: #FF8000">// Set event information.<br> </span><span style="color: #DD0000">'event_id' </span><span style="color: #007700">=> isset(</span><span style="color: #0000BB">$event</span><span style="color: #007700">[</span><span style="color: #DD0000">'id'</span><span style="color: #007700">]) ? </span><span style="color: #0000BB">$event</span><span style="color: #007700">[</span><span style="color: #DD0000">'id'</span><span style="color: #007700">] : </span><span style="color: #0000BB">NULL</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'event_name' </span><span style="color: #007700">=> isset(</span><span style="color: #0000BB">$event</span><span style="color: #007700">[</span><span style="color: #DD0000">'name'</span><span style="color: #007700">]) ? </span><span style="color: #0000BB">$event</span><span style="color: #007700">[</span><span style="color: #DD0000">'name'</span><span style="color: #007700">] : </span><span style="color: #0000BB">NULL</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'event_date' </span><span style="color: #007700">=> isset(</span><span style="color: #0000BB">$event</span><span style="color: #007700">[</span><span style="color: #DD0000">'date'</span><span style="color: #007700">]) ? </span><span style="color: #0000BB">$event</span><span style="color: #007700">[</span><span style="color: #DD0000">'date'</span><span style="color: #007700">] : </span><span style="color: #0000BB">NULL</span><span style="color: #007700">,<br> );<br> }<br><br> </span><span style="color: #FF8000">// Return the newly-created list of entities.<br> </span><span style="color: #007700">return </span><span style="color: #0000BB">$entities</span><span style="color: #007700">;<br> }<br></span><span style="color: #0000BB">?></span></span>
Error handling
We provide a helper method dealing with errors raised in other methods. It records the specific error message in the log and throws an exception based on the message and the code.
<span style="color: #000000"><span style="color: #0000BB"><?php<br> </span><span style="color: #FF8000">/**<br> * Throw an exception when there's a problem.<br> *<br> * @param string $code<br> * The error code.<br> *<br> * @param string $message<br> * A user-friendly message describing the problem.<br> *<br> * @throws Exception<br> */<br> </span><span style="color: #007700">function </span><span style="color: #0000BB">throwException</span><span style="color: #007700">(</span><span style="color: #0000BB">$code</span><span style="color: #007700">, </span><span style="color: #0000BB">$message</span><span style="color: #007700">) {<br><br> </span><span style="color: #FF8000">// Report error to the logs.<br> </span><span style="color: #0000BB">watchdog</span><span style="color: #007700">(</span><span style="color: #DD0000">'siteshortname_entities_remote'</span><span style="color: #007700">, </span><span style="color: #DD0000">'ERROR: OurRestRemoteSelectQuery: "@code", "@message".'</span><span style="color: #007700">, array(<br> </span><span style="color: #DD0000">'@code' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$code</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'@message' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$message</span><span style="color: #007700">,<br> ));<br><br> </span><span style="color: #FF8000">// Throw an error with which callers must deal.<br> </span><span style="color: #007700">throw new </span><span style="color: #0000BB">Exception</span><span style="color: #007700">(</span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">"OurRestRemoteSelectQuery error, got message '@message'."</span><span style="color: #007700">, array(<br> </span><span style="color: #DD0000">'@message' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$message</span><span style="color: #007700">,<br> )), </span><span style="color: #0000BB">$code</span><span style="color: #007700">);<br> }<br></span><span style="color: #0000BB">?></span></span>
Everything we've covered so far gets our remote data into Drupal. Below, we'll expose it to Views.
At the beginning of this article, I stated that we required the EntityFieldQuery Views Backend module. This allows us to replace the default Views query back-end, a local SQL database, with one that supports querying entities fetchable through the Remote Entity API. Make sure to add it, efq_views, to your custom remote entity module as a dependency.
For the curious, the changes I made to EFQ Views Backend to add this support can be found in the issue Add support for remote entities.
I added official documentation for all of this to the Remote Entity API README (via Explain how to integrate remote querying through Views). As it may not be obvious, when creating a new view of your remote entities, make sure that the base entity is the EntityFieldQuery version, not simply the entity itself. When selecting the entity type on which to base the view, you should see each entity twice: the standard one (via the default query back-end) and the EFQ version.
As stated in the documentation, you need to a add a buildFromEFQ() method to your RemoteEntityQuery subclass (which we went over in the previous section). We'll review why this is necessary and give an example next.
Converting from an EntityFieldQuery
As EFQ Views only builds EntityFieldQuery objects, we need to convert that type of query to an instance of our RemoteEntityQuery subclass. If EFQ Views stumbles upon a remote query instead of a local one, it will run the execute() method on one of these objects instead.
So we need to tell our subclass how to generate an instance of itself when provided with an EntityFieldQuery object. The method below handles the conversion, which EFQ Views calls when necessary.
<span style="color: #000000"><span style="color: #0000BB"><?php<br> </span><span style="color: #FF8000">/**<br> * Build the query from an EntityFieldQuery object.<br> *<br> * To have our query work with Views using the EntityFieldQuery Views module,<br> * which assumes EntityFieldQuery query objects, it's necessary to convert<br> * from the EFQ so that we may execute this one instead.<br> *<br> * @param $efq<br> * The built-up EntityFieldQuery object.<br> *<br> * @return<br> * The current object. Helpful for chaining methods.<br> */<br> </span><span style="color: #007700">function </span><span style="color: #0000BB">buildFromEFQ</span><span style="color: #007700">(</span><span style="color: #0000BB">$efq</span><span style="color: #007700">) {<br><br> </span><span style="color: #FF8000">// Copy all of the conditions.<br> </span><span style="color: #007700">foreach (</span><span style="color: #0000BB">$efq</span><span style="color: #007700">-></span><span style="color: #0000BB">propertyConditions </span><span style="color: #007700">as </span><span style="color: #0000BB">$condition</span><span style="color: #007700">) {<br><br> </span><span style="color: #FF8000">// Handle various conditions in different ways.<br> </span><span style="color: #007700">switch (</span><span style="color: #0000BB">$condition</span><span style="color: #007700">[</span><span style="color: #DD0000">'column'</span><span style="color: #007700">]) {<br><br> </span><span style="color: #FF8000">// Get the from date.<br> </span><span style="color: #007700">case </span><span style="color: #DD0000">'from_date' </span><span style="color: #007700">:<br> </span><span style="color: #0000BB">$from_date </span><span style="color: #007700">= </span><span style="color: #0000BB">$condition</span><span style="color: #007700">[</span><span style="color: #DD0000">'value'</span><span style="color: #007700">];<br> </span><span style="color: #FF8000">// Convert the date to the correct format for the REST service<br> </span><span style="color: #0000BB">$result </span><span style="color: #007700">= </span><span style="color: #0000BB">$from_date</span><span style="color: #007700">-></span><span style="color: #0000BB">format</span><span style="color: #007700">(</span><span style="color: #DD0000">'Y/m/d'</span><span style="color: #007700">);<br> </span><span style="color: #FF8000">// The above format() can return FALSE in some cases, so add a check<br> </span><span style="color: #007700">if ( </span><span style="color: #0000BB">$result </span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">from_date </span><span style="color: #007700">= </span><span style="color: #0000BB">$result</span><span style="color: #007700">;<br> }<br> break;<br><br> </span><span style="color: #FF8000">// Get the to date.<br> </span><span style="color: #007700">case </span><span style="color: #DD0000">'to_date'</span><span style="color: #007700">:<br> </span><span style="color: #0000BB">$to_date </span><span style="color: #007700">= </span><span style="color: #0000BB">$condition</span><span style="color: #007700">[</span><span style="color: #DD0000">'value'</span><span style="color: #007700">];<br> </span><span style="color: #FF8000">// Convert the date to the correct format for the REST service<br> </span><span style="color: #0000BB">$result </span><span style="color: #007700">= </span><span style="color: #0000BB">$to_date</span><span style="color: #007700">-></span><span style="color: #0000BB">format</span><span style="color: #007700">(</span><span style="color: #DD0000">'Y/m/d'</span><span style="color: #007700">);<br> </span><span style="color: #FF8000">// The above format() can return FALSE in some cases, so add a check<br> </span><span style="color: #007700">if ( </span><span style="color: #0000BB">$result </span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">to_date </span><span style="color: #007700">= </span><span style="color: #0000BB">$result</span><span style="color: #007700">;<br> }<br> break;<br><br> </span><span style="color: #FF8000">// Get the user ID.<br> </span><span style="color: #007700">case </span><span style="color: #DD0000">'user_id'</span><span style="color: #007700">:<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">user_id </span><span style="color: #007700">= </span><span style="color: #0000BB">$condition</span><span style="color: #007700">[</span><span style="color: #DD0000">'value'</span><span style="color: #007700">];<br> break;<br><br> default:<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">conditions</span><span style="color: #007700">[</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">remote_base</span><span style="color: #007700">][] = array(<br> </span><span style="color: #DD0000">'field' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$condition</span><span style="color: #007700">[</span><span style="color: #DD0000">'column'</span><span style="color: #007700">],<br> </span><span style="color: #DD0000">'value' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$condition</span><span style="color: #007700">[</span><span style="color: #DD0000">'value'</span><span style="color: #007700">],<br> </span><span style="color: #DD0000">'operator' </span><span style="color: #007700">=> isset(</span><span style="color: #0000BB">$condition</span><span style="color: #007700">[</span><span style="color: #DD0000">'operator'</span><span style="color: #007700">]) ? </span><span style="color: #0000BB">$condition</span><span style="color: #007700">[</span><span style="color: #DD0000">'operator'</span><span style="color: #007700">] : </span><span style="color: #0000BB">NULL</span><span style="color: #007700">,<br> );<br> break;<br> }<br> }<br><br> return </span><span style="color: #0000BB">$this</span><span style="color: #007700">;<br> }<br></span><span style="color: #0000BB">?></span></span>
That should be it! You'll now need to spend some time (if you haven't already) getting everything connected as above to fit your specific situation. If you can get these details sorted, you'll then be ready to go.
At the time of this writing, there appears to be only one alternative to the Remote Entity API (not including custom architectures). It's the Web Service Data suite. The main difference between the modules is that Web Service Data doesn't store a local cache of remote data; the data is always passed through directly.
If this more closely matches what you'd like to do, be aware that there is currently no EntityFieldQuery support:
Support for EntityFieldQuery (coming soon) will allow developers to make entity field queries with web service data.
This is very clearly stated on the main project page, but I wasn't able to find an issue in the queue tracking progress. So if you choose this method, you may have to add EFQ support yourself, or you may not be able to use Views with your remote entities.
- The MS Dynamics Client Connection reference implementation
- The Programming Drupal 7 Entities book
This article, Integrating remote data into Drupal 7 and exposing it to Views, appeared first on the Colan Schwartz Consulting Services blog.