|
@@ -0,0 +1,437 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+/**
|
|
|
+ * @file
|
|
|
+ * Contains base code for CER handlers, which are objects responsible for
|
|
|
+ * creating, updating and deleting corresponding references between entities.
|
|
|
+ */
|
|
|
+
|
|
|
+/**
|
|
|
+ * Exception related to CER operations.
|
|
|
+ */
|
|
|
+class CerException extends Exception {
|
|
|
+}
|
|
|
+
|
|
|
+interface CerHandlerInterface {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @constructor
|
|
|
+ *
|
|
|
+ * @param string $preset
|
|
|
+ * The CER preset string, in the format:
|
|
|
+ * entity_a*bundle_a*field_a*entity_b*bundle_b*field_b.
|
|
|
+ *
|
|
|
+ * @param $entity.
|
|
|
+ * The local (home) entity to be wrapped by this instance.
|
|
|
+ */
|
|
|
+ public function __construct($preset, $entity);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Create reciprocal references on referenced entities after the
|
|
|
+ * local entity has been created.
|
|
|
+ */
|
|
|
+ public function insert();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Delete reciprocal references on entities the local entity is no
|
|
|
+ * longer referencing, and create new reciprocal references, after
|
|
|
+ * the local entity has been updated.
|
|
|
+ */
|
|
|
+ public function update();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Delete all reciprocal references after the local entity is deleted.
|
|
|
+ */
|
|
|
+ public function delete();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Check if $entity is referenced by the local entity.
|
|
|
+ *
|
|
|
+ * @param object $entity
|
|
|
+ * The remote entity.
|
|
|
+ *
|
|
|
+ * @return boolean
|
|
|
+ */
|
|
|
+ public function references($entity);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Check if the local entity is referenced by $entity.
|
|
|
+ *
|
|
|
+ * @param object $entity
|
|
|
+ * The remote entiy.
|
|
|
+ *
|
|
|
+ * @return boolean
|
|
|
+ */
|
|
|
+ public function referencedBy($entity);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Check if the remote entity can reference the local entity, and vice-versa.
|
|
|
+ *
|
|
|
+ * @param object $entity
|
|
|
+ * The remote entity.
|
|
|
+ *
|
|
|
+ * @return boolean
|
|
|
+ */
|
|
|
+ public function referenceable($entity);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Create a reference to the local entity on the remote entity, and vice-versa
|
|
|
+ * if needed. Should throw CerException if the reference(s) can't be created
|
|
|
+ * for any reason.
|
|
|
+ *
|
|
|
+ * @param object $entity
|
|
|
+ */
|
|
|
+ public function reference($entity);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Delete all references to the remote entity from the local entity,
|
|
|
+ * and delete reciprocal references from the remote entity.
|
|
|
+ *
|
|
|
+ * @param object $entity.
|
|
|
+ */
|
|
|
+ public function dereference($entity);
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * @class
|
|
|
+ * Base class for CER handlers. All this does is parse the preset
|
|
|
+ * and store instance info about the local and remote fields.
|
|
|
+ */
|
|
|
+abstract class CerHandlerBase {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Local field instance definition.
|
|
|
+ */
|
|
|
+ protected $local;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Remote field instance definition.
|
|
|
+ */
|
|
|
+ protected $remote;
|
|
|
+
|
|
|
+ public function __construct($preset) {
|
|
|
+ $keys = explode('*', $preset);
|
|
|
+
|
|
|
+ if (sizeof($keys) != 6) {
|
|
|
+ throw new CerException(t('Invalid configuration: @preset', array('@preset' => $preset)));
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->local = field_info_instance($keys[0], $keys[2], $keys[1]);
|
|
|
+ if ($this->local) {
|
|
|
+ $this->local['field'] = field_info_field($keys[2]);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ throw new CerException(t('Local field instance does not exist.'));
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->remote = field_info_instance($keys[3], $keys[5], $keys[4]);
|
|
|
+ if ($this->remote) {
|
|
|
+ $this->remote['field'] = field_info_field($keys[5]);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ throw new CerException(t('Remote field instance does not exist.'));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * @class
|
|
|
+ * Generic CER handler with rudimentary language handling.
|
|
|
+ */
|
|
|
+class CerHandler extends CerHandlerBase implements CerHandlerInterface {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The local (home) entity.
|
|
|
+ */
|
|
|
+ protected $entity;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The local entity's ID.
|
|
|
+ */
|
|
|
+ protected $id;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Implements CerHandlerInterface::__construct().
|
|
|
+ */
|
|
|
+ public function __construct($preset, $entity) {
|
|
|
+ parent::__construct($preset);
|
|
|
+
|
|
|
+ // If $entity is of the wrong type, entity_extract_IDs()
|
|
|
+ // will throw EntityMalformedException here.
|
|
|
+ $extract_ids = entity_extract_IDs($this->local['entity_type'], $entity);
|
|
|
+ $this->id = array_shift($extract_ids);
|
|
|
+
|
|
|
+ $this->entity = $entity;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Implements CerHandlerInterface::insert().
|
|
|
+ */
|
|
|
+ public function insert() {
|
|
|
+ foreach ($this->getReferencedEntities() as $referenced_entity) {
|
|
|
+ $this->reference($referenced_entity);
|
|
|
+ _cer_update($this->remote['entity_type'], $referenced_entity);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Implements CerHandlerInterface::update().
|
|
|
+ */
|
|
|
+ public function update() {
|
|
|
+ $original = isset($this->entity->original) ? $this->entity->original : $this->entity;
|
|
|
+
|
|
|
+ $deleted = array_diff($this->getReferenceIDs($original, $this->local), $this->getLocalReferenceIDs());
|
|
|
+ if ($deleted) {
|
|
|
+ $entities = entity_load($this->remote['entity_type'], $deleted);
|
|
|
+ foreach ($entities as $referenced_entity) {
|
|
|
+ $this->dereference($referenced_entity);
|
|
|
+ _cer_update($this->remote['entity_type'], $referenced_entity);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->insert();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Implements CerHandlerInterface::delete().
|
|
|
+ */
|
|
|
+ public function delete() {
|
|
|
+ foreach ($this->getReferencedEntities() as $referenced_entity) {
|
|
|
+ $this->dereference($referenced_entity);
|
|
|
+ _cer_update($this->remote['entity_type'], $referenced_entity);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Implements CerHandlerInterface::references().
|
|
|
+ */
|
|
|
+ public function references($entity) {
|
|
|
+ return in_array($this->getRemoteEntityID($entity), $this->getLocalReferenceIDs());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Implements CerHandlerInterface::referencedBy().
|
|
|
+ */
|
|
|
+ public function referencedBy($entity) {
|
|
|
+ return in_array($this->id, $this->getRemoteReferenceIDs($entity));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Implements CerHandlerInterface::referenceable().
|
|
|
+ */
|
|
|
+ public function referenceable($entity) {
|
|
|
+ $id = $this->getRemoteEntityID($entity);
|
|
|
+
|
|
|
+ $allowed = array(
|
|
|
+ entityreference_get_selection_handler(
|
|
|
+ $this->local['field'],
|
|
|
+ $this->local,
|
|
|
+ $this->local['entity_type'],
|
|
|
+ $this->entity
|
|
|
+ )
|
|
|
+ ->validateReferencableEntities(array($id)),
|
|
|
+ entityreference_get_selection_handler(
|
|
|
+ $this->remote['field'],
|
|
|
+ $this->remote,
|
|
|
+ $this->remote['entity_type'],
|
|
|
+ $entity
|
|
|
+ )
|
|
|
+ ->validateReferencableEntities(array($this->id)),
|
|
|
+ );
|
|
|
+
|
|
|
+ return in_array($id, $allowed[0]) && in_array($this->id, $allowed[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Implements CerHandlerInterface::reference().
|
|
|
+ */
|
|
|
+ public function reference($entity) {
|
|
|
+ if ($this->referenceable($entity)) {
|
|
|
+ try {
|
|
|
+ $this->addReferenceTo($entity);
|
|
|
+ }
|
|
|
+ catch (CerException $e) {
|
|
|
+ // Fail silently
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ $this->addReferenceFrom($entity);
|
|
|
+ }
|
|
|
+ catch (CerException $e) {
|
|
|
+ // Fail silently
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ throw new CerException(t('Cannot create invalid reference to remote entity.'));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Implements CerHandlerInterface::dereference().
|
|
|
+ */
|
|
|
+ public function dereference($entity) {
|
|
|
+ if ($this->references($entity)) {
|
|
|
+ $id = $this->getRemoteEntityID($entity);
|
|
|
+
|
|
|
+ foreach ($this->entity->{$this->local['field_name']} as $language => $references) {
|
|
|
+ foreach ($references as $delta => $reference) {
|
|
|
+ if ($reference['target_id'] == $id) {
|
|
|
+ unset($this->entity->{$this->local['field_name']}[$language][$delta]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($this->referencedBy($entity)) {
|
|
|
+ foreach ($entity->{$this->remote['field_name']} as $language => $references) {
|
|
|
+ foreach ($references as $delta => $reference) {
|
|
|
+ if ($reference['target_id'] == $this->id) {
|
|
|
+ unset($entity->{$this->remote['field_name']}[$language][$delta]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a reference to the local entity on the remote entity. Throws CerException
|
|
|
+ * if the local entity is already referenced by the remote entity, or if the remote
|
|
|
+ * field cannot hold any more values.
|
|
|
+ *
|
|
|
+ * @param object $entity
|
|
|
+ * The remote entity.
|
|
|
+ */
|
|
|
+ protected function addReferenceFrom($entity) {
|
|
|
+ if ($this->referencedBy($entity)) {
|
|
|
+ throw new CerException(t('Cannot create duplicate reference from remote entity.'));
|
|
|
+ }
|
|
|
+ elseif ($this->filled($this->getRemoteReferenceIDs($entity), $this->remote['field'])) {
|
|
|
+ throw new CerException(t('Remote field cannot support any more references.'));
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ $languages = field_available_languages($this->remote['entity_type'], $this->remote['field']);
|
|
|
+ foreach ($languages as $language) {
|
|
|
+ $entity->{$this->remote['field_name']}[$language][] = array('target_id' => $this->id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a reference to the remote entity on the local entity. Throws CerException
|
|
|
+ * if the local entity already references the remote entity, or if the field cannot
|
|
|
+ * hold any more values.
|
|
|
+ *
|
|
|
+ * @param object $entity
|
|
|
+ * The remote entity.
|
|
|
+ */
|
|
|
+ protected function addReferenceTo($entity) {
|
|
|
+ $id = $this->getRemoteEntityID($entity);
|
|
|
+
|
|
|
+ if ($this->references($entity)) {
|
|
|
+ throw new CerException(t('Cannot create duplicate reference to remote entity.'));
|
|
|
+ }
|
|
|
+ elseif ($this->filled($this->getLocalReferenceIDs(), $this->local['field'])) {
|
|
|
+ throw new CerException(t('Local field cannot support any more references.'));
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ $languages = field_available_languages($this->local['entity_type'], $this->local['field']);
|
|
|
+ foreach ($languages as $language) {
|
|
|
+ $this->entity->{$this->local['field_name']}[$language][] = array('target_id' => $id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get the ID of the remote entity. If the entity is of the wrong type,
|
|
|
+ * EntityMalformedException will be thrown.
|
|
|
+ *
|
|
|
+ * @param object $entity
|
|
|
+ * The remote entity.
|
|
|
+ *
|
|
|
+ * @return mixed
|
|
|
+ * The remote entity ID.
|
|
|
+ */
|
|
|
+ protected function getRemoteEntityID($entity) {
|
|
|
+ $extract_ids = entity_extract_IDs($this->remote['entity_type'], $entity);
|
|
|
+ return array_shift($extract_ids);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Gets all the entities referenced by the local entity.
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ * Array of fully loaded referenced entities keyed by ID, or empty
|
|
|
+ * array if nothing has been referenced.
|
|
|
+ */
|
|
|
+ protected function getReferencedEntities() {
|
|
|
+ $IDs = $this->getLocalReferenceIDs();
|
|
|
+ return $IDs ? entity_load($this->remote['entity_type'], $IDs) : array();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Gets the IDs of the entities referenced by the local entity.
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ * Array of entity IDs, empty array if there are no references.
|
|
|
+ */
|
|
|
+ protected function getLocalReferenceIDs() {
|
|
|
+ return $this->getReferenceIDs($this->entity, $this->local);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Gets the IDs of the entities referenced by $entity.
|
|
|
+ *
|
|
|
+ * @param object $entity
|
|
|
+ * The remote entity.
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ * Array of entity IDs, empty array if there are no references.
|
|
|
+ */
|
|
|
+ protected function getRemoteReferenceIDs($entity) {
|
|
|
+ return $this->getReferenceIDs($entity, $this->remote);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Check if a field can support any more values. Formerly known as
|
|
|
+ * "reference overloading".
|
|
|
+ *
|
|
|
+ * @param array $references
|
|
|
+ * The values in the field.
|
|
|
+ *
|
|
|
+ * @param $field
|
|
|
+ * Field definition (i.e., from field_info_field).
|
|
|
+ *
|
|
|
+ * @return boolean
|
|
|
+ */
|
|
|
+ private function filled($references, $field) {
|
|
|
+ return $field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && sizeof($references) >= $field['cardinality'];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Gets all the referenced entity IDs from a specific field on $entity.
|
|
|
+ *
|
|
|
+ * @param object $entity
|
|
|
+ * The entity to scan for references.
|
|
|
+ *
|
|
|
+ * @param array $field
|
|
|
+ * Field or instance definition.
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ * Array of unique IDs, empty if there are no references or the field
|
|
|
+ * does not exist on $entity.
|
|
|
+ */
|
|
|
+ private function getReferenceIDs($entity, $field) {
|
|
|
+ $IDs = array();
|
|
|
+ if (isset($entity->{$field['field_name']})) {
|
|
|
+ foreach ($entity->{$field['field_name']} as $references) {
|
|
|
+ foreach ($references as $reference) {
|
|
|
+ $IDs[] = $reference['target_id'];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return array_unique(array_filter($IDs));
|
|
|
+ }
|
|
|
+
|
|
|
+}
|