Introduction
This is the second in a series of posts on using the Google Cloud Datastore on PHP App Engine.
The code for the examples in this post is here: https://github.com/amygdala/appengine_php_datastore_example.
The previous post walked through the process of setting up your PHP App Engine app to connect with the Datastore, using the Google API client libraries.
It demonstrated how to create a test Datastore entity, and showed how to set up a “Datastore service” instance, using a DatastoreService
class, to make it easier to send requests.
However, using the API client libraries, there is a fair bit of overhead in composing requests to the Datastore and processing the responses, and mapping entities to instances of classes in your app. So, in this post, we’ll define some classes to make it easier to create, update, and fetch Datastore entities.
We’ll define a `Model` base class, then show an example of subclassing it to map to a specific Datastore Kind.
A Model Base Class
The Model
base class is an abstract class.
Using the Google API client library, Model
provides ‘wrapper’ methods for actions like defining, saving, deleting, and querying Datastore entities, and looking them up by key. This makes it more straightforward to write application-level code that manipulates Datastore objects.
(The Model
class is not complete, w.r.t coverage of the Datastore API exposed by the API client libraries, but it will serve as a good starting point for your own code.)
In addition to its base methods, the Model
class also specifies several methods that must be implemented by any valid Model
subclass: getKindName()
, getKindProperties()
, and extractQueryResults()
. In a subclass, these methods encode information about how to map objects to/from entities of a specific Kind.
The Model::fetch_by_key()
method is a representative base method. It supports entity lookup given a Key. It constructs a Google_Service_Datastore_LookupRequest object, sets its options appropriately, does the lookup via the DatastoreService
object, and extracts the results.
private static function fetch_by_key($key, $txn = null) { $lookup_req = new Google_Service_Datastore_LookupRequest(); $lookup_req->setKeys([$key]); if ($txn) { $ros = new Google_Service_Datastore_ReadOptions(); $ros->setTransaction($txn); $lookup_req->setReadOptions($ros); } $response = DatastoreService::getInstance()->lookup($lookup_req); $found = $response->getFound(); $extracted = static::extractQueryResults($found); return $extracted; }
As another example, below is the method to perform a Datastore ‘put’. It creates a Google_Service_Datastore_Entity (which will be set with a Kind-specific property map), then creates a Google_Service_Datastore_Mutation set to that entity. Next, the method creates a Google_Service_Datastore_CommitRequest— set to be transactional if so specified— and uses the DatastoreService
instance to commit the write.
public function put($txn = null) { $entity = $this->create_entity(); $mutation = new Google_Service_Datastore_Mutation(); if ($this->key_id || $this->key_name) { $mutation->setUpsert([$entity]); } else { $mutation->setInsertAutoId([$entity]); } $req = new Google_Service_Datastore_CommitRequest(); if ($txn) { $req->setTransaction($txn); } else { $req->setMode('NON_TRANSACTIONAL'); } $req->setMutation($mutation); DatastoreService::getInstance()->commit($req); $this->onItemWrite(); }
Our Model
base class facilitates many common Datastore operations.
To use it in an app, we’ll extend it by defining subclass(es) that map to specific Datastore Kinds.
Model subclass example
Let’s look at a simple example of a Model
subclass.
Suppose we’re building an RSS feed reader app, where we want to store feed objects in the Datastore, and retrieve them. (In fact, watch this space for such a sample app.)
To make this process easier, we’ll build a FeedModel
class that subclasses Model
, and has a subscriber_url
field where we’ll store the feed URL.
The FeedModel
class defines instance variables, and getters and setters, in a standard fashion. Its constructor sets $this->key_name
. This means that the feed entities will have key names, not auto-generated IDs.
require_once 'Model.php'; class FeedModel extends Model { const FEED_MODEL_KIND = 'FeedModelTest'; const SUBSCRIBER_URL_NAME = 'subscriber_url'; private $subscriber_url; public function __construct($url) { parent::__construct(); $this->key_name = sha1($url); $this->subscriber_url = $url; } ...
The class must also implement methods that support mapping a FeedModel
instance to a Datastore entity object (of Kind ‘FeedModelTest’), and conversely, mapping Datastore query results to FeedModel
instances.
Specifically, it must implement the getKindProperties()
method— required by the Model
parent class— to generate an entity property map from a Feed object’s fields. In this simple example, we just need to map one field, subscriber_url
.
protected function getKindProperties() { $property_map = []; $property_map[self::SUBSCRIBER_URL_NAME] = parent::createStringProperty($this->subscriber_url, true); return $property_map; }
The createStringProperty()
method (implemented in the parent Model
) is used to generate the subscriber_url
property.
The FeedModel
class must also implement the extractQueryResults()
method— again required by its parent class— which constructs FeedModel
instances from the results of a query to the Datastore. As above, its implementation is specific to FeedModel’s properties, and so should handle extracting the subscriber_url
property value from the query results.
protected static function extractQueryResults($results) { $query_results = []; foreach($results as $result) { $id = @$result['entity']['key']['path'][0]['id']; $key_name = @$result['entity']['key']['path'][0]['name']; $props = $result['entity']['properties']; $url = $props[self::SUBSCRIBER_URL_NAME]->getStringValue(); $feed_model = new FeedModel($url); $feed_model->setKeyId($id); $feed_model->setKeyName($key_name); // Cache this feed obj $feed_model->onItemWrite(); $query_results[] = $feed_model; } return $query_results; }
FeedModel’s get()
method shows an example of constructing a query that filters on URL string, querying the Datastore, and extracting the results:
public static function get($feed_url) { $mc = new Memcache(); $key = self::getCacheKey($feed_url); $response = $mc->get($key); if ($response) { return [$response]; } $query = parent::createQuery(self::FEED_MODEL_KIND); $feed_url_filter = parent::createStringFilter(self::SUBSCRIBER_URL_NAME, $feed_url); $filter = parent::createCompositeFilter([$feed_url_filter]); $query->setFilter($filter); $results = parent::executeQuery($query); $extracted = self::extractQueryResults($results); return $extracted; }
By extending the Model
class in this manner, it becomes straightforward to create and update, as well as fetch and query for, feed entities.
The script model_example.php
, from the example project, shows an example of creating and then fetching a feed object from the Datastore. If you run this handler script, e.g.:
http://<your-app-id>.appspot.com/model_example
then in your app’s Admin Console, in the Datastore Viewer, you’ll see an entity of type FeedModelTest
.
Summary
This post picks up where the previous post left off, and shows how to access the Cloud Datastore from a PHP App Engine app.
This post centered on using a Model
base class to make Datastore access easier.
Then, we showed an example of a Model
subclass (a FeedModel
class).
In the next post in this series, we’ll show a complete sample app.
I'm trying to replace embedding the private key information to sign requests via Google_Auth_OAuth2 with the methods already built in to Google App Engine via AppIdentityService
Using calls to getApplicationId, getServiceAccountNmae, and getAccessToken($scope) I can generate the oath access token and retrieve most of the same data. However, while it works for a few tests, after half a dozen test calls I get a daily usage error:
[domain] => usageLimits
[reason] => dailyLimitExceededUnreg
[message] => Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup.
[extendedHelp] => https://code.google.com/apis/console
I'm not quite sure exactly what signup is required: it's PHP Google App Engine application with billing enabled, and the Datastore API is enabled. Thus it should work I would think?
I presume this response is from the API you're calling – is this a response from calling a method on CloudDatastore?
When will the next post appear? 😉
Pretty soon!