From ffea09fb489fe0bdf5a19050012e70018308a4dc Mon Sep 17 00:00:00 2001
From: Sam Boyer <drupal@samboyer.org>
Date: Fri, 18 Jun 2010 15:37:57 -0700
Subject: [PATCH] dbtng integration and entities conversion

- Entities-based approach.
- Initial refactor of the VersioncontrolRepository class to make it properly receive data loaded by the entity controllers via the backend.
- Note about class listings for factory behavior of VersioncontrolBackend
- Update the backends to allow them to alter queries as they are being built by the entity controller.
- Add an optional callback for direct query modification. Note that this potentially screws with caching, so we may not want to retain this approach.
- Initial pseudocode for a VersioncontrolRepository::getLabels() method that utilizes an entity controller stored on the Repository object itself.
---
 includes/VersioncontrolBackend.php          |   45 ++++-
 includes/VersioncontrolLabel.php            |   23 +-
 includes/VersioncontrolRepository.php       |   78 +++----
 includes/classes.inc                        |  328 ++++++++++++++++++++++++++-
 versioncontrol.info                         |    1 +
 versioncontrol.module                       |    1 +
 versioncontrol_fakevcs/includes/classes.inc |   13 +-
 7 files changed, 423 insertions(+), 66 deletions(-)

diff --git includes/VersioncontrolBackend.php includes/VersioncontrolBackend.php
index 136dc2e..87dc88a 100644
--- includes/VersioncontrolBackend.php
+++ includes/VersioncontrolBackend.php
@@ -14,7 +14,7 @@ abstract class VersioncontrolBackend implements ArrayAccess {
   /**
    * The user-visible name of the VCS.
    *
-   * @var    string
+   * @var string
    */
   public $name;
 
@@ -22,7 +22,7 @@ abstract class VersioncontrolBackend implements ArrayAccess {
    * A short description of the backend, if possible not longer than
    * one or two sentences.
    *
-   * @var    string
+   * @var string
    */
   public $description;
 
@@ -33,14 +33,51 @@ abstract class VersioncontrolBackend implements ArrayAccess {
    * of VERSIONCONTROL_CAPABILITY_* values. If no additional capabilities
    * are supported by the backend, this array will be empty.
    *
-   * @var    array
+   * @var array
    */
   public $capabilities;
 
   /**
    * classes which this backend overwrite
    */
-  public $classes;
+  public $classes = array();
+
+  public function __construct() {
+    // Add defaults to $this->classes
+    // FIXME currently all these classes are abstract, so this won't work. Decide
+    // if this should be removed, or if they should be made concrete classes
+    $this->classes += array(
+      'repo'      => 'VersioncontrolRepository',
+      'account'   => 'VersioncontrolAccount',
+      'operation' => 'VersioncontrolOperation',
+      'item'      => 'VersioncontrolItem',
+      'branch'    => 'VersioncontrolBranch',
+      'tag'       => 'VersioncontrolTag',
+    );
+  }
+
+  public function buildObject($type, $data) {
+    $class = $this->classes[$type];
+    if (!is_subclass_of($class, VersioncontrolEntity)) {
+      throw new Exception('Invalid Versioncontrol entity class specified; all entity classes should have VersioncontrolEntity as a parent', $class);
+    }
+    $obj = new $this->classes[$type]($this);
+    $obj->build($data);
+    return $obj;
+  }
+
+  /**
+   * Augment a select query with options specific to this backend.
+   *
+   * This method is fired by entity controllers whenever the backends type of
+   * the entities to be loaded is known prior to the query being issued.
+   *
+   * @param SelectQuery $query
+   *   The query object being built.
+   * @param string $entity_type
+   *   The type of entity being loaded.
+   */
+  public function augmentEntitySelectQuery($query, $entity_type) {}
 
   //ArrayAccess interface implementation
   public function offsetExists($offset) {
diff --git includes/VersioncontrolLabel.php includes/VersioncontrolLabel.php
index 92b2969..f68a044 100644
--- includes/VersioncontrolLabel.php
+++ includes/VersioncontrolLabel.php
@@ -78,24 +78,21 @@ abstract class VersioncontrolLabel implements ArrayAccess {
     if (!empty($this->label_id)) { // already in the database
       return;
     }
-    $result = db_query(
-      "SELECT label_id, repo_id, name, type FROM {versioncontrol_labels}
-    WHERE repo_id = %d AND name = '%s' AND type = %d",
-    $this->repository->repo_id, $this->name, $this->type
-  );
-    while ($row = db_fetch_object($result)) {
-      // Replace / fill in properties that were not in the WHERE condition.
-      $this->label_id = $row->label_id;
-      return;
+    $result = db_result(db_query("SELECT label_id FROM {versioncontrol_labels} WHERE repo_id = %d AND name = '%s' AND type = %d",
+      $this->repository->repo_id, $this->name, $this->type));
+    if ($result) {
+      $this->label_id = $result;
+    }
+    else {
+      // The item doesn't yet exist in the database, so create it.
+      $this->insert();
     }
-    // The item doesn't yet exist in the database, so create it.
-    $this->insert();
   }
 
   /**
    * Insert label to db
    */
-  private function insert() {
+  protected function insert() {
     $this->repo_id = $this->repository->repo_id; // for drupal_write_record() only
 
     if (isset($this->label_id)) {
@@ -104,7 +101,7 @@ abstract class VersioncontrolLabel implements ArrayAccess {
     }
     else {
       // The label does not yet exist, create it.
-      // drupal_write_record() also adds the 'label_id' to the $label array.
+      // drupal_write_record() also assigns the new id to $this->label_id.
       drupal_write_record('versioncontrol_labels', $this);
     }
     unset($this->repo_id);
diff --git includes/VersioncontrolRepository.php includes/VersioncontrolRepository.php
index c81ff55..8840882 100644
--- includes/VersioncontrolRepository.php
+++ includes/VersioncontrolRepository.php
@@ -8,7 +8,7 @@
 /**
  * Contain fundamental information about the repository.
  */
-abstract class VersioncontrolRepository implements ArrayAccess {
+abstract class VersioncontrolRepository extends VersioncontrolEntity implements ArrayAccess {
   // Attributes
   /**
    * db identifier
@@ -51,8 +51,6 @@ abstract class VersioncontrolRepository implements ArrayAccess {
    */
   public $data = array();
 
-  protected $built = FALSE;
-
   // Associations
   /**
    * The backend associated with this repository
@@ -61,39 +59,14 @@ abstract class VersioncontrolRepository implements ArrayAccess {
    */
   public $backend;
 
-  // Operations
   /**
-   * Constructor
+   * An array of VersioncontrolEntityController objects used to spawn more
+   * entities from this repository, if needed. These objects are lazy-
+   * instanciated to avoid unnecessary object creation.
+   *
+   * @var array
    */
-  public function __construct($repo_id, $args = array(), $buildSelf = TRUE) {
-    $this->repo_id = $repo_id;
-    if ($buildSelf) {
-      $this->buildSelf();
-    }
-    else {
-      $this->build($args);
-    }
-    $this->built = TRUE;
-  }
-
-  protected function buildSelf() {
-    $data = db_fetch_array(db_query("
-      SELECT
-      vr.name, vr.root, vr.authorization_method, vr.data
-      FROM {versioncontrol_repositories} vr
-      WHERE vr.repo_id = %d",
-      $this->repo_id));
-    $this->build($data);
-  }
-
-  protected function build($args = array()) {
-    foreach ($args as $prop => $value) {
-      $this->$prop = $value;
-    }
-    if (is_string($this->data)) {
-      $this->data = unserialize($this->data);
-    }
-  }
+  protected $controllers = array();
 
   /**
    * Title callback for repository arrays.
@@ -103,6 +76,32 @@ abstract class VersioncontrolRepository implements ArrayAccess {
   }
 
   /**
+   * Retrieve known branches and/or tags in a repository from the database
+   * as an array of VersioncontrolLabel-descended objects.
+   *
+   * @param array $ids
+   *   An array of label ids. If given, only labels with one of these ids will
+   *   be returned.
+   * @param array $conditions
+   *   An associative array of additional conditions. These will be passed to
+   *   the entity controller and composed into the query. The array should be
+   *   key/value pairs with the field name as key, and desired field value as
+   *   value. The value may also be an array, in which case the IN operator is
+   *   used. For more complex requirements,
+   *   @see VersioncontrolEntityController::buildQuery() .
+   *
+   * @return
+   *   An associative array of label objects, keyed on their
+   */
+  public function getLabels($ids = array(), $conditions = array()) {
+    if (!isset($this->controllers['labels'])) {
+      $this->controllers['labels'] = new VersioncontrolLabelController();
+      $this->controllers['labels']->setBackend($this->backend);
+    }
+    return $this->controllers['labels']->load($ids, $conditions);
+  }
+
+  /**
    * Retrieve known branches and/or tags in a repository as a set of label arrays.
    *
    * @param $constraints
@@ -124,7 +123,9 @@ abstract class VersioncontrolRepository implements ArrayAccess {
    *   If not a single known label in the given repository matches these
    *   constraints, an empty array is returned.
    */
-  public function getLabels($constraints = array()) {
+  public function OLDgetLabels($constraints = array()) {
+    $query = db_select('versioncontrol_labels', 'vcl')
+      ->condition('vcl.repo_id', $this->repo_id);
     $and_constraints = array('repo_id = %d');
     $params = array($this->repo_id);
 
@@ -217,13 +218,6 @@ abstract class VersioncontrolRepository implements ArrayAccess {
   }
 
   /**
-   * Let child backend repo classes add information that _is not_ in
-   * VersioncontrolRepository::data
-   */
-  public function _getRepository() {
-  }
-
-  /**
    * Update a repository in the database, and call the necessary hooks.
    * The 'repo_id' and 'vcs' properties of the repository object must stay
    * the same as the ones given on repository creation,
diff --git includes/classes.inc includes/classes.inc
index d630d68..be4e247 100644
--- includes/classes.inc
+++ includes/classes.inc
@@ -3,9 +3,335 @@
 
 /**
  * @file
- * Needed basic clases that are not entities.
+ * Controller/loader classes. Modelled on the Drupal 7 entity system.
  */
 
+abstract class VersioncontrolEntityController {
+  protected $entityType;
+  protected $entityCache = array();
+  protected $baseTable;
+  protected $idKey;
+  protected $cache = TRUE;
+  protected $backends = array();
+
+  /**
+   * If set, contains an instance of a VersioncontrolBackend object; this object
+   * provides meta-information, as well as acting as a factory that takes data
+   * retrieved by this controller and instanciating entities.
+   *
+   * @var VersioncontrolBackend
+   */
+  protected $backend;
+
+  /**
+   * A mapping of shortened strings used as keys to query building methods they
+   * should call.
+   *
+   * @var array
+   */
+  protected $typeMap = array(
+    'repo'      => 'Repository',
+    'account'   => 'Account',
+    'operation' => 'Operation',
+    'item'      => 'Item',
+    'branch'    => 'Label',
+    'tag'       => 'Label',
+    'label'     => 'Label',
+  );
+
+  public function __construct() {
+    $this->backends = versioncontrol_get_backends();
+  }
+
+  /**
+   * Indicate that this controller can safely restrict itself to a single
+   * backend type. This results in some logic & query optimization.
+   *
+   * @param string $backend
+   */
+  public function setBackend($backend) {
+    if (isset($this->backends[$backend])) {
+      $this->backend = $this->backends[$backend];
+    }
+  }
+
+  public function resetBackend() {
+    $this->backend = NULL;
+  }
+
+  public function resetCache() {
+    $this->entityCache = array();
+  }
+
+  public function load($ids = array(), $conditions = array(), $callback = NULL) {
+    $entities = array();
+
+    // 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($ids) ? array_flip($ids) : FALSE;
+    // Try to load entities from the static cache.
+    if ($this->cache) {
+      $entities += $this->cacheGet($ids, $conditions);
+      // If any entities were loaded, remove them from the ids still to load.
+      if ($passed_ids) {
+        $ids = array_keys(array_diff_key($passed_ids, $entities));
+      }
+    }
+
+    // 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 ($ids === FALSE || $ids || ($conditions && !$passed_ids)) {
+      // Build the query.
+      $query = $this->buildQuery($ids, $conditions, $revision_id);
+      // If a query modification callback was provided, fire it.
+      if (!is_null($callback) && function_exists($callback)) {
+        $callback($query, $ids, $conditions);
+      }
+      $queried_entities = $query
+        ->execute()
+        ->fetchAllAssoc($this->idKey);
+    }
+
+    if (!empty($queried_entities)) {
+      $built_entities = $this->buildEntities($queried_entities);
+      $entities += $built_entities;
+    }
+
+    if ($this->cache) {
+      // Add entities to the cache.
+      if (!empty($built_entities)) {
+        $this->cacheSet($built_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->idKey}] = $entity;
+      }
+      $entities = $passed_ids;
+    }
+
+    return $entities;
+  }
+
+  /**
+   * Build the query to load the entity.
+   *
+   * This has full revision support. For entities requiring special queries,
+   * the class can be extended, and the default query can be constructed by
+   * calling parent::buildQuery(). This is usually necessary when the object
+   * being loaded needs to be augmented with additional data from another
+   * table, such as loading node type into comments or vocabulary machine name
+   * into terms, however it can also support $conditions on different tables.
+   * See CommentController::buildQuery() or TaxonomyTermController::buildQuery()
+   * for examples.
+   *
+   * @return SelectQuery
+   *   A SelectQuery object for loading the entity.
+   */
+  protected function buildQuery($ids, $conditions = array()) {
+    $query = db_select($this->baseTable, 'base');
+
+    $query->addTag($this->entityType . '_load_multiple');
+
+    // Add fields from the {entity} table.
+    $entity_fields = drupal_schema_fields_sql($this->baseTable);
+
+    $query->fields('base', $entity_fields);
+
+    if ($ids) {
+      $query->condition("base.{$this->idKey}", $ids, 'IN');
+    }
+    if ($conditions) {
+      foreach ($conditions as $field => $value) {
+        // If a condition value uses this special structure, we know the
+        // requestor wants to do a complex condition with operator control.
+        if (is_array($value) && isset($value['values']) && isset($value['operator'])) {
+          $query->condition('base.' . $field, $value['values'], $value['operator']);
+        }
+        // Otherwise, we just pass the value straight in.
+        else {
+          $query->condition('base.' . $field, $value);
+        }
+      }
+    }
+    // Allow the current backend to augment the query as needed.
+    $this->backend->augmentEntitySelectQuery($query, $this->entityType);
+    return $query;
+  }
+
+  protected function queryAlterGetBackendType($query) {
+    if (!isset($this->backend)) {
+      // Add a join to the repo table so we know which backend to use.
+      $query->join('versioncontrol_repositories', 'vcr', "vcr.repo_id = base.repo_id");
+      $query->addField('vcr', 'vcs');
+    }
+  }
+
+  /**
+   * Transform the queried data into the appropriate object types.
+   *
+   * Empty here because each entity type needs to specify their process.
+   *
+   * @param array $queried_entities
+   */
+  protected function buildEntities(&$queried_entities) {
+    $built = array();
+    foreach ($queried_entities as $id => $entity) {
+      $built[$id] = $this->backends[$entity->vcs]->buildObject($this->entityType, $entity);
+    }
+    return $built;
+  }
+
+  /**
+   * Get entities from the static cache.
+   *
+   * @param $ids
+   *   If not empty, return entities that match these IDs.
+   * @param $conditions
+   *   If set, return entities that match all of these conditions.
+   */
+  protected function cacheGet($ids, $conditions = array()) {
+    $entities = array();
+    // Load any available entities from the internal cache.
+    if (!empty($this->entityCache)) {
+      if ($ids) {
+        $entities += array_intersect_key($this->entityCache, array_flip($ids));
+      }
+      // If loading entities only by conditions, fetch all available entities
+      // from the cache. Entities which don't match are removed later.
+      elseif ($conditions) {
+        $entities = $this->entityCache;
+      }
+    }
+
+    // Exclude any entities loaded from cache if they don't match $conditions.
+    // This ensures the same behavior whether loading from memory or database.
+    if ($conditions) {
+      foreach ($entities as $entity) {
+        // FIXME this probably needs to be more complex for our purposes
+        $entity_values = (array) $entity;
+        if (array_diff_assoc($conditions, $entity_values)) {
+          unset($entities[$entity->{$this->idKey}]);
+        }
+      }
+    }
+    return $entities;
+  }
+
+  /**
+   * Store entities in the static entity cache.
+   */
+  protected function cacheSet($entities) {
+    $this->entityCache += $entities;
+  }
+}
+
+class VersioncontrolRepositoryController extends VersioncontrolEntityController {
+  protected $entityType = 'repo';
+  protected $baseTable = 'versioncontrol_repositories';
+  protected $idKey = 'repo_id';
+}
+
+class VersioncontrolAccountController extends VersioncontrolEntityController {
+  protected $entityType = 'account';
+  protected $baseTable = 'versioncontrol_accounts';
+  protected $idKey = 'repo_id'; // FIXME woah fugly. A lot needs to be reworked b/c it's got two primary keys
+
+  protected function buildQuery($ids, $conditions = array()) {
+    $query = parent::buildQuery($ids, $conditions);
+    $this->buildQueryAttachBackend($query);
+    return $query;
+  }
+}
+
+class VersioncontrolLabelController extends VersioncontrolEntityController {
+  protected $entityType = 'label';
+  protected $baseTable = 'versioncontrol_labels';
+  protected $idKey = 'label_id';
+
+  protected function buildQuery($ids, $conditions = array()) {
+    $query = parent::buildQuery($ids, $conditions);
+    $this->buildQueryAttachBackend($query);
+    return $query;
+  }
+}
+
+class VersioncontrolOperationController extends VersioncontrolEntityController {
+  protected $entityType = 'operation';
+  protected $baseTable = 'versioncontrol_operations';
+  protected $idKey = 'vc_op_id';
+
+  protected function buildQuery($ids, $conditions = array()) {
+    $query = parent::buildQuery($ids, $conditions);
+    $this->buildQueryAttachBackend($query);
+    return $query;
+  }
+}
+
+class VersioncontrolItemController extends VersioncontrolEntityController {
+  protected $entityType = 'item';
+  protected $baseTable = 'versioncontrol_items';
+  protected $idKey = 'item_revision_id';
+
+  protected function buildQuery($ids, $conditions = array()) {
+    $query = parent::buildQuery($ids, $conditions);
+    $this->buildQueryAttachBackend($query);
+    return $query;
+  }
+}
+
+/**
+ * Abstract parent class for all the various entity classes utilized by VC API.
+ *
+ * Basically just defines shared CRUD/loader-type behavior.
+ */
+abstract class VersioncontrolEntity {
+  protected $built = FALSE;
+
+  /**
+   * An instance of the Backend factory used to create this object, passed in
+   * to the constructor. If this entity needs to spawn more entities, then it
+   * should reuse this backend object to do so.
+   *
+   * @var VersioncontrolBackend
+   */
+  protected $backend;
+
+  public function __construct(VersioncontrolBackend $backend) {
+    $this->backend = $backend;
+  }
+
+  /**
+   * Pseudo-constructor method; call this method with an associative array of
+   * properties to be assigned to this object.
+   *
+   * @param array $args
+   */
+  public function build($args = array()) {
+    // If this object has already been built, bail out.
+    if ($this->built == TRUE) {
+      return FALSE;
+    }
+
+    foreach ($args as $prop => $value) {
+      $this->$prop = $value;
+    }
+    if (is_string($this->data)) {
+      $this->data = unserialize($this->data);
+    }
+    $this->built = TRUE;
+  }
+}
+
 /**
  * Repository loader, singleton class.
  */
diff --git versioncontrol.info versioncontrol.info
index a0b3f0f..cf817ae 100644
--- versioncontrol.info
+++ versioncontrol.info
@@ -2,6 +2,7 @@
 name = "Version Control API"
 description = "An interface to version control systems whose functionality is provided by pluggable back-end modules."
 dependencies[] = autoload
+dependencies[] = dbtng
 package = Version Control
 core = 6.x
 php = 5.2
diff --git versioncontrol.module versioncontrol.module
index 6230e3b..57c5486 100644
--- versioncontrol.module
+++ versioncontrol.module
@@ -143,6 +143,7 @@ function versioncontrol_autoload_info() {
   // Add mushed together classes.inc
   $classes = array(
     'VersioncontrolItemParallelItems',
+    'VersioncontrolEntity',
     'VersioncontrolRepositoryCache',
     'VersioncontrolAccountCache',
     'VersioncontrolOperationCache',
diff --git versioncontrol_fakevcs/includes/classes.inc versioncontrol_fakevcs/includes/classes.inc
index 89f8803..8a2d280 100644
--- versioncontrol_fakevcs/includes/classes.inc
+++ versioncontrol_fakevcs/includes/classes.inc
@@ -3,6 +3,13 @@
 
 class VersioncontrolFakeBackend extends VersioncontrolBackend {
 
+  public $classes = array(
+    'repo'      => 'VersioncontrolFakeRepository',
+    'account'   => 'VersioncontrolFakeAccount',
+    'operation' => 'VersioncontrolFakeOperation',
+    'item'      => 'VersioncontrolFakeItem',
+  );
+
   public function __construct() {
     $this->name = 'FakeVCS';
     $this->description = t('FakeVCS is a version control system that is specifically capable in doing everything that any other version control system might ever do.');
@@ -24,12 +31,6 @@ class VersioncontrolFakeBackend extends VersioncontrolBackend {
         // but also to directories.
         VERSIONCONTROL_CAPABILITY_DIRECTORY_REVISIONS,
     );
-    $this->classes = array(
-      'repo'      => 'VersioncontrolFakeRepository',
-      'account'   => 'VersioncontrolFakeAccount',
-      'operation' => 'VersioncontrolFakeOperation',
-      'item'      => 'VersioncontrolFakeItem',
-    );
   }
 
 }
-- 
1.7.1

