Commit e5cc736b authored by JacobSingh's avatar JacobSingh

#725194: Added Entity API and tests as basis for future work.

parent dd36ffcd
<?php
// $Id$
/**
* @file
* API include file from the entity API.
* See http://drupal.org/project/entity.
*/
/**
* Defines status codes used for exportable entities.
*/
if (!defined('ENTITY_IN_DB')) {
/**
* A bit flag used to let us know if an entity is in the database.
*/
define('ENTITY_IN_DB', 0x01);
/**
* A bit flag used to let us know if an entity is a 'default' in code.
*/
define('ENTITY_IN_CODE', 0x02);
/**
* A bit flag used to mark entities as fixed, thus not changeable for any
* user.
*/
define('ENTITY_FIXED', 0x04);
}
if (!interface_exists('EntityAPIControllerInterface', FALSE)) {
/**
* Interface for EntityControllers compatible with the entity API.
*/
interface EntityAPIControllerInterface extends DrupalEntityControllerInterface {
/**
* Delete permanently saved entities.
*
* @param $ids
* An array of entity IDs.
*/
public function delete($ids);
}
/**
* Provides an interface for entites using the entity API.
*/
interface EntityAPIInterface {
/**
* Permanently save this entity.
*
* @return
* Failure to write a record will return FALSE. Otherwise SAVED_NEW or
* SAVED_UPDATED is returned depending on the operation performed.
*/
public function save();
/**
* Permanently delete this entity.
*/
public function delete();
/**
* Returns the internal identifier of this instance.
*
* @return
* The primary ID or NULL, if this entity hasn't been saved yet.
*/
public function internalIdentifier();
/**
* Returns the entity type of this instance.
*
* @return
* The name of the entity type of this instance.
*/
public function entityType();
/**
* Returns the entity info for this instance as returned by entity_get_info().
*
* @return
* An array of entity info for this entity type.
*
* @see
* entity_get_info()
*/
public function entityInfo();
/**
* Invokes a hook on behalf the entity. For hooks that have a respective
* field API attacher like insert/update/.. the attacher is called too.
*/
public function invoke($hook);
}
}
if (!class_exists('EntityAPIController', FALSE)) {
/**
* A controller that supports loading instances of the 'entity class' and
* provides mass delete functionality.
*/
class EntityAPIController extends DrupalDefaultEntityController implements EntityAPIControllerInterface {
protected $defaultEntities;
protected $nameKey, $statusKey;
/**
* Overridden.
* @see DrupalDefaultEntityController#__construct()
*
* Allows specifying a name key serving as uniform identifier for this entity
* type while still internally we are using numeric identifieres.
*/
public function __construct($entityType) {
parent::__construct($entityType);
// Use the name key as primary identifier.
$this->nameKey = isset($this->entityInfo['object keys']['name']) ? $this->entityInfo['object keys']['name'] : $this->idKey;
if (isset($this->entityInfo['exportable'])) {
$this->statusKey = isset($this->entityInfo['export']['status key']) ? $this->entityInfo['export']['status key'] : 'status';
}
}
/**
* Builds and executes the query for loading.
*
* @return The results in a Traversable object.
*/
public function query() {
// Build the query.
$this->buildQuery();
$result = $this->query->execute();
if (!empty($this->entityInfo['entity class'])) {
$result->setFetchMode(PDO::FETCH_CLASS, $this->entityInfo['entity class'], array(array(), $this->entityType));
}
return $result;
}
protected function buildQuery() {
// If we have a separate name key use it to filter for ids in $this->ids.
if ($this->ids && $this->nameKey != $this->idKey) {
$ids = $this->ids;
$this->ids = array();
parent::buildQuery();
$this->query->condition("base.{$this->nameKey}", $ids, 'IN');
}
else {
parent::buildQuery();
}
}
/**
* Overridden.
* @see DrupalDefaultEntityController#load($ids, $conditions)
*
* In contrast to the parent implementation we factor out query execution, so
* fetching can be further customized easily. Also we add any in code defined
* objects.
*/
public function load($ids = array(), $conditions = array()) {
$this->ids = $ids;
$this->conditions = $conditions;
$entities = array();
// Revisions are not statically cached, and require a different query to
// other conditions, so separate the revision id into its own variable.
if ($this->revisionKey && isset($this->conditions[$this->revisionKey])) {
$this->revisionId = $this->conditions[$this->revisionKey];
unset($this->conditions[$this->revisionKey]);
}
else {
$this->revisionId = FALSE;
}
// Create a new variable which is either a prepared version of the $ids
// array for later comparison with the entity cache, or FALSE if no $ids
// were passed. The $ids array is reduced as items are loaded from cache,
// and we need to know if it's empty for this reason to avoid querying the
// database when all requested entities are loaded from cache.
$passed_ids = !empty($this->ids) ? array_flip($this->ids) : FALSE;
// Try to load entities from the static cache, if the entity type supports
// static caching.
if ($this->cache) {
$entities = $this->cacheGet($this->ids, $this->conditions);
// If any entities were loaded, remove them from the ids still to load.
if ($passed_ids) {
$this->ids = array_keys(array_diff_key($passed_ids, $entities));
}
}
if (!empty($this->entityInfo['exportable'])) {
// Add default entities defined in code.
$entities += $this->getDefaults($this->ids, $this->conditions);
}
// Load any remaining entities from the database. This is the case if $ids
// is set to FALSE (so we load all entities), if there are any ids left to
// load, if loading a revision, or if $conditions was passed without $ids.
if ($this->ids === FALSE || $this->ids || $this->revisionId || ($this->conditions && !$passed_ids)) {
$schema = drupal_get_schema($this->entityInfo['base table']);
$queried_entities = array();
foreach ($this->query() as $record) {
// Care for serialized columns.
foreach ($schema['fields'] as $field => $info) {
if (!empty($info['serialize']) && isset($record->$field)) {
$record->$field = unserialize($record->$field);
// Support automatic merging of 'data' fields into the entity.
if (!empty($info['merge']) && is_array($record->$field)) {
foreach ($record->$field as $key => $value) {
$record->$key = $value;
}
unset($record->$field);
}
}
}
if (isset($this->statusKey)) {
// Care for setting the status key properly.
$record->{$this->statusKey} |= ENTITY_IN_DB;
$id = $record->{$this->nameKey};
if (isset($entities[$id]) && $entities[$id]->{$this->statusKey} & ENTITY_IN_CODE) {
$record->{$this->statusKey} |= ENTITY_IN_CODE;
unset($entities[$id]);
}
}
$queried_entities[$record->{$this->nameKey}] = $record;
}
}
// Pass all entities loaded from the database through $this->attachLoad(),
// which attaches fields (if supported by the entity type) and calls the
// entity type specific load callback, for example hook_node_load().
if (!empty($queried_entities)) {
$this->attachLoad($queried_entities);
$entities += $queried_entities;
}
if ($this->cache) {
// Add entities to the cache if we are not loading a revision.
if (!empty($queried_entities) && !$this->revisionId) {
$this->cacheSet($queried_entities);
}
}
// Ensure that the returned array is ordered the same as the original
// $ids array if this was passed in and remove any invalid ids.
if ($passed_ids) {
// Remove any invalid ids from the array.
$passed_ids = array_intersect_key($passed_ids, $entities);
foreach ($entities as $entity) {
$passed_ids[$entity->{$this->nameKey}] = $entity;
}
$entities = $passed_ids;
}
return $entities;
}
/**
* Implement EntityAPIControllerInterface.
*/
public function delete($ids) {
$entities = $this->load($ids);
db_delete($this->entityInfo['base table'])
->condition($this->idKey, $ids, 'IN')
->execute();
foreach ($entities as $entity) {
$entity->invoke('delete');
}
$this->resetCache();
}
/**
* For exportables call the hook to get all default entities.
*/
protected function getDefaults($ids, $conditions = array()) {
if (!isset($this->defaultEntities)) {
$this->defaultEntities = array();
if (!empty($this->entityInfo['exportable'])) {
$this->entityInfo += array('export' => array());
$this->entityInfo['export'] += array('default hook' => 'default_' . $this->entityType);
if ($hook = $this->entityInfo['export']['default hook']) {
$this->defaultEntities = module_invoke_all($hook);
drupal_alter($hook, $this->defaultEntities);
foreach ($this->defaultEntities as $entity) {
$entity->{$this->statusKey} |= ENTITY_IN_CODE;
}
}
}
}
$entities = $ids ? array_intersect_key($this->defaultEntities, array_flip($ids)) : $this->defaultEntities;
return $this->applyConditions($entities, $conditions);
}
protected function applyConditions($entities, $conditions = array()) {
if ($conditions) {
foreach ($entities as $key => $entity) {
$entity_values = (array) $entity;
if (array_diff_assoc($conditions, $entity_values)) {
unset($entities[$key]);
}
}
}
return $entities;
}
/**
* Overridden.
* @see includes/DrupalDefaultEntityController#cacheGet($ids, $conditions)
*
* If there is nameKey given, we index our entities by this key. This
* overrides cacheGet() to respect that when applying $conditions.
*/
protected function cacheGet($ids, $conditions = array()) {
return $this->applyConditions(parent::cacheGet($ids), $conditions);
}
public function resetCache() {
$this->entityCache = array();
unset($this->defaultEntities);
}
}
}
if (!class_exists('EntityDB', FALSE)) {
/**
* A common class for db entities.
*/
class EntityDB extends FacesExtendable implements EntityAPIInterface {
protected $entityType;
protected $entityInfo;
protected $idKey, $nameKey, $bundleKey;
public function __construct(array $values = array(), $entityType = NULL) {
if (empty($entityType)) {
throw new Exception('Cannot created an instance of EntityDB without a specified entity type.');
}
$this->entityType = $entityType;
$this->entityInfo = entity_get_info($entityType);
$this->idKey = $this->entityInfo['object keys']['id'];
$this->nameKey = isset($this->entityInfo['object keys']['name']) ? $this->entityInfo['object keys']['name'] : $this->idKey;
// If this is the bundle of another entity, set the bundle key.
if (isset($this->entityInfo['bundle of'])) {
$info = entity_get_info($this->entityInfo['bundle of']);
$this->bundleKey = $info['bundle keys']['bundle'];
}
// Set initial values.
foreach ($values as $key => $value) {
$this->$key = $value;
}
}
public function internalIdentifier() {
return isset($this->{$this->idKey}) ? $this->{$this->idKey} : NULL;
}
public function identifier() {
return isset($this->{$this->nameKey}) ? $this->{$this->nameKey} : NULL;
}
public function entityInfo() {
return $this->entityInfo;
}
public function entityType() {
return $this->entityType;
}
public function save() {
$this->invoke('presave');
if (isset($this->{$this->idKey})) {
$return = drupal_write_record($this->entityInfo['base table'], $this, $this->idKey);
$this->invoke('update');
}
else {
$return = drupal_write_record($this->entityInfo['base table'], $this);
$this->invoke('insert');
}
return $return;
}
public function delete() {
$id = $this->internalIdentifier();
if (isset($id)) {
db_delete($this->entityInfo['base table'])
->condition($this->idKey, $id)
->execute();
$this->invoke('delete');
entity_get_controller($this->entityType)->resetCache();
}
}
/**
* Invokes a hook and calls any field API attachers.
*/
public function invoke($hook) {
if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $hook)) {
$function($this->entityType, $this);
}
if (isset($this->entityInfo['bundle of']) && $type = $this->entityInfo['bundle of']) {
// Call field API bundle attachers for the entity we are a bundle of.
if ($hook == 'insert') {
field_attach_create_bundle($type, $this->{$this->bundleKey});
}
elseif ($hook == 'delete') {
field_attach_delete_bundle($type, $this->{$this->bundleKey});
}
elseif ($hook == 'update' && $id = $this->{$this->idKey}) {
$entities = entity_load($this->entityType, array($id));
if ($entities[$id]->{$this->bundleKey} != $this->{$this->bundleKey}) {
field_attach_rename_bundle($type, $entities[$id]->{$this->bundleKey}, $this->{$this->bundleKey});
}
}
}
module_invoke_all($this->entityType . '_' . $hook, $this);
if ($hook == 'insert' || $hook == 'update') {
entity_invoke($hook, $this->entityType, $this);
}
}
}
}
<?php
// $Id$
/**
* @file Extendable Object Faces API. Provided by the faces module.
*/
if (!interface_exists('FacesExtendableInterface', FALSE)) {
/**
* Interface for extendable objects.
*/
interface FacesExtendableInterface {
/**
* Extend the object by a class to implement the given interfaces.
*
* @param $interface
* The interface name or an array of interface names.
* @param $class
* The extender class, which has to implement the FacesExtenderInterface.
* @param $include
* An optional array describing the file to include before invoking the
* class. The array entries known are 'type', 'module', and 'name'
* matching the parameters of module_load_include(). Only 'module' is
* required as 'type' defaults to 'inc' and 'name' to NULL.
*/
public function extendByClass($interface, $class, array $include = array());
/**
* Extend the object by the given functions to implement the given
* interface. There has to be an implementation function for each method of
* the interface.
*
* @param $interface
* The interface name.
* @param $methods
* An array, where the keys are methods of the given interface and the
* values the callback functions to use.
* @param $includes
* An optional array to describe files to include before invoking the
* callbacks. You may pass a single array describing one include for all
* callbacks or an array of arrays, keyed by the method names. Look at the
* extendByClass() $include parameter for more details about how to
* describe a single file.
*/
public function extend($interface, array $methods = array(), array $includes = array());
/**
* Override the implementation of an extended method.
*
* @param $methods
* An array of methods of the interface, that should be overriden, where
* the keys are methods to override and the values the callback functions
* to use.
* @param $includes
* An optional array to describe files to include before invoking the
* callbacks. You may pass a single array describing one include for all
* callbacks or an array of arrays, keyed by the method names. Look at the
* extendByClass() $include parameter for more details about how to
* describe a single file.
*/
public function override(array $methods = array(), array $includes = array());
/**
* Returns whether the object can face as the given interface, thus it returns
* TRUE if this oject has been extended by an appropriate implementation.
*
* @param $interface
* Optional. A interface to test for. If it's omitted, all interfaces that
* the object can be faced as are returned.
* @return
* Whether the object can face as the interface or an array of interface names.
*/
public function facesAs($interface = NULL);
}
/**
* Interface for extenders.
*/
interface FacesExtenderInterface {
/**
* Returns an array of interface names the extender implements dynamically.
*/
function implementsFaces();
}
/**
* The Exception thrown by the FacesExtendable.
*/
class FacesExtendableException extends ErrorException {}
}
if (!class_exists('FacesRoot', FALSE)) {
/**
* Provides a common ancestor that makes it possible for extenders to access
* protected object properties and methods of the extendable object.
*/
abstract class FacesRoot {
/**
* Returns any property.
*/
protected function &property($name) {
return $this->$name;
}
/**
* Invokes any method.
*
* This also allows to pass arguments by reference, so it may be used to
* pass arguments by reference to dynamically extended methods.
*
* @param $name
* The method name.
* @param $arguments
* An array of arguments to pass to the method.
*/
public function call($name, array $args = array()) {
if (method_exists($this, $name)) {
return call_user_func_array(array($this, $name), $args);
}
return $this->__call($name, $args);
}
}
}
if (!class_exists('FacesExtender', FALSE)) {
/**
* A common base class for FacesExtenders. Extenders may access protected
* methods and properties of the extendable using the property() and call()
* methods.
*/
abstract class FacesExtender extends FacesRoot implements FacesExtenderInterface {}
}
if (!class_exists('FacesExtendable', FALSE)) {
/**
* Class providing an implementation of FacesExtendableInterface.
*/
abstract class FacesExtendable extends FacesRoot implements FacesExtendableInterface {
protected $facesMethods = array();
protected $faces = array();
protected $facesIncludes = array();
static protected $facesIncluded = array();
/**
* Wraps calls to module_load_include() to prevent multiple inclusions.
*
* @see module_load_include()
*/
protected static function load_include($args) {
$args += array('type' => 'inc', 'module' => '', 'name' => NULL);
$key = implode(':', $args);
if (!isset(self::$facesIncluded[$key])) {
self::$facesIncluded[$key] = TRUE;
module_load_include($args['type'], $args['module'], $args['name']);
}
}
/**
* Magic method: Invoke the dynamically implemented methods.
*/
function __call($name, $arguments = array()) {
if (isset($this->facesMethods[$name])) {
$method = $this->facesMethods[$name];
// Include code, if necessary.
if (isset($this->facesIncludes[$name])) {
self::load_include($this->facesIncludes[$name]);
$this->facesIncludes[$name] = NULL;
}
// We always pass the object reference and the name of the invoked method.
array_push($arguments, $this);
array_push($arguments, $name);
// Invoke the callback or extender class.
$callback = isset($method[0]) ? $method[0] : array($method[1], $name);
return call_user_func_array($callback, $arguments);
}
$class = check_plain(get_class($this));
throw new FacesExtendableException("There is no method $name for this instance of the class $class.");
}
/**
* Implements FacesExtendableInterface.
*/
public function facesAs($interface = NULL) {
if (!isset($interface)) {
return array_values($this->faces);
}
return in_array($interface, $this->faces) || $this instanceof $interface;
}
/**
* Implements FacesExtendableInterface.
*/
public function extendByClass($interface, $className, array $includes = array()) {
if (!in_array('FacesExtenderInterface', class_implements($className))) {
throw new FacesExtendableException("The class " . check_plain($className) . " doesn't implement the FacesExtenderInterface.");
}
$faces = call_user_func(array($className, 'implementsFaces'));
$interfaces = is_array($interface) ? $interface : array($interface);
foreach ($interfaces as $interface) {
if (!in_array($interface, $faces)) {
throw new FacesExtendableException("The class " . check_plain($className) . " doesn't implement the interface " . check_plain($interface) . ".");
}
$this->faces[$interface] = $interface;
$this->faces += class_implements($interface);
$face_methods = get_class_methods($interface);
$this->addIncludes($face_methods, $includes);
foreach ($face_methods as $method) {
$this->facesMethods[$method] = array(1 => $className);
}
}
}
/**
* Implements FacesExtendableInterface.
*/
public function extend($interface, array $callbacks = array(), array $includes = array()) {
$face_methods = get_class_methods($interface);
if (array_diff($face_methods, array_keys($callbacks))) {
throw new FacesExtendableException("Missing methods for implementing the interface " . check_plain($interface) . ".");
}
$this->faces[$interface] = $interface;
$this->faces += class_implements($interface);
$this->addIncludes($face_methods, $includes);
foreach ($face_methods as $method) {
$this->facesMethods[$method] = array(0 => $callbacks[$method]);
}
}
/**
* Implements FacesExtendableInterface.
*/
public function override(array $callbacks = array(), array $includes = array()) {
if (array_diff_key($callbacks, $this->facesMethods)) {
throw new FacesExtendableException("A not implemented method is to be overridden.");
}
$this->addIncludes(array_keys($callbacks), $includes);
foreach ($callbacks as $method => $callback) {
$this->facesMethods[$method] = array(0 => $callback);
}
}
/**
* Adds in include files for the given methods while removing any old files.
* If a single include file is described, it's added for all methods.
*/
protected function addIncludes($methods, $includes) {
$includes = isset($includes['module']) && is_string($includes['module']) ? array_fill_keys($methods, $includes) : $includes;
$this->facesIncludes = $includes + array_diff_key($this->facesIncludes, array_flip($methods));
}
/**