updated some more modules

This commit is contained in:
Bachir Soussi Chiadmi
2017-01-22 19:41:28 +01:00
parent 8416e3eea1
commit a0fadb0757
271 changed files with 11309 additions and 2135 deletions
@@ -0,0 +1,82 @@
<?php
/**
* @file
* Contains CerEndPointIterator.
*/
/**
* @class
* The purpose of this iterator is to wrap around all the "endpoints" in a field chain.
* An endpoint is a CerFieldHandler for a field that hasn't got a child. This is necessary
* in order to support infinite levels of embedded entities (read: field collections).
* This class is only instantiated by CerFieldChainHandler if its initial field handler
* has a child (@see CerFieldHandler::__construct()).
*/
class CerEndPointIterator implements RecursiveIterator {
/**
* @var CerField
*/
protected $field;
/**
* @var CerFieldHandler
*/
protected $handler;
public function __construct(CerField $field, EntityDrupalWrapper $entity) {
$this->field = $field;
$this->handler = $field->getHandler($entity);
}
/**
* Implements Iterator::current().
*/
public function current() {
return $this->field->child()->getHandler($this->handler->current());
}
/**
* Implements Iterator::key().
*/
public function key() {
return $this->handler->key();
}
/**
* Implements Iterator::next().
*/
public function next() {
$this->handler->next();
}
/**
* Implements Iterator::rewind().
*/
public function rewind() {
$this->handler->rewind();
}
/**
* Implements Iterator::handler().
*/
public function valid() {
return $this->handler->valid();
}
/**
* Implements RecursiveIterator::hasChildren().
*/
public function hasChildren() {
return ($this->field->child() instanceof CerEntityContainerInterface);
}
/**
* Implements RecursiveIterator::getChildren().
*/
public function getChildren() {
return new CerEndPointIterator($this->field->child(), $this->handler->current());
}
}
@@ -0,0 +1,24 @@
<?php
/**
* @interface
*
* This interface should be implemented by field plugins which can create embedded
* entities on the fly.
*
* The prime example is field collections. CER might need to add a backreference on
* a field which is in a field collection, yet there might be no field collection items
* on which to add the reference. In that case, a new field collection item must be
* be created and referenced properly by the owner.
*/
interface CerEntityContainerInterface {
/**
* Creates an embedded entity.
*
* @return EntityDrupalWrapper
* The newly created embedded entity, in a metadata wrapper.
*/
public function createInnerEntity(EntityDrupalWrapper $owner);
}
@@ -0,0 +1,159 @@
<?php
/**
* @file
* Contains the base class for CER field plugins.
*
* A field plugin tells CER how to interact with fields of a certain type. If a particular
* field type integrates with Entity API, its CER plugin can be as simple as extending
* CerField and implementing the getTargetType() method.
*
* @todo
* More info about extending CerFieldHandler to further customize field plugins.
*/
/**
* @class
* Represents a single field instance.
*/
abstract class CerField extends FieldInstance {
/**
* @var array
* The plugin definition.
*/
protected $plugin;
/**
* @var array
*/
protected $settings = array();
/**
* @var string
*/
public $fieldTypeLabel;
/**
* Gets the type of entity that can be referenced by this field, e.g. 'node'.
*
* @return string
*/
abstract public function getTargetType();
/**
* Constructor. Pretty self-explanatory!
*/
public function __construct(array $plugin) {
// Store a copy of our plugin definition.
$this->plugin = $plugin;
list ($entity_type, $bundle, $field_name) = explode(':', $plugin['identifier']);
parent::__construct($entity_type, $bundle, $field_name);
// Store a copy of the field settings for convenience. At the time of this
// writing, this is needed by the Entity Reference, Node Reference,
// and Term Reference plugins.
$info = field_info_field($this->name);
$this->settings = $info['settings'];
$type_info = field_info_field_types($info['type']);
$this->fieldTypeLabel = $type_info['label'];
}
/**
* Returns a CerFieldHandler subclass instance for the given entity.
*
* @param object $entity
* The entity to be wrapped by the handler.
*
* @return CerFieldHandler
*/
public function getHandler(EntityDrupalWrapper $entity) {
return new $this->plugin['handler']($this, $entity);
}
/**
* Returns the bundles that this field instance can reference.
*
* @return array
*/
public function getTargetBundles() {
$info = entity_get_info($this->getTargetType());
return array_keys($info['bundles']);
}
/**
* Overridden.
*/
public function requireParent() {
return $this->plugin['require parent'];
}
/**
* Overridden.
*/
public function getParents() {
return array_map('CerField::getPlugin', $this->plugin['parents']);
}
/**
* Returns information about a particular field plugin by its identifier, or all
* available plugins (i.e., defined by hook_cer_fields()) if no identifier is given.
* The aggregated result of hook_cer_fields() is statically cached.
*/
public static function getPluginInfo($identifier = NULL) {
$info = &drupal_static(__METHOD__);
if (! isset($info)) {
$info = module_invoke_all('cer_fields');
foreach ($info as $key => &$field) {
$field += array(
'identifier' =>
$key,
'parents' =>
array(),
'require parent' =>
FALSE,
'handler' =>
'CerFieldHandler',
);
}
drupal_alter('cer_fields', $info);
}
return ($identifier ? (isset($info[$identifier]) ? $info[$identifier] : NULL) : $info);
}
/**
* Returns a single field plugin instance, by its identifier. All plugin instances
* are statically cached.
*
* @param string $identifier
* The plugin's identifier, in the format entity_type:bundle:field_name.
*
* @return CerField
*
* @throws Exception if there's no plugin for the given identifier. Why so harsh, you
* ask? Because CerFieldChain::unpack() utterly depends on being able to get plugin
* instances for every field in the chain, and if it can't, it could result in myriad
* weird and serious problems. Throwing an exception will head that off at the pass.
*/
public static function getPlugin($identifier) {
$instances = &drupal_static(__METHOD__);
if (! isset($instances[$identifier])) {
$info = self::getPluginInfo($identifier);
if ($info) {
$instances[$identifier] = new $info['class']($info);
}
else {
throw new Exception("Cannot get instance of invalid plugin $identifier.");
}
}
return $instances[$identifier];
}
}
@@ -0,0 +1,91 @@
<?php
/**
* @file
* Contains the CerFieldChain class.
*/
class CerFieldChain extends FieldChain {
/**
* Convenience method. Returns a handler for this chain in the context of
* the given entity.
*
* @return CerFieldChainHandler
*/
public function getHandler(EntityDrupalWrapper $entity) {
return new CerFieldChainHandler($this, $entity);
}
/**
* Gets a regular expression to match field chain identifiers that this chain
* can reference, e.g. /^node:(page|article):/
*/
public function regex() {
$end = $this->end();
return '/^' . $end->getTargetType() . ':(' . implode('|', $end->getTargetBundles()) . '):/';
}
/**
* Returns a Features export pipe for this chain, including every field and
* field instance in it.
*/
public function export() {
$pipe = array();
foreach ($this->chain as $field) {
$pipe['field_instance'][] = "{$field->entityType}-{$field->bundle}-{$field->name}";
}
return $pipe;
}
/**
* Returns an array of every possible field chain for every field defined in
* hook_cer_fields().
*
* @return array
*/
public static function collectAll() {
$chains = array();
foreach (array_keys(CerField::getPluginInfo()) as $identifier) {
$chains = array_merge($chains, self::collect($identifier));
}
return $chains;
}
/**
* Returns an array of every possible field chain for a single field,
* identified by its key in hook_cer_fields().
*
* @return array
*/
public static function collect($identifier) {
$chains = array();
$chain = new CerFieldChain();
$chain->addField(CerField::getPlugin($identifier), $chains);
return $chains;
}
/**
* Constructs and returns a CerFieldChain object from an encoded string
* of field plugin identifiers glued together with ::.
*
* @return CerFieldChain
*/
public static function unpack($identifier) {
$chain = new CerFieldChain();
foreach (array_reverse(explode('::', $identifier)) as $field) {
$chain->addField(CerField::getPlugin($field));
}
return $chain;
}
}
@@ -0,0 +1,107 @@
<?php
/**
* @file
* Contains the CerFieldChainHandler object.
*/
/**
* @class
* Wraps around every CerFieldHandler object in a chain. In any given chain, there
* could be many entities that need to be processed -- think about multi-value field
* collections embedded within other multi-value field collections, and you quickly
* see how extensive the recursion can be. CER needs to be able to handle crazy
* scenarios like that and still perform add/delete operations transparently. That's
* what this guy does.
*/
class CerFieldChainHandler {
/**
* @var CerFieldChain
*/
protected $chain;
/**
* @var EntityDrupalWrapper
*/
protected $entity;
/**
* @var array or RecursiveIteratorIterator
*/
protected $handlers;
public function __construct(CerFieldChain $chain, EntityDrupalWrapper $entity) {
$this->chain = $chain;
$this->entity = $entity;
$chain->__wakeup();
$chain->seek($entity->cer->depth->value());
$field = $chain->current();
// If the field has a child, there could be extensive recusion here. So we'll need
// to iterate over the entire chain recursively -- luckily, SPL provides the
// RecursiveIteratorIterator class for this purpose. But if there are no children,
// we don't need to recurse; the only handler we'll need to load is the current
// field's, for the current entity.
if ($field->child()) {
$this->handlers = new RecursiveIteratorIterator(new CerEndPointIterator($field, $entity));
}
else {
// Wrap the handler in an array, just to normalize things internally.
$this->handlers = array( $field->getHandler($entity) );
}
}
/**
* Returns the IDs of every entity referenced in this chain. If there are no references,
* an empty array is returned.
*
* @return array
*/
public function getIDs() {
$IDs = array();
foreach ($this->handlers as $handler) {
$IDs = array_merge($handler->getIDs(), $IDs);
}
return array_unique($IDs);
}
/**
* Adds a reference to the given entity.
*/
public function add(EntityDrupalWrapper $entity) {
$owner = $this->entity;
foreach ($this->chain as $field) {
// If the current field implements CerEntityContainerInterface, we can
// create an entity on-the-fly to receive the reference, if there isn't
// one already.
if ($field instanceof CerEntityContainerInterface) {
$items = $field->getHandler($owner);
// If there is are items which could receive the reference, seek to the
// last one. Otherwise, create one.
if (sizeof($items) == 0) {
$owner = $field->createInnerEntity($owner);
}
else {
$items->seek(-1);
$owner = $items->current();
}
}
}
$field->getHandler($owner)->add($entity);
}
public function delete(EntityDrupalWrapper $entity) {
foreach ($this->handlers as $handler) {
$handler->delete($entity);
}
}
}
@@ -0,0 +1,306 @@
<?php
/**
* @file
* Contains CerFieldHandler.
*/
/**
* @class
* Handles low-level operations for a single field on a single entity. Exposes
* methods to add, delete and check for references. This will also iterate over
* the references, returning each one as an EntityDrupalWrapper object.
*/
class CerFieldHandler implements Countable, SeekableIterator {
/**
* @var CerField
*/
protected $field;
/**
* @var EntityDrupalWrapper
*/
protected $entity;
/**
* @var EntityMetadataWrapper
*/
protected $value;
/**
* @var integer
*/
protected $delta = 0;
/**
* @var boolean
*/
protected $isMultiValue;
public function __construct(CerField $field, EntityDrupalWrapper $entity) {
$this->field = $field;
$this->entity = $entity;
$this->value = $entity->{ $field->name };
$this->isMultiValue = ($this->value instanceof EntityListWrapper);
$this->rewind();
}
/**
* Adds a reference to $entity, validating it first.
*
* @param EntityDrupalWrapper $entity
* The wrapped entity to reference.
*/
public function add(EntityDrupalWrapper $entity) {
if ($this->validate($entity)) {
$this->write();
}
}
/**
* Deletes all references to $entity.
*
* @param EntityDrupalWrapper $entity
* The wrapped entity to dereference.
*/
public function delete(EntityDrupalWrapper $entity) {
$entityID = $entity->getIdentifier();
if ($this->isMultiValue) {
foreach ($this->value as $delta => $ref) {
if ($entityID == $ref->getIdentifier()) {
$this->value[$delta]->set(NULL);
}
}
}
elseif ($entityID == $this->value->getIdentifier()) {
$this->value->set(NULL);
}
$this->write();
}
/**
* Validates a potential reference. After doing a cardinality check, the
* reference is validated through the Field Attach API, allowing the module
* which owns the field to do its normal validation logic. If validation
* fails, the error(s) are logged.
*
* @param EntityDrupalWrapper $entity
* The wrapped entity to validate.
*
* @return boolean
*/
protected function validate(EntityDrupalWrapper $entity) {
// Before we do anything else, check that the field has enough space to add the
// reference. If there isn't, bail out so we don't blindly overwrite existing
// field data.
if ($this->checkCardinality()) {
// Keep the previous value so we can restore it if validation fails.
$prev_value = $this->value->value();
if ($this->isMultiValue) {
$value = $this->value->value();
$value[] = $entity->value();
$this->value->set($value);
}
else {
$this->value->set( $entity->value() );
}
// Leverage the Field Attach API to validate the reference. If errors occur,
// field_attach_validate() throws FieldValidationException, containing an array
// of every validation error.
try {
// Only validate this field.
field_attach_validate($this->entity->type(), $this->entity->value(), array('field_name' => $this->field->name));
return TRUE;
}
catch (FieldValidationException $e) {
foreach ($e->errors as $field) {
foreach ($field as $language) {
foreach ($language as $errors) {
foreach ($errors as $error) {
$this->logError($error['message'], $entity);
}
}
}
}
$this->value->set($prev_value);
}
}
else {
$this->logError('Cannot add reference to !that_link from !field_label on !this_link because there are no more slots available.', $entity);
}
return FALSE;
}
/**
* Checks that there are enough slots in the field to add a reference.
*
* @return boolean
*/
protected function checkCardinality() {
return ($this->field->cardinality == FIELD_CARDINALITY_UNLIMITED ? TRUE : ($this->field->cardinality > $this->count()));
}
/**
* Saves changes to the entity and resets the iterator.
*/
protected function write() {
$entity_type = $this->entity->type();
$entityID = $this->entity->getIdentifier();
$entity = $this->entity->value();
$entity->cer_processed = TRUE;
entity_save($entity_type, $entity);
// Reload the entity we just saved and cleared from the static cache.
$entities = entity_load($entity_type, (array) $entityID);
$this->entity->set($entities[$entityID]);
$this->__construct($this->field, $this->entity);
}
/**
* Logs an error, optionally against a specific entity. If the cer_debug
* variable is set, the error will also be set as a message.
*
* @param string $message
* The untranslated message to log.
*
* @param EntityDrupalWrapper $entity
* The entity that has caused the error, if any.
*/
protected function logError($message, EntityDrupalWrapper $entity = NULL) {
$variables = array(
'!field_name' => $this->field->name,
'!field_type' => $this->field->fieldTypeLabel,
'!field_label' => $this->field->label,
);
$variables['!this_type'] = $this->entity->type();
$variables['!this_label'] = $this->entity->label();
// If the entity has a URI, provide a link to it. Otherwise, its "link"
// will just be an unlinked label. Entity API doesn't reliably expose a url
// property on entities, and there doesn't appear to be a way to check for
// it without risking an EntityMetadataWrapperException. So I need to use
// this clunky BS instead...ugh.
$this_uri = entity_uri($this->entity->type(), $this->entity->value());
if (isset($this_uri)) {
$variables['!this_url'] = url($this_uri['path'], $this_uri['options']);
$variables['!this_link'] = l($this->entity->label(), $this_uri['path'], $this_uri['options']);
}
else {
$variables['!this_link'] = $this->entity->label();
}
if ($entity) {
$variables['!that_type'] = $entity->type();
$variables['!that_label'] = $entity->label();
// If the entity has a URI, link to it.
$that_uri = entity_uri($entity->type(), $entity->value());
if (isset($that_uri)) {
$variables['!that_url'] = url($that_uri['path'], $that_uri['options']);
$variables['!that_link'] = l($entity->label(), $that_uri['path'], $that_uri['options']);
}
else {
$variables['!that_link'] = $entity->label();
}
}
watchdog('cer', $message, $variables, WATCHDOG_ERROR);
if (variable_get('cer_debug', FALSE)) {
drupal_set_message(t($message, $variables), 'error');
}
}
public function getIDs() {
$IDs = array();
if ($this->isMultiValue) {
foreach ($this->value as $ref) {
$IDs[] = $ref->raw();
}
}
else {
$IDs[] = $this->value->raw();
}
return array_unique(array_filter($IDs));
}
/**
* Implements Countable::count().
*/
public function count() {
if ($this->isMultiValue) {
return sizeof($this->value);
}
else {
return ($this->value->value() ? 1 : 0);
}
}
/**
* Implements SeekableIterator::seek().
*/
public function seek($position) {
$length = $this->count();
if ($position < 0) {
$position += $length;
}
if ($position >= 0 && $position < $length) {
$this->delta = $position;
}
else {
throw new OutOfBoundsException(t('Cannot seek to invalid position.'));
}
}
/**
* Implements Iterator::current().
*/
public function current() {
return ($this->isMultiValue ? $this->value[$this->delta] : $this->value);
}
/**
* Implements Iterator::key().
*/
public function key() {
return $this->current()->getIdentifier();
}
/**
* Implements Iterator::next().
*/
public function next() {
$this->delta++;
}
/**
* Implements Iterator::rewind().
*/
public function rewind() {
$this->delta = 0;
}
/**
* Implements Iterator::valid().
*/
public function valid() {
return ($this->delta < $this->count());
}
}
@@ -0,0 +1,75 @@
<?php
/**
* @file
* Contains the entity class for CER presets.
*/
class CerPreset extends Entity {
/**
* @var integer
* The preset's numeric ID in the database.
*/
public $pid = 0;
/**
* @var string
* The export identifier, in the format $this->cer_left*$this->cer_right.
*/
public $identifier;
/**
* @var EntityMetadataWrapper
* A metadata wrapper around this preset, for convenience.
*/
public $wrapper;
/**
* @var integer
* The exportable status of this preset.
*/
public $status = 0x01; // ENTITY_CUSTOM
/**
* @var string
* The module exporting this preset.
*/
public $module = 'cer';
/**
* Overrides Entity::__construct().
*/
public function __construct(array $values = array()) {
parent::__construct($values, 'cer');
$this->wrapper = new EntityDrupalWrapper('cer', $this);
}
/**
* Overrides Entity::label().
*/
public function label() {
return isset($this->label_variables) ? t('@left !operator @right', $this->label_variables) : $this->defaultLabel();
}
/**
* Overrides Entity::save().
*/
public function save() {
// Generate the export identifier automagically before saving.
$this->identifier = $this->wrapper->cer_left->value() . '*' . $this->wrapper->cer_right->value();
parent::save();
}
public function invert() {
$init = get_object_vars($this);
unset($init['pid'], $init['wrapper'], $init['identifier'], $init['status']);
$buf = $init['cer_left'];
$init['cer_left'] = $init['cer_right'];
$init['cer_right'] = $buf;
return entity_create('cer', $init);
}
}
@@ -0,0 +1,50 @@
<?php
/**
* The controller class for CerPreset entities.
*/
class CerPresetController extends EntityAPIControllerExportable {
/**
* Overridden.
*/
public function export($entity, $prefix = '') {
$variables = get_object_vars($entity);
// I really wish Entity API tried to notify the entity that it's being
// exported so that it could clean itself up first, but it doesn't. So we
// gotta do this bizness.
unset($variables['pid'], $variables['wrapper'], $variables['status'], $variables['module'], $variables['label_variables'], $variables['uid']);
// Features 2.x checks for overriddenness using sorted keys, which means
// that if the variables aren't key-sorted the presets will always be
// considered overridden, even if they actually aren't.
ksort($variables);
return entity_var_json_export($variables, $prefix);
}
/**
* Overridden.
*/
protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
parent::attachLoad($queried_entities, $revision_id);
foreach ($queried_entities as $preset) {
// Attach variables used to build the human-readable preset label. These
// need to be attached after the Field API has done its magic (i.e.,
// during parent::attachLoad()), since the label depends on field values.
// @see CerPreset::label().
$fields = field_attach_view('cer', $preset, 'default');
$preset->label_variables = array(
'@left' =>
render($fields['cer_left'][0]),
'@right' =>
render($fields['cer_right'][0]),
'!operator' =>
$preset->wrapper->cer_bidirectional->value() ? '<>' : '>',
);
}
}
}
@@ -0,0 +1,26 @@
<?php
/**
* Contains the controller class for exporting CER presets via Features.
*/
class CerPresetFeaturesController extends EntityDefaultFeaturesController {
/**
* Overridden.
*/
public function export($data, &$export, $module = '') {
$pipe = parent::export($data, $export, $module);
// Every field in both chains may need to export additional things (the
// field base and instance definitions at least, plus any extra dependencies).
// All that logic is delegated to CerFieldChain.
foreach (entity_load_multiple_by_name($this->type, $data) as $preset) {
$pipe = array_merge_recursive($pipe, $preset->wrapper->cer_left->chain->value()->export());
$pipe = array_merge_recursive($pipe, $preset->wrapper->cer_right->chain->value()->export());
}
return $pipe;
}
}
@@ -0,0 +1,71 @@
<?php
/**
* This class is a unified way for CER to find the presets that apply to a given
* entity. The result set is segmented into two parts: presets where the entity
* is on the left side, and bidirectional presets with the entity on the right
* side (i.e., the ones which need to be inverted before use). The execute()
* method will return a merged and sorted array of presets, but the segmented
* result set is exposed to the world as well for other uses (i.e., CER Entity
* Settings' selection handler).
*/
class CerPresetFinder extends EntityFieldQuery {
public $result = array();
protected $entity;
public function __construct(EntityDrupalWrapper $entity) {
$this->entity = $entity;
$this
->entityCondition('entity_type', 'cer')
->addTag('cer_presets')
->addMetaData('entity', $entity);
}
public function execute() {
$lineage = $this->entity->cer->lineage->value();
$this->result['cer'] = $this
->fieldCondition('cer_enabled', 'value', TRUE)
->fieldCondition('cer_left', 'path', $lineage, 'STARTS_WITH')
->_load(parent::execute());
$this->fieldConditions = array();
$this->result['cer__invert'] = $this
->fieldCondition('cer_enabled', 'value', TRUE)
->fieldCondition('cer_bidirectional', 'value', TRUE)
->fieldCondition('cer_right', 'path', $lineage, 'STARTS_WITH')
->_load(parent::execute());
$result = $this->result['cer'];
foreach ($this->result['cer__invert'] as $preset) {
$result[] = $preset->invert();
}
usort($result, array($this, '_sort'));
return $result;
}
private function _load(array $result) {
return isset($result['cer']) ? entity_load('cer', array_keys($result['cer'])) : array();
}
private function _sort(CerPreset $a, CerPreset $b) {
$a_weight = $a->wrapper->cer_weight->value();
$b_weight = $b->wrapper->cer_weight->value();
if ($a_weight > $b_weight) {
return 1;
}
elseif ($b_weight > $a_weight) {
return -1;
}
else {
return 0;
}
}
}
@@ -0,0 +1,155 @@
<?php
/**
* @file
* Contains CerPresetHandler.
*/
/**
* @class
* Contains the logic for performing CER operations on a single entity,
* using a single preset.
*/
class CerPresetHandler {
/**
* @var CerFieldChain
*/
protected $left;
/**
* @var CerFieldChain
*/
protected $right;
/**
* @var EntityDrupalWrapper
*/
protected $entity;
/**
* @var array
*/
protected $refIDs;
public function __construct(CerPreset $preset, EntityDrupalWrapper $entity) {
$this->left = $preset->wrapper->cer_left->chain->value();
$this->right = $preset->wrapper->cer_right->chain->value();
$this->entity = $entity;
// Store the current set of reference IDs so that we only need to instantiate
// the left handler once.
$this->refIDs = $this->left->getHandler( $entity )->getIDs();
}
/**
* Process an entity insert. This loops through the referenced entity $IDs and
* adds a reference to this entity if the reference doesn't already have one.
*/
public function insert(array $IDs = array()) {
// If no IDs were passed in, use the current reference set.
$IDs = ($IDs ? $IDs : $this->refIDs);
// Get this entity's ID right now, so we don't have to keep calling
// $this->entity->cer->owner->getIdentifier(). Hooray for micro-optimization!
$myID = $this->entity->cer->owner->getIdentifier();
foreach ($this->load( $IDs ) as $ref) {
$handler = $this->right->getHandler( $ref );
// Only create the backreference if the reference doesn't already reference
// this entity (which it might, if there is more than one preset that references
// a single field instance).
if (! in_array($myID, $handler->getIDs())) {
$handler->add( $this->entity->cer->owner );
}
}
}
/**
* Process an entity update. This could be either a normal update done by a user,
* or a bulk update.
*/
public function update() {
// Get the previous set of reference IDs. $entity->cer->original will return either
// $entity->original, if it exists, or the current entity. So, if this is a bulk
// update, $originalIDs will be identical to $this->refIDs.
$originalIDs = $this->left->getHandler( $this->entity->cer->original )->getIDs();
// If there are any references that were in the previous set but not the current
// set, delete those backreferences. Under normal circumstances, there will be
// nothing to delete during a bulk update, since the previous set and current
// set should be identical.
$deleted = array_diff($originalIDs, $this->refIDs);
if ($deleted) {
$this->delete($deleted);
}
// If the previous set is identical to the current set, we'll be processing
// all existing references (see the first line of $this->insert()).
$added = array_diff($this->refIDs, $originalIDs);
$this->insert($added);
}
/**
* Process an entity delete. Loops through the referenced entity IDs and clears
* their references to this entity.
*/
public function delete(array $IDs = array()) {
// As with $this->insert(), we can process a specific set of references or
// everything in the current set.
$IDs = ($IDs ? $IDs : $this->refIDs);
foreach ($this->load( $IDs ) as $ref) {
$this->right->getHandler( $ref )->delete( $this->entity->cer->owner );
}
}
/**
* Loads referenced entities. This might seem like a convenience method, but it
* is a critical part CER's core logic.
*
* @param array $IDs
* Array of entity IDs to load.
*
* @return array
* The requested entities, wrapped by EntityDrupalWrapper. If nothing could be
* loaded, an empty array is returned.
*/
protected function load(array $IDs) {
if (empty($IDs)) {
return array();
}
$this->right->rewind();
$right = $this->right->current();
$entity_type = $right->entityType;
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', $entity_type);
$query->entityCondition('entity_id', $IDs);
// If the right entity type has bundles, we need to filter by that too. If we don't,
// we could run into a bug where, if the left field can reference multiple bundles,
// we might try to modify the wrong entity. Essentially, the loading of referenced
// entities should be as targeted as possible to prevent ambiguities and buggery.
if ($right->isBundleable) {
$query->entityCondition('bundle', $right->bundle);
}
$result = $query->execute();
if (isset($result[$entity_type])) {
$result[$entity_type] = entity_load($entity_type, array_keys($result[$entity_type]));
foreach ($result[$entity_type] as $id => $entity) {
$result[$entity_type][$id] = new EntityDrupalWrapper($entity_type, $entity);
}
return $result[$entity_type];
}
else {
return array();
}
}
}
@@ -0,0 +1,220 @@
<?php
/**
* Contains the controller class for CER's UI (i.e., preset management pages),
* used by Entity API.
*/
class CerUIController extends EntityDefaultUIController {
public function hook_menu() {
$items = parent::hook_menu();
$items[$this->path]['title'] = t('Corresponding Entity References');
$items["{$this->path}/list"]['title'] = t('Presets');
$this->setTitle($items["{$this->path}/add"], t('Add preset'));
$this->setTitle($items["{$this->path}/import"], t('Import preset'));
$items["{$this->path}/manage/%entity_object/toggle"] = $this->createCallback('cer_toggle_preset', 'update');
$items["{$this->path}/manage/%entity_object/invert"] = $this->createCallback('cer_invert_preset', 'create');
// Don't provide a page for cloning a preset.
unset($items["{$this->path}/manage/%entity_object/clone"]);
return $items;
}
private function createCallback($function, $operation, array $init = array()) {
return $init + array(
'type' => MENU_CALLBACK,
'page callback' => $function,
'page arguments' => array(5),
'load arguments' => array('cer'),
'access callback' => 'entity_access',
'access arguments' => array($operation, 'cer'),
'file' => 'cer.admin.inc',
'file path' => drupal_get_path('module', 'cer'),
);
}
/**
* Sets a static title on a menu item.
*/
private function setTitle(array &$item, $title) {
$item['title'] = $title;
unset($item['title callback'], $item['title arguments']);
}
public function operationForm($form, &$form_state, $entity, $action) {
switch ($action) {
case 'delete':
return confirm_form($form, t('Are you sure you want to delete this preset?'), $this->path, t('@left will no longer synchronize with @right.', $entity->label_variables));
default:
return parent::operationForm($form, $form_state, $entity, $action);
}
}
public function overviewForm($form, &$form_state) {
$form = parent::overviewForm($form, $form_state);
$form['actions'] = array(
'update' => array(
'#type' => 'submit',
'#value' => t('Save changes'),
),
'#type' => 'actions',
);
return $form;
}
public function overviewFormSubmit($form, &$form_state) {
foreach ($form_state['values']['table'] as $id => $values) {
$preset = entity_object_load($id, $this->entityType);
$preset->wrapper->cer_enabled->set($values['cer_enabled'][LANGUAGE_NONE][0]['value']);
$preset->wrapper->cer_bidirectional->set($values['cer_bidirectional'][LANGUAGE_NONE][0]['value']);
$preset->wrapper->cer_weight->set($values['cer_weight'][LANGUAGE_NONE][0]['value']);
$preset->save();
}
drupal_set_message(t('The changes have been saved.'));
}
public function overviewTable($conditions = array()) {
$render = array(
'#header' => array(
t('Left Field'),
t('Right Field'),
t('Status'),
t('Enabled'),
t('Bidirectional'),
t('Weight'),
t('Operations'),
),
'#tabledrag' => array(
array(
'action' => 'order',
'relationship' => 'sibling',
'group' => 'cer-weight',
),
),
'#empty' => t('None.'),
'#type' => 'table',
);
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', $this->entityType);
// Add all conditions to query.
foreach ($conditions as $key => $value) {
$query->propertyCondition($key, $value);
}
if ($this->overviewPagerLimit) {
$query->pager($this->overviewPagerLimit);
}
$query->fieldOrderBy('cer_weight', 'value');
$results = $query->execute();
$entities = isset($results['cer']) ? entity_load('cer', array_keys($results['cer'])) : array();
foreach ($entities as $entity) {
$render[$entity->pid] = $this->overviewTableRow($conditions, $entity->pid, $entity);
}
return $render;
}
protected function overviewTableRow($conditions, $id, $entity, $additional_cols = array()) {
$render_fields = field_attach_view($this->entityType, $entity, 'default');
$render_fields['cer_left']['#label_display'] = 'inline';
$render_fields['cer_left']['#title'] = $entity->wrapper->cer_left->chain->value()->end()->fieldTypeLabel;
$row['cer_left'] = $render_fields['cer_left'];
$render_fields['cer_right']['#label_display'] = 'inline';
$render_fields['cer_right']['#title'] = $entity->wrapper->cer_right->chain->value()->end()->fieldTypeLabel;
$row['cer_right'] = $render_fields['cer_right'];
$row['status'] = array(
'#theme' => 'entity_status',
'#status' => $entity->status,
);
$form_fields = array();
$form_state = form_state_defaults();
$form_state['build_info']['form_id'] = 'cer_overview_form';
field_attach_form($this->entityType, $entity, $form_fields, $form_state);
unset($form_fields['cer_enabled'][LANGUAGE_NONE]['#title']);
$row['cer_enabled'] = $form_fields['cer_enabled'];
unset($form_fields['cer_bidirectional'][LANGUAGE_NONE]['#title']);
$row['cer_bidirectional'] = $form_fields['cer_bidirectional'];
unset($form_fields['cer_weight'][LANGUAGE_NONE]['#title']);
$form_fields['cer_weight'][LANGUAGE_NONE]['#attributes']['class'][] = 'cer-weight';
$row['cer_weight'] = $form_fields['cer_weight'];
// Add in any passed additional cols.
foreach ($additional_cols as $key => $col) {
$row[$key] = $col;
}
// I like drop buttons more than an inline set of links.
$links = array(
'toggle' => array(
'title' => $entity->wrapper->cer_enabled->value() ? t('disable') : t('enable'),
'href' => "{$this->path}/manage/{$id}/toggle",
'query' => drupal_get_destination(),
),
'edit' => array(
'title' => t('edit'),
'href' => "{$this->path}/manage/{$id}",
),
);
// If the preset is one-directional, expose a link to invert it.
if (! $entity->wrapper->cer_bidirectional->value()) {
$links['invert'] = array(
'title' => t('invert'),
'href' => "{$this->path}/manage/{$id}/invert",
'query' => drupal_get_destination(),
);
}
if (entity_has_status($this->entityType, $entity, ENTITY_OVERRIDDEN)) {
$links['revert'] = array(
'title' => t('revert'),
'href' => "{$this->path}/manage/{$id}/revert",
'query' => drupal_get_destination(),
);
}
else {
$links['delete'] = array(
'title' => t('delete'),
'href' => "{$this->path}/manage/{$id}/delete",
'query' => drupal_get_destination(),
);
}
$links['export'] = array(
'title' => t('export'),
'href' => "{$this->path}/manage/{$id}/export",
);
$row['operations'] = array(
'#theme' => 'links__ctools_dropbutton',
'#links' => $links,
);
$row['#attributes']['class'][] = 'draggable';
return $row;
}
}
@@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains the CER plugin for Entity Reference fields.
*/
class CerEntityReferenceField extends CerField {
/**
* Implements CerField::getTargetType().
*/
public function getTargetType() {
return $this->settings['target_type'];
}
/**
* @override CerField::getTargetBundles().
*/
public function getTargetBundles() {
$bundles = array();
// If the reference field is using a view, load the view and see if it's filtering by the entity
// type's bundle filter. If it is, the filter values are the target bundles. Otherwise,
// assume that all bundles can be referenced.
//
// @todo Support contextual filters?
//
// NOTE: Selection handlers (i.e., $field['settings']['handler']) are plugins owned by
// Entity Reference. There is no method defined to get an array of referenceable
// bundles, but hopefully, if CER gains enough traction in the community, such a
// method can be added to the EntityReference_SelectionHandler interface. This
// function could then be deprecated, which would be a more flexible, future-proof
// method of finding a field's target bundles.
//
if ($this->settings['handler'] == 'views') {
$view = views_get_view($this->settings['handler_settings']['view']['view_name']);
$view->set_display($this->settings['handler_settings']['view']['display_name']);
$info = entity_get_info($this->getTargetType());
if (isset($info['entity keys']['bundle'])) {
$handler = $view->display_handler->get_handler('filter', $info['entity keys']['bundle']);
if ($handler) {
$bundles = $handler->value;
}
}
}
else {
$bundles = $this->settings['handler_settings']['target_bundles'];
}
return ($bundles ? $bundles : parent::getTargetBundles());
}
}
@@ -0,0 +1,39 @@
<?php
/**
* @file
* Contains the CER plugin for Field Collection fields.
*/
class CerFieldCollectionField extends CerField implements CerEntityContainerInterface {
/**
* Implements CerField::getTargetType().
*/
public function getTargetType() {
return 'field_collection_item';
}
/**
* @override CerField::getTargetBundles().
*/
public function getTargetBundles() {
return array($this->name);
}
/**
* Implements CerEntityContainerInterface::createInnerEntity().
*/
public function createInnerEntity(EntityDrupalWrapper $owner) {
// Create an empty field collection item.
$collection = new EntityDrupalWrapper('field_collection_item', entity_create('field_collection_item', array('field_name' => $this->name)));
$collection->host_entity->set( $owner );
$collection->save(TRUE);
// 'Reference' the newly created field collection item.
$this->getHandler($owner)->add($collection);
return $collection;
}
}
@@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains the CER plugin for Node Reference fields.
*/
class CerNodeReferenceField extends CerField {
/**
* Implements CerField::getTargetType().
*/
public function getTargetType() {
return 'node';
}
/**
* @override CerField::getTargetBundles().
*/
public function getTargetBundles() {
$bundles = array();
$view = $this->settings['view']['view_name'];
if ($view) {
$view = views_get_view($view);
$view->set_display($this->settings['view']['view_display']);
$handler = $view->display_handler->get_handler('filter', 'type');
if ($handler) {
$bundles = $handler->value;
}
}
else {
$bundles = array_filter($this->settings['referenceable_types']);
}
return ($bundles ? $bundles : parent::getTargetBundles());
}
}
@@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains the CER plugin for Taxonomy Term Reference fields.
*/
class CerTaxonomyTermReferenceField extends CerField {
/**
* Implements CerField::getTargetType().
*/
public function getTargetType() {
return 'taxonomy_term';
}
/**
* @override CerField::getTargetBundles().
*/
public function getTargetBundles() {
$bundles = array();
foreach ($this->settings['allowed_values'] as $item) {
$bundles[] = $item['vocabulary'];
}
return $bundles;
}
}
@@ -0,0 +1,18 @@
<?php
/**
* @file
* Contains the CER plugin for User Reference fields.
*/
class CerUserReferenceField extends CerField {
/**
* Implements CerField::getTargetType().
*/
public function getTargetType() {
return 'user';
}
}