Planet Drupal

Subscribe to flux Planet Drupal
Drupal.org - aggregated feeds in category Planet Drupal
Mis à jour : il y a 18 min 50 sec

Chapter Three: Content Strategy for Drupal 8

jeu, 19/02/2015 - 02:40

We've been publishing a lot of technical blogs about Drupal 8 to educate and inspire the community. But what about the non-technical folk? How will Drupal 8 shift the way designers, content strategists and project managers plan websites? While many of the changes will not affect our day to day work, there are a few new terms and ways of thinking that can streamline the strategy process, save developers time and save clients money.



Entities: The Word of the Day

Entities are our new friend. They are easygoing and flexible. The sooner we get comfortable with the word Entity and begin using it with our teams, the sooner we can all reap the rewards of the budding relationship.

Catégories: Elsewhere

Victor Kane: Desgrabación de mi presentación sobre DurableDrupal Lean UX+Dev+DevOps Drupalcon Latin America

jeu, 19/02/2015 - 00:46

Desgrabación de la Presentación en el DrupalCon Latin America 2015

Poner en pie una fábrica de DurableDrupal (Drupal Duradero) en base de un proceso Lean reutilizable

[Bajar el pdf de 18 páginas al pie de la página]

Ya que el sonido de la grabación de mi presentación quedó muy bajo en volumen, quiero presentar la desgrabación con la esperanza de que mi mensaje llegue a la mayor cantidad de personas posible.

El video de la presentación en sí se encuentra aquí: https://www.youtube.com/watch?v=bNbkBvtQ8Z0

Los diapositivas: http://awebfactory.com/drupalcon2015lean/#/

Para cada diapositiva, incluyo a continuación el link al slide y el texto correspondiente del video.

En algunos casos hay correcciones inevitables o he extendido el texto para su mejor comprensión. También he traducido diapositivas importantes y agregado algunos comentarios para incluir puntos de suma importancia que no fueron mencionados en la presentación por falta de tiempo, como el Kanban y sus características.

El artículo como consecuencia se extiende bastante, pero espero que resulte de utilidad para quienes se interesan por la cuestión del proceso Lean en los desarrollos con Drupal.

Mi plan es publicar en breve un libro que integre estos conceptos en la práctica con un ejemplo concreto de desarrollo de una aplicación web en Drupal 7 y 8.

read more

Catégories: Elsewhere

DrupalCon News: One week left to submit your DrupalCon sessions

mer, 18/02/2015 - 19:06

We are in the fourth week of session submissions for DrupalCon Los Angeles and only one week remains before the deadline. Now is your chance to shine! Send us your talk idea and you could find yourself presenting to the Drupal community's largest annual event this spring.

Catégories: Elsewhere

InternetDevels: Drupal vulnerability or developers' carelessness?

mer, 18/02/2015 - 16:16

In October, 2014 Sektion Eins company has discovered vulnerability which affects all branches of Drupal 7 versions. It allows performing any SQL-request to database even without having permissions in the system. The security risk was recognized as highly critical. The corresponding core update was released on October, 15. It upgraded the core to 7.32 version and eliminates this vulnerability. And now we’ll talk about some other kinds of vulnerabilities.

Read more
Catégories: Elsewhere

Dcycle: A quick intro to Docker for a Drupal project

mer, 18/02/2015 - 16:05

I recently added Docker support to Realistic Dummy Content, a project I maintain on Drupal.org. It is now possible to run ./scripts/dev.sh directly from the project directory (use the latest dev version if you try this), and have a development environment, sans MAMP.

I don't consider myself an expert in Docker, virtualization, DevOps and config management, but here, nonetheless, is my experience. If I'm wrong about something, please leave a comment!

Intro: Docker and DevOps

The DevOps movement, popularized in the last years, promises to include environment information along with application information in the same git repo for smoother development, testing, and production environments. For example, if your Drupal module requires version 5.4 of PHP, along with a given library, then that information should be somewhere in your Git repo. Building an environment for testing, development or production should then use that information and not be dependent on anything which is unversioned. Docker is a tool which is anchored in the DevOps movement.

DevOps: the Config management approach

The family of tools which has been around for awhile now includes Puppet, Chef, and Ansible. These tools are configuration management tools: they define environment information (PHP version should be 5.3, Apache mod_rewrite should be on, etc.) and make sure a given environment conforms to that information.

I have used Puppet, along with Vagrant, to deliver applications, including my Jenkins server hosted on GitHub.

Virtualization and containers

Using Puppet and Vagrant, you need to use Virtualization: create a Virtual Machine on your host machine. Docker uses containers so resources are shared. The article Getting Started with Docker (Servers for Hackers, 2014/03/20) contains some graphics which demonstrate how much more efficient containers are as opposed to virtualization.

Puppet and Vagrant are slow; Docker is fast

Puppet and Vagrant together work for packaging software and environment configuration, but it is excruciatingly slow: it can take several minutes to launch an environment. My reaction to this has been to cringe every time I have to do it.

Docker, on the other hand, uses caching agressively: if a server was already in a given state, Docker uses a cached version of it move faster. So, when building a container, Docker goes through a series of steps, and caches each step to make it lightning fast.

One example: launching a dev environment of Jenkins projects on Mac OS takes over five minutes, but launching dev environment of my Drupal project Realistic Dummy Content (which uses Docker), takes less than 15 seconds the first time it is run once the server code has been downloaded, and, because of caching, less than one (1) second subsequent times if no changes have been made.

Configuration management is idempotent, Docker is not

Before we move on, note that Docker is not incompatible with config management tools, but Docker does not require them. Here is why I think, in many cases, config management tools are not necessary.

The config management tools such as Puppet are idempotent: you define how an environment should, and the tools runs whatever steps are necessary to make it that way. This sounds like a good idea in theory, but it looks like this in practice. I have come to the conclusion that this is not the way I think, and it forces me to relearn how to think of my environments. I suspect that many developers have a hard time wrapping their heads around idempotence.

Docker is not idempotent; it defines a series of steps to get to a given state. If you like idempotence, one of the steps can be to run a puppet manifest; but if, like me, you think idempotence is overrated, then you don't need to use it. Here is what a Dockerfile looks like: I understood it at first glace, it doesn't require me to learn a new way of thinking.

The CoreOS project

The CoreOS project has seen the promise of Docker and containers. It is an OS which ships with Docker, Git, and a few other tools, but is designed so that everything you do happens within containers (using the included Docker, and eventually Rocket, a tool they are building). The result is that CoreOS is tiny: it takes 10 seconds to build a CoreOS instance on DigitalOcean, for example, but almost a minute to set up a CentOS instance.

Because Docker does not work on Mac OS without going through hoops, I decided to use Vagrant to set up a CoreOS VM on my Mac, which is speedy and works great.

Docker for deploying to production

We have seen that Docker can work for quickly setting up dev and testing environments. Can it be used to deploy to production? I don't see why not, especially if used with CoreOS. For an example see the blog post Building an Internal Cloud with Docker and CoreOS (Shopify, Oct. 15, 2014).

In conclusion, I am just beginning to play with Docker, and it just feels right to me. I remember working with Joomla in 2006, when I discovered Drupal, and it just felt right, and I have made a career of it since then. I am having the same feeling now discovering Docker and CoreOs.

I am looking forward to your comments explaining why I am wrong about not liking idempotence, how to make config management and virutalization faster, and how and why to integrate config management tools with Docker!

Tags: blogplanet
Catégories: Elsewhere

Acquia: Helping Remote Teams Work - The Manager

mer, 18/02/2015 - 15:51
Language Undefined

Part 2 of 2 – I ran into Elia Albarran, Four Kitchens' Operations Manager at BADCamp 2014. She mentioned she'd read my blog post 10 Tips for Success as a Remote Employee; we started exchanging tips and ideas until I basically yelled, "Stop! I need to get this on camera for the podcast!" She graciously agreed and brought along two Four Kitchens developers for the session, too: Taylor Smith and Matt Grill, whom I spoke with in part 1.

Catégories: Elsewhere

Drupalize.Me: Release Day: PhpStorm for Modern PHP Development

mer, 18/02/2015 - 15:15

Ready to take your PHP development to the next level? This week, we have another batch of video tutorials from the awesome folks at JetBrains on their IDE, PhpStorm. In these tutorials, you'll learn how to generate code using templates, set up your modern PHP app with namespaces, PSR-0 or PSR-4, integrate Composer, and debug like a pro.

Catégories: Elsewhere

Propeople Blog: Varnish Tips and Tricks

mer, 18/02/2015 - 06:02

In this article we would like to share some use cases and recipes for configuring Varnish.

Use custom data for building varnish cache hash

By default, Varnish uses URL as a parameter for caching. In the VCL file, it is also possible to add custom data (for example: location, or custom header value) to hashing by using the sub vcl_hash{} configuration part. But there is yet another solution to be used that Varnish comes equipped with out of the box. We can set a Vary header that is respected. So, if we want our cache to be based on header X-MyCustomHeader in Drupal, then we can set the header to

Vary: Cookie,Accept-Encoding,X-MyCustomHeader. This way, different values of our custom header will have different cache records in Varnish.

Limit access to the site by ip address

When we build an intranet website, we can limit access to it on the Varnish level. This can be done in following way:

First we define list of allowed IP addresses:

acl offices {

   "localhost";

   "127.0.0.1";

   "1.2.3.4";`

   "5.6.7.8";

}

Then we restrict access to non matching addresses:

sub vcl_recv {

   if ( req.http.host ~ "(intranet\.example\.com)$" && !(client.ip ~ offices) ) {

        error 403 "Access denied";

   }

}

SSL termination

As Varnish is not handling https traffic, we need to terminate SSL before it hits Varnish. For that we can use nginx. Here is a list of links to articles that dive deeper into this topic:

https://www.digitalocean.com/community/tutorials/how-to-configure-varnish-cache-4-0-with-ssl-termination-on-ubuntu-14-04

http://edoceo.com/howto/nginx-varnish-ssl

http://mikkel.hoegh.org/2012/07/24/varnish-as-reverse-proxy-with-nginx-as-web-server-and-ssl-terminator/

https://wiki.deimos.fr/Nginx_%2B_Varnish_:_Cache_even_in_HTTPS_by_offloading_SSL

ESI

On a recent Propeople project, we had the requirement to include a block with data from an external website without any caching. The tricky part was that the external site was providing XML with the data. The solution we implemented was to use ESI block pointing to the custom php file that was pulling that XML and parsing it on the fly.

Hiding js requests to external domains

If we need to do some CORS (http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) requests instead of our Javascript doing requests directly to external domain, we can do requests to our site, but with a specific URL. Then, on the Varnish level, we can redirect that request to external domain. In this case, Varnish will act like a proxy. This can be achieved with backend options.

backend google {

 .host = "209.85.147.106";

 .port = "80";

}

sub vcl_fetch {

 if (req.url ~ "^/masq") {

   set req.backend = google;

   set req.http.host = "www.google.com";

   set req.url = regsub(req.url, "^/masq", "");

   remove req.http.Cookie;

   return(deliver);

 }

}

This is an example from a brilliant book: https://www.varnish-software.com/static/book/

Multiple backends, load balancing

It is possible to define multiple backends for Varnish and switch between them. Most basic implementation is round robin or random. Here is an example:

backend web01 {

   .host = "example1";

   .port = "80";

   .connect_timeout = 120s;

   .first_byte_timeout = 300s;

   .between_bytes_timeout = 60s;

   .max_connections = 50;

   .probe = {

    .url = "/";

    .timeout = 10s;

    .interval = 20s;

    .window = 5;

    .threshold = 3;

   }

}

 

backend web02 {

   .host = "example2";

   .port = "80";

   .max_connections = 100;

   .connect_timeout = 120s;

   .first_byte_timeout = 300s;

   .between_bytes_timeout = 60s;

   .probe = {

       .url = "/";

       .timeout = 10s;          

       .interval = 20s;       

       .window = 5;

       .threshold = 3;

   }

}

 

director apache round-robin {

 { .backend = web01; }

 { .backend = web02; }

}

 

sub vcl_recv {

 set req.backend = apache;

}

 

It is also possible to set a specific backend for visitors coming from specific IP addresses. This can have a number of helpful uses, such as making sure that the editors team has  adedicated backend server.

if (client.ip ~ offices) {

 set req.backend = web03;

}


I hope you have enjoyed our tips regarding Varnish configuration. Please feel free to share your own thoughts and tips on Varnish in the comments below!

Tags: VarnishService category: TechnologyCheck this option to include this post in Planet Drupal aggregator: planetTopics: Tech & Development
Catégories: Elsewhere

Four Kitchens: Announcing SANDcamp Training for Advanced Responsive Web Design

mer, 18/02/2015 - 00:44

Patrick Coffey and I have been busy building a new version of the popular Advanced Responsive Web Design all-day training program and are excited to host it at San Diego’s SANDcamp next week. Registration is open and there are several spaces remaining and we would love for you to join us!

Responsive Web Design is on everyone’s mind at the moment, and for good reason. The old techniques we have used to create pixel perfect sites for desktop audiences have already become a thing of the past as mobile usage accelerates.

Training Drupal Camp Drupal
Catégories: Elsewhere

Isovera Ideas & Insights: When Do You Make the Move to Drupal 8?

mar, 17/02/2015 - 21:54
Lately, whenever we start a project at Isovera, we are typically asked, "would you build this with Drupal 8"? It's a good question. There are many good reasons to get a leg up with the (currently) beta release, but there are also good reasons to keep your head down and stick with Drupal 7. The official release of Drupal 8 is rapidly approaching. What might this mean to you?
Catégories: Elsewhere

Phase2: The Pros And Cons of Headless Drupal

mar, 17/02/2015 - 19:38

Drupal is an excellent content management system. Nodes and fields allow site administrators the ability to create complex datasets and models without having to write a singe mysql query. Unfortunately Drupal’s theming system isn’t as flexible. Complete control over the dom is nearly impossible without a lot of work. Headless Drupal bridges the gap and gives us the best of both worlds.

What is headless Drupal?

Headless Drupal is an approach that decouples Drupal’s backend from the frontend theme. Drupal is used as a content store and admin interface. Using the services module in Drupal 7 or Core in Drupal 8, a rest web service can be created. Visitors to the site don’t view a traditional Drupal theme instead they are presented with pages created with Ember.js, Angular.js, or even a custom framework. Using the web service the chosen framework can be used to transfer data from Drupal to the front end and vice versa.

Pros

So what makes Headless Drupal so great? For one thing it allows frontend developers full control over the page markup. Page speed also increases since display logic is on the client side instead of the server. Sites can also become much more interactive with page transitions and animations. But most importantly the Drupal admin can also be used to power not only web apps but also mobile applications including Android and iOS.

Cons

Unfortunately Headless Drupal is not without its drawbacks. For one layout control for editors becomes much more difficult. Something that could be done easily via context or panels now requires a lot of custom fronted logic. Also if proper caching isn’t utilized and the requests aren’t batched properly lots of roundtrips can occur, causing things to slow down drastically.

Want to learn more about Headless Drupal?  Check out the Headless Drupal Initiative on Drupal.org.  And on a related topic, check out “Drupal 8 And The Changing CMS Landscape.

Catégories: Elsewhere

Cameron Eagans: Use the Force!

mar, 17/02/2015 - 18:00

Python developers have Jedi. Go developers have gocode. Hack developers have the built-in autocomplete functionality in hhvm. PHP developers have….nothing.

Catégories: Elsewhere

Cheeky Monkey Media: How to add typekit fonts to your drupal website

mar, 17/02/2015 - 18:00

So you just got the latest design from your graphics department. Now it’s up to you, the drupal developer, to take that design and turn it into reality. The problem is that they used some fancy pants new font and you need to make sure it works on every browser and mobile device.

There are a few solid options to choose from, including Google fonts and the popular @font-your-face drupal module. However, one of the services that I have been using lately is Adobe Typekit. They offer thousands of fonts and make it easy to scale. Typekit offers a basic free account as well as paid...Read More

Catégories: Elsewhere

Appnovation Technologies: Export Data From Views to CSV File

mar, 17/02/2015 - 17:55

It is sometimes useful to be able to save our view results into a document to allow non-technical people to manipulate the data.

Catégories: Elsewhere

Tag1 Consulting: How to Maintain Contrib Modules for Drupal and Backdrop at the Same Time - Part 2

mar, 17/02/2015 - 17:00

This is the second in a series of blog posts about the relationship between Drupal and Backdrop CMS, a recently-released fork of Drupal. The goal of the series is to explain how a module (or theme) developer can take a Drupal project they currently maintain and support it for Backdrop as well, while keeping duplicate work to a minimum.

read more

Catégories: Elsewhere

Clemens Tolboom: Delete and edit comments on closed node

mar, 17/02/2015 - 16:29

Having a forum you needs quick deletions of improper comments.

In Drupal 7 and Drupal 8 you have to visit admin/content/comments to do so. But then you loose the thread.

You could review and use this patch or add this to your custom module. The first needs review and testing. The later needs a Drupal coder.

Catégories: Elsewhere

Drupal Commerce: Using OpenID Connect for Single Sign-On with Drupal

mar, 17/02/2015 - 16:03

At Commerce Guys we provide a varied range of services, including our cloud PaaS Platform.sh, this Drupal Commerce community website, support, and the Commerce Marketplace.

Our users may need to log in to any of these services, and sometimes several at the same time. So we needed to have a shared authentication system, a way of synchronizing user accounts, and single sign-on (SSO) functionality.

After a lot of research on the existing methods, such as CAS, we found that there was no generic open-source solution which would cover all of our current needs and would also allow us to grow and scale in the future when adding new features or applications.

We decided to implement the OAuth 2.0 and OpenID Connect protocols, which were designed to be flexible, yet simple and standardized - exactly what we wanted.

Catégories: Elsewhere

Drupal @ Penn State: Autopost to Facebook

mar, 17/02/2015 - 15:15

I ran into an issue with the Drupal for Facebook module, both for D6 and D7, where I wanted articles to auomatically be posted to Facebook when they are submitted.  There appeared to be no way to do this via the module and I had played around with Rules to see if that would work, but no luck.

Catégories: Elsewhere

Colan Schwartz: Integrating remote data into Drupal 7 and exposing it to Views

lun, 16/02/2015 - 20:45
Topics: 

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.

  1. Introduction
  2. What's Changed
  3. Architecture
    1. Remote entity definition
    2. Access to remote properties
    3. Remote property definition
    4. Entity instances as Web pages
    5. Web services integration
    6. Temporary local storage
    7. Implementing the remote connection class
    8. Implementing the remote query class
  4. Views support
    1. Basic set-up
    2. Converting from an EntityFieldQuery
  5. Alternatives
  6. References
Introduction

This exposition is effectively a follow-up to some excellent articles from years past:

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.

  1. The entity API in Drupal Core
  2. The Entity API contributed module
What's Changed

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.

Architecture

So we really need to take advantage of the three (3) entity APIs to load and display individual remote records.

  1. The entity API in Drupal Core
  2. The Entity API contributed module
  3. 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.

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:

  1. Entity APIs: Code for defining remote entities through any of the above entity APIs. (Part I)
  2. 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)
  3. 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.

Remote entity definition

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.

<?php
/****************************************************************************
 ** Entity APIs
 ****************************************************************************/

/**
 * Implements hook_entity_info().
 *
 * @todo Add 'bundles' for different types of remote content.
 * @todo Add 'entity keys' => 'needs remote save' if remote saving required.
 * @todo Remove 'static cache' and 'field cache' settings after development.
 */
function siteshortname_entities_remote_entity_info() {
  $entities['siteshortname_entities_remote_event'] = array(

    // Core properties.
    'label' => t('Event'),
    'controller class' => 'RemoteEntityAPIDefaultController',
    'base table' => 'siteshortname_entities_remote_events',
    'uri callback' => 'entity_class_uri',
    'label callback' => 'remote_entity_entity_label',
    'fieldable' => FALSE,
    'entity keys' => array(
      'id' => 'eid',
      'label' => 'event_name',
    ),
    'view modes' => array(
      'full' => array(
        'label' => t('Full content'),
        'custom settings' => FALSE,
      ),
    ),
    'static cache' => FALSE,
    'field cache' => FALSE,

    // Entity API properties.
    'entity class' => 'SiteshortnameEvent',
    'module' => 'siteshortname_entities_remote',
    'metadata controller class' => 'RemoteEntityAPIDefaultMetadataController',
    'views controller class' => 'EntityDefaultViewsController',

    // Remote Entity API properties.
    'remote base table' => 'siteshortname_entities_remote_events',
    'remote entity keys' => array(
      'remote id' => 'event_id',
      'label' => 'event_name',
    ),
    'expiry' => array(
      // Number of seconds before a locally cached instance must be refreshed
      // from the remote source.
      'expiry time' => 600,
      // A boolean indicating whether or not to delete expired local entities
      // on cron.
      'purge' => FALSE,
    ),
  );

  // Get the property map data.
  $remote_properties = siteshortname_entities_remote_get_remote_properties();

  // Assign each map to its corresponding entity.
  foreach ($entities as $key => $einfo) {
    $entities[$key]['property map'] =
      drupal_map_assoc(array_keys($remote_properties[$key]));
  }

  // Return all of the entity information.
  return $entities;
}
?>
Notes
  1. 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.
  2. In this article, we won't be covering remote saving (only remote loading), but it is possible through the remote API.
  3. Make sure to adjust the cache settings properly once development is complete.
  4. Detailed documentation on the APIs is available for the Core entity API, the Entity API, and the the Remote Entity API.
Access to remote properties

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.

<?php
/**
 * Implements hook_entity_property_info_alter().
 *
 * This is needed to use wrappers to access the remote entity
 * data in the entity_data property of remote entities.
 *
 * @see: Page 107 of the Programming Drupal 7 Entities book.  The code below is
 *   a variation on it.
 * @todo: Remove whenever this gets added to the remote_entity module.
 */
function siteshortname_entities_remote_entity_property_info_alter(&$info) {

  // Set the entity types and get their properties.
  $entity_types = array(
    'siteshortname_entities_remote_event',
  );

  $remote_properties = siteshortname_entities_remote_get_remote_properties();

  // Assign the property data to each entity.
  foreach ($entity_types as $entity_type) {
    $properties = &$info[$entity_type]['properties'];
    $entity_data = &$properties['entity_data'];
    $pp = &$remote_properties[$entity_type];
    $entity_data['type'] = 'remote_entity_' . $entity_type;

    // Set the default getter callback for each property.
    foreach ($pp as $key => $pinfo) {
      $pp[$key]['getter callback'] = 'entity_property_verbatim_get';
    }

    // Assign the updated property info to the entity info.
    $entity_data['property info'] = $pp;
  }
}
?>
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.

<?php
/**
 * Get remote property information for remote entities.
 *
 * @return
 *   An array of property information keyed by entity type.
 */
function siteshortname_entities_remote_get_remote_properties() {

  // Initialize a list of entity properties.
  $properties = array();

  // Define properties for the entity type.
  $properties['siteshortname_entities_remote_event'] = array(

    // Event information.
    'event_id' => array(
      'label' => 'Remote Event ID',
      'type' => 'integer',
      'description' => 'The remote attribute "id".',
      'views' => array(
        'filter' => 'siteshortname_entities_remote_views_handler_filter_event_id',
      ),
    ),
    'event_date' => array(
      'label' => 'Date',
      'type' => 'date',
      'description' => 'The remote attribute "date".',
      'views' => array(
        'filter' => 'siteshortname_entities_remote_views_handler_filter_event_date',
      ),
    ),
    'event_details' => array(
      'label' => 'Details',
      'type' => 'text',
      'description' => 'The remote attribute "details".',
    ),
  );

  // Return all of the defined property info.
  return $properties;
}
?>
Notes
  1. 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.
  2. 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.
Entity instances as Web pages

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.

<?php
/****************************************************************************
 ** Drupal Core
 ****************************************************************************/

/**
 * Implements hook_menu().
 */
function siteshortname_entities_remote_menu() {
  $items = array();

  $items['event/%siteshortname_entities_remote_event'] = array(
    'title' => 'Remote Event',
    'page callback' => 'siteshortname_entities_remote_event_view',
    'page arguments' => array(1),
    'access arguments' => array('access content'),
  );

  return $items;
}

/**
 * Menu autoloader wildcard for path 'event/REMOTE_ID'.
 *
 * @see hook_menu() documentation.
 * @param $remote_id
 *   The remote ID of the record to load.
 * @return
 *   The loaded object, or FALSE on failure.
 */
function siteshortname_entities_remote_event_load($remote_id) {
  return remote_entity_load_by_remote_id('siteshortname_entities_remote_event', $remote_id);
}

/**
 * Page callback for path 'event/%remote_id'.
 *
 * @param $event
 *   The auto-loaded object.
 * @return
 *   The themed output for the event page.
 */
function siteshortname_entities_remote_event_view($event) {
  $fullname = $event->name;
  drupal_set_title($fullname);
  $event_output = theme('siteshortname_entities_remote_event', array(
    'event' => $event,
  ));
  return $event_output;
}

/**
 * Implements hook_theme().
 */
function siteshortname_entities_remote_theme() {
  return array(
    'siteshortname_entities_remote_event' => array(
      'variables' => array('event' => NULL),
      'file' => 'includes/siteshortname_entities_remote.theme.inc',
    ),
  );
}
?>

There's one more thing to do here. In our hook_entity_info() implementation, we stated the following:

<?php
    'entity class' => 'SiteshortnameEvent',
?>

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:

<?php
class SiteshortnameEvent extends Entity {
  /**
   * Override defaultUri().
   */
  protected function defaultUri() {
    return array('path' => 'event/' . $this->remote_id);
  }
}
?>
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. <?php
/****************************************************************************
 ** Web Service Clients
 ****************************************************************************/

/**
 * Implements hook_clients_connection_type_info().
 */
function siteshortname_entities_remote_clients_connection_type_info() {
  return array(
    'our_rest' => array(
      'label'  => t('REST Data Services'),
      'description' => t('Connects to our data service using REST endpoints.'),
      'tests' => array(
        'event_retrieve_raw' => 'SiteshortnameEntitiesRemoteConnectionTestEventRetrieveRaw',
      ),
      'interfaces' => array(
        'ClientsRemoteEntityInterface',
      ),
    ),
  );
}

/**
 * Implements hook_clients_default_connections().
 */
function siteshortname_entities_remote_clients_default_connections() {

  $connections['my_rest_connection'] = new clients_connection_our_rest(array(
    'endpoint' => 'https://data.example.com',
    'configuration' => array(
      'username' => '',
      'password' => '',
    ),
    'label' => 'Our REST Service',
    'type' => 'our_rest',
  ), 'clients_connection');

  return $connections;
}

/**
 * Implements hook_clients_default_resources().
 */
function siteshortname_entities_remote_clients_default_resources() {
  $resources['siteshortname_entities_remote_event'] = new clients_resource_remote_entity(array(
    'component' => 'siteshortname_entities_remote_event',
    'connection' => 'my_rest_connection',
    'label' => 'Resource for remote events',
    'type' => 'remote_entity',
  ), 'clients_resource');

  return $resources;
}
?>

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.

Temporary local storage

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.

<?php
/**
 * Implementation of hook_schema().
 */
function siteshortname_entities_remote_schema() {
  $schema = array(
    'siteshortname_entities_remote_events' => remote_entity_schema_table('our remote event entity type'),
  );

  return $schema;
}
?>

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.

<?php
/**
 * Entity controller extending RemoteEntityAPIDefaultController
 *
 * For most of our cases the default controller is fine, but we can use
 * this one for entities we don't want stored locally.  Override the save
 * behaviour and do not keep a local cached copy.
 */
class SiteshortnameEntitiesRemoteNoLocalAPIController extends RemoteEntityAPIDefaultController {

  /**
   * Don't actually save anything.
   */
  public function save($entity, DatabaseTransaction $transaction = NULL) {
    $entity->eid = uniqid();
  }
}
?>
Implementing the remote connection class

Create a file for the connection class.

<?php
/**
 * @file
 * Contains the clients_connection_our_rest class.
 */

/**
 * Set up a client connection to our REST services.
 *
 *  @todo Make private functions private once development is done.
 */
class clients_connection_our_rest extends clients_connection_base
  implements ClientsConnectionAdminUIInterface, ClientsRemoteEntityInterface {

}
?>

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.
<?php
  /**************************************************************************
   * ClientsRemoteEntityInterface implementations.
   **************************************************************************/

  /**
   * Load a remote entity.
   *
   * @param $entity_type
   *   The entity type to load.
   * @param $id
   *   The (remote) ID of the entity.
   *
   * @return
   *  An entity object.
   */
  function remote_entity_load($entity_type, $id) {
    $query = $this->getRemoteEntityQuery('select');
    $query->base($entity_type);
    $query->entityCondition('entity_id', $id);
    $result = $query->execute();

    // There's only one. Same pattern as entity_load_single().
    return reset($result);
  }

  /**
   * Provide a map of remote property types to Drupal types.
   *
   * Roughly analogous to _entity_metadata_convert_schema_type().
   *
   * @return
   *   An array whose keys are remote property types as used as types for fields
   *   in hook_remote_entity_query_table_info(), and whose values are types
   *   recognized by the Entity Metadata API (as listed in the documentation for
   *   hook_entity_property_info()).
   *   If a remote property type is not listed here, it will be mapped to 'text'
   *   by default.
   */
  function entity_property_type_map() {
    return array(
      'EntityCollection' => 'list<string>',
    );
  }

  /**
   * Get a new RemoteEntityQuery object appropriate for the connection.
   *
   * @param $query_type
   *  (optional) The type of the query. Defaults to 'select'.
   *
   * @return
   *  A remote query object of the type appropriate to the query type.
   */
  function getRemoteEntityQuery($query_type = 'select') {
    switch ($query_type) {
      case 'select':
        return new OurRestRemoteSelectQuery($this);
      case 'insert':
        return new OurRestRemoteInsertQuery($this);
      case 'update':
        return new OurRestRemoteUpdateQuery($this);
    }
  }
?>
Parent overrides

The only method we need to worry about here is callMethodArray(). Basically, it sets up the remote call.

<?php
  /**************************************************************************
   * clients_connection_base overrides
   **************************************************************************/

  /**
   * Call a remote method with an array of parameters.
   *
   * This is intended for internal use from callMethod() and
   * clients_connection_call().
   * If you need to call a method on given connection object, use callMethod
   * which has a nicer form.
   *
   * Subclasses do not necessarily have to override this method if their
   * connection type does not make sense with this.
   *
   * @param $method
   *  The name of the remote method to call.
   * @param $method_params
   *  An array of parameters to passed to the remote method.
   *
   * @return
   *  Whatever is returned from the remote site.
   *
   * @throws Exception on error from the remote site.
   *  It's up to subclasses to implement this, as the test for an error and
   *  the way to get information about it varies according to service type.
   */
  function callMethodArray($method, $method_params = array()) {

    switch ($method) {
      case 'makeRequest':

        // Set the parameters.
        $resource_path = $method_params[0];
        $http_method = $method_params[1];
        $data = isset($method_params[2]) ? $method_params[2] : array();

        // Make the request.
        $results = $this->makeRequest($resource_path, $http_method, $data);
        break;
    }

    return $results;
  }
?>
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.

<?php
  /**************************************************************************
   * Local methods
   **************************************************************************/

  /**
   * Make a REST request.
   *
   * Originally from clients_connection_drupal_services_rest_7->makeRequest().
   * Examples:
   * Retrieve an event:
   *  makeRequest('event?eventId=ID', 'GET');
   * Update a node:
   *  makeRequest('node/NID', 'POST', $data);
   *
   * @param $resource_path
   *  The path of the resource. Eg, 'node', 'node/1', etc.
   * @param $http_method
   *  The HTTP method. One of 'GET', 'POST', 'PUT', 'DELETE'. For an explanation
   *  of how the HTTP method affects the resource request, see the Services
   *  documentation at http://drupal.org/node/783254.
   * @param $data = array()
   *  (Optional) An array of data to pass to the request.
   * @param boolean $data_as_headers
   *   Data will be sent in the headers if this is set to TRUE.
   *
   * @return
   *  The data from the request response.
   *
   *  @todo Update the first two test classes to not assume a SimpleXMLElement.
   */
  function makeRequest($resource_path, $http_method, $data = array(), $data_as_headers = FALSE) {

    // Tap into this function's cache if there is one.
    $request_cache_map = &drupal_static(__FUNCTION__);

    // Set the options.
    $options = array(
      'headers' => $this->getHeaders(),  // Define if you need it.
      'method'  => $http_method,
      'data'    => $data,
    );

    // If cached, we have already issued this request during this page request so
    // just use the cached value.
    $request_path = $this->endpoint . $context_path . '/' . $resource_path;

    // Either get the data from the cache or send a request for it.
    if (isset($request_cache_map[$request_path])) {
      // Use the cached copy.
      $response = $request_cache_map[$request_path];
    } else {
      // Not cached yet so fire off the request.
      $response = drupal_http_request($request_path, $options);

      // And then cache to avoid duplicate calls within the page request.
      $request_cache_map[$request_path] = $response;
    }

    // Handle any errors and then return the response.
    $this->handleRestError($request_path, $response);
    return $response;
  }

  /**
   * Common helper for reacting to an error from a REST call.
   *
   * Originally from clients_connection_drupal_services_rest_7->handleRestError().
   * Gets the error from the response, logs the error message,
   * and throws an exception, which should be caught by the module making use
   * of the Clients connection API.
   *
   * @param $response
   *  The REST response data, decoded.
   *
   * @throws Exception
   */
  function handleRestError($request, $response) {

    // Report and throw an error if we get anything unexpected.
    if (!in_array($response->code, array(200, 201, 202, 204, 404))) {

      // Report error to the logs.
      watchdog('clients', 'Error with REST request (@req). Error was code @code with error "@error" and message "@message".', array(
        '@req'      => $request,
        '@code'     => $response->code,
        '@error'    => $response->error,
        '@message'  => isset($response->status_message) ? $response->status_message : '(no message)',
      ), WATCHDOG_ERROR);

      // Throw an error with which callers must deal.
      throw new Exception(t("Clients connection error, got message '@message'.", array(
        '@message' => isset($response->status_message) ? $response->status_message : $response->error,
      )), $response->code);
    }
  }
?>
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.

<?php
/**
 * @file
 * Contains the OurRestRemoteSelectQuery class.
 */

/**
 * Select query for our remote data.
 *
 * @todo Make vars protected once no longer developing.
 */
class OurRestRemoteSelectQuery extends RemoteEntityQuery {

  /**
   * Determines whether the query is RetrieveMultiple or Retrieve.
   *
   * The query is Multiple by default, until an ID condition causes it to be
   * single.
   */
  public $retrieve_multiple = TRUE;

  /**
   * An array of conditions on the query. These are grouped by the table they
   * are on.
   */
  public $conditions = array();

  /**
   * The from date filter for event searches
   */
  public $from_date = NULL;

  /**
   * The to date filter for event searches
   */
  public $to_date = NULL;

  /**
   * The user id.
   */
  public $user_id = NULL;

  /**
   * Constructor to generically set up the user id condition if
   * there is a current user.
   *
   * @param $connection
   */
  function __construct($connection) {
    parent::__construct($connection);
    if (user_is_logged_in()) {
      global $user;
      $this->useridCondition($user->name);
    }
  }
}
?>
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.

<?php
  /**
   * Add a condition to the query.
   *
   * Originally based on the entityCondition() method in EntityFieldQuery, but
   * largely from USDARemoteSelectQuery (Programming Drupal 7 Entities) and
   * MSDynamicsSoapSelectQuery.
   *
   * @param $name
   *  The name of the entity property.
   */
  function entityCondition($name, $value, $operator = NULL) {

    // We only support the entity ID for now.
    if ($name == 'entity_id') {

      // Get the remote field name of the entity ID.
      $field = $this->entity_info['remote entity keys']['remote id'];

      // Set the remote ID field to the passed value.
      $this->conditions[$this->remote_base][] = array(
        'field' => $field,
        'value' => $value,
        'operator' => $operator,
      );

      // Record that we'll only be retrieving a single item.
      if (is_null($operator) || ($operator == '=')) {
        $this->retrieve_multiple = FALSE;
      }
    }
    else {

      // Report an invalid entity condition.
      $this->throwException(
        'OURRESTREMOTESELECTQUERY_INVALID_ENTITY_CONDITION',
        'The query object can only accept the \'entity_id\' condition.'
      );
    }
  }

  /**
   * Add a condition to the query, using local property keys.
   *
   * Based on MSDynamicsSoapSelectQuery::propertyCondition().
   *
   * @param $property_name
   *  A local property. Ie, a key in the $entity_info 'property map' array.
   */
  function propertyCondition($property_name, $value, $operator = NULL) {

    // Make sure the entity base has been set up.
    if (!isset($this->entity_info)) {
      $this->throwException(
        'OURRESTREMOTESELECTQUERY_ENTITY_BASE_NOT_SET',
        'The query object was not set with an entity type.'
      );
    }

    // Make sure that the provided property is valid.
    if (!isset($this->entity_info['property map'][$property_name])) {
      $this->throwException(
        'OURRESTREMOTESELECTQUERY_INVALID_PROPERY',
        'The query object cannot set a non-existent property.'
      );
    }

    // Adding a field condition (probably) automatically makes this a multiple.
    // TODO: figure this out for sure!
    $this->retrieve_multiple = TRUE;

    // Use the property map to determine the remote field name.
    $remote_field_name = $this->entity_info['property map'][$property_name];

    // Set the condition for use during execution.
    $this->conditions[$this->remote_base][] = array(
      'field' => $remote_field_name,
      'value' => $value,
      'operator' => $operator,
    );
  }

  /**
   * Add a user id condition to the query.
   *
   * @param $user_id
   *   The user to search for appointments.
   */
  function useridCondition($user_id) {
    $this->user_id = $user_id;
  }
?>
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.

<?php
  /**
   * Run the query and return a result.
   *
   * @return
   *  Remote entity objects as retrieved from the remote connection.
   */
  function execute() {

    // If there are any validation errors, don't perform a search.
    if (form_set_error()) {
      return array();
    }

    $querystring = array();

    $path = variable_get($this->base_entity_type . '_resource_name', '');

    // Iterate through all of the conditions and add them to the query.
    if (isset($this->conditions[$this->remote_base])) {
      foreach ($this->conditions[$this->remote_base] as $condition) {
        switch ($condition['field']) {
          case 'event_id':
            $querystring['eventId'] = $condition['value'];
            break;
          case 'login_id':
            $querystring['userId'] = $condition['value'];
            break;
        }
      }
    }

    // "From date" parameter.
    if (isset($this->from_date)) {
      $querystring['startDate'] = $this->from_date;
    }

    // "To date" parameter.
    if (isset($this->to_date)) {
      $querystring['endDate'] = $this->to_date;
    }

    // Add user id based filter if present.
    if (isset($this->user_id)) {
      $querystring['userId'] = $this->user_id;
    }

    // Assemble all of the query parameters.
    if (count($querystring)) {
      $path .= '?' . drupal_http_build_query($querystring);
    }

    // Make the request.
    try {
      $response = $this->connection->makeRequest($path, 'GET');
    } catch (Exception $e) {
      if ($e->getCode() == OUR_REST_LOGIN_REQUIRED_NO_SESSION) {
        drupal_set_message($e->getMessage());
        drupal_goto('user/login', array('query' => drupal_get_destination()));
      }
      elseif ($e->getCode() == OUR_REST_LOGIN_REQUIRED_TOKEN_EXPIRED) {

        // Logout
        global $user;
        module_invoke_all('user_logout', $user);
        session_destroy();

        // Redirect
        drupal_set_message($e->getMessage());
        drupal_goto('user/login', array('query' => drupal_get_destination()));
      }
    }

    switch($this->base_entity_type) {
      case 'siteshortname_entities_remote_event' :
        $entities = $this->parseEventResponse($response);
        break;
    }

    // Return the list of results.
    return $entities;
  }
?>
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.

<?php
  /**
   * Helper for execute() which parses the JSON response for event entities.
   *
   * May also set the $total_record_count property on the query, if applicable.
   *
   * @param $response
   *  The JSON/XML/whatever response from the REST server.
   *
   * @return
   *  An list of entity objects, keyed numerically.
   *  An empty array is returned if the response contains no entities.
   *
   * @throws
   *  Exception if a fault is received when the REST call was made.
   */
  function parseEventResponse($response) {

    // Fetch the list of events.
    if ($response->code == 404) {
      // No data was returned so let's provide an empty list.
      $events = array();
    }
    else /* we have response data */ {

      // Convert the JSON (assuming that's what we're getting) into a PHP array.
      // Do any unmarshalling to convert the response data into a PHP array.
      $events = json_decode($response->data, TRUE);
    }

    // Initialize an empty list of entities for returning.
    $entities = array();

    // Iterate through each event.
    foreach ($events as $event) {
      $entities[] = (object) array(

        // Set event information.
        'event_id' => isset($event['id']) ? $event['id'] : NULL,
        'event_name' => isset($event['name']) ? $event['name'] : NULL,
        'event_date' => isset($event['date']) ? $event['date'] : NULL,
      );
    }

    // Return the newly-created list of entities.
    return $entities;
  }
?>
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.

<?php
  /**
   * Throw an exception when there's a problem.
   *
   * @param string $code
   *   The error code.
   *
   * @param string $message
   *   A user-friendly message describing the problem.
   *
   * @throws Exception
   */
  function throwException($code, $message) {

    // Report error to the logs.
    watchdog('siteshortname_entities_remote', 'ERROR: OurRestRemoteSelectQuery: "@code", "@message".', array(
      '@code' => $code,
      '@message' => $message,
    ));

    // Throw an error with which callers must deal.
   throw new Exception(t("OurRestRemoteSelectQuery error, got message '@message'.", array(
      '@message' => $message,
    )), $code);
  }
?>

Everything we've covered so far gets our remote data into Drupal. Below, we'll expose it to Views.

Views support Basic set-up

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.

<?php
  /**
   * Build the query from an EntityFieldQuery object.
   *
   * To have our query work with Views using the EntityFieldQuery Views module,
   * which assumes EntityFieldQuery query objects, it's necessary to convert
   * from the EFQ so that we may execute this one instead.
   *
   * @param $efq
   *   The built-up EntityFieldQuery object.
   *
   * @return
   *   The current object.  Helpful for chaining methods.
   */
  function buildFromEFQ($efq) {

    // Copy all of the conditions.
    foreach ($efq->propertyConditions as $condition) {

      // Handle various conditions in different ways.
      switch ($condition['column']) {

        // Get the from date.
        case 'from_date' :
          $from_date = $condition['value'];
          // Convert the date to the correct format for the REST service
          $result = $from_date->format('Y/m/d');
          // The above format() can return FALSE in some cases, so add a check
          if ( $result ) {
            $this->from_date = $result;
          }
          break;

        // Get the to date.
        case 'to_date':
          $to_date = $condition['value'];
          // Convert the date to the correct format for the REST service
          $result = $to_date->format('Y/m/d');
          // The above format() can return FALSE in some cases, so add a check
          if ( $result ) {
            $this->to_date = $result;
          }
          break;

        // Get the user ID.
        case 'user_id':
          $this->user_id = $condition['value'];
          break;

        default:
          $this->conditions[$this->remote_base][] = array(
            'field' => $condition['column'],
            'value' => $condition['value'],
            'operator' => isset($condition['operator']) ? $condition['operator'] : NULL,
          );
          break;
      }
    }

    return $this;
  }
?>

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.

Alternatives

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.

References

This article, Integrating remote data into Drupal 7 and exposing it to Views, appeared first on the Colan Schwartz Consulting Services blog.

Catégories: Elsewhere

DrupalCon News: Making website magic with the DrupalCon site building track

lun, 16/02/2015 - 19:45

In honor of this year’s DrupalCon in Tinseltown, we invite you to indulge in a bit of Drupal movie magic.

Imagine the scene…

NARRATOR
You are about to enter another dimension, a dimension not only of configuration and security but of UI. A journey into a wondrous land of complex sites without custom development. Next stop, the Drupal Zone!

THE SCENE
Intl. Acme, Inc. Meeting Room - it is day

FADE IN

Catégories: Elsewhere

Pages