Photo of Joakim Hedlund

Joakim Hedlund

Web enthusiast

Treating Elasticsearch documents like Eloquent models

Last updated

Sometimes you want to use Eloquent without a regular database table storing the data, so what do you do?

Recently, I wanted to load some Elasticsearch documents but I still wanted to take advantage of the comfortable features that Eloquent offers, such as toJson() and mutators. I eventually found the awesomeness that is jenssegers/model which is essentially the same thing as Eloquent but without the database-related stuff.

All we need to do is to use a different base class when extending our model, and then declare our own find-method. Shouldn't be too hard!

<?php
use \Jenssegers\Model\Model as EloquentLike;

/**
 * Eloquent-like syntax for loading orders from Elasticsearch.
 * Jens Segers base model gives us the essentials.
 */
class Order extends EloquentLike {

  /**
   * A static variable means that the cache will be shared
   * between multiple instances of the Order class.
   */
  protected static $orders = array();

  /**
   * Just like Eloquent, we can tell it to always include
   * our "email" attribute when exporting to JSON or array
   */
  protected $appends = array(
    'email',
  );

  /**
   * Load a specific document from Elasticsearch
   */
  public static function find($ordernumber)
  {
    if(empty($ordernumber)) return FALSE;
    // If we already fetched it once, lets just return the cached value
    if(isset(static::$orders[$ordernumber])) return static::$orders[$ordernumber];

    // Prep an Elasticsearch query
    $searchParams['index'] = 'store';
    $searchParams['type'] = 'order';
    $searchParams['size'] = 1;
    $searchParams['body'] = array(
      'query' => array(
        'simple_query_string' => array(
          'query' => '"'.$ordernumber.'"',
          'fields' => array('ordernumber'),
        ),
      )
    );
    $result = Es::search($searchParams);

    // Either the foreach contains one result..
    foreach($result['hits']['hits'] as $hit)
    {
      // Prepare a filled Order model
      $instance = new static;
      static::unguard();
      $instance->fill($hit['_source']);
      static::reguard();
      // Cache it before returning it
      static::$orders[$ordernumber] = $instance;
      return $instance;
    }

    // ..or the foreach is never run, meaning we didnt find anything.
    static::$orders[$ordernumber] = FALSE;
    return FALSE;
  }

  /**
   * Example mutator to deal with legacy. You dont need this. :)
   */
  public function getIdAttribute($value)
  {
    if($value) return $value;
    return $this->entity_id; // Legacy property name
  }

  /**
   * Figure out the customer's email and put it in a
   * property that we guarantee is always correct.
   *
   * This method is called every time we use toJson() or toArray()
   * because the "email" attribute is defined in our $appends property
   */
  public function getEmailAttribute()
  {
    if(isset($this->entity_id))
    {
      // Legacy documents use this
      $email = $this->customer_email;
    }
    else
    {
      $email = $this->customer['email'];
    }
    return $email;
  }
}

Of course, you could replace the Elasticsearch functionality with whatever you want, like a remote API call or reading data from a CSV.

Now, thanks to the brilliance of science, we can do fancy stuff like:

$order  = Order::find('12345');
if(!$order) die('No order with that ordernumber found :(');

echo 'Found order with id '.$order->id;

Hopefully this lets you save some time fetching these documents.