Skip to content
data.module 10.6 KiB
Newer Older
<?php
// $Id$
/**
 * @file
 * Hooks and API functions for data module.
 * @todo: Move helper functions into data.inc or other .inc file.
// Constant designating an undefined export state.
// Used in absence of EXPORT_IN_CODE, EXPORT_IN_DATABASE
define('DATA_EXPORT_UNDEFINED', 0);

/**
 * Implementation of hook_views_api().
 */
function data_views_api() {
  return array(
    'api' => '2.0',
    'path' => drupal_get_path('module', 'data'),
/**
 * Implementation of hook_schema_alter().
 * 
 * This is a central piece of data module: 
 * Here we tack schema information that has been defined through the API in data_tables
 * or by hook_data_default onto the $schema array.
 * 
 * We do not use hook_schema() for exposing schema information as this would cause a race
 * condition: ctools/exports looks for data module's data_tables at the same time when
 * we are actually rebuilding it - follow path through 
 * data_get_all_tables() ... _data_load_table() ... ctools_export_load_object().
 */
function data_schema_alter(&$schema) {
  $tables = data_get_all_tables();
  foreach ($tables as $table) {
    $schema[$table->get('name')] = $table->get('table_schema');
  }
}

 * $table = data_create_table('my_table', $schema, 'My table');
 * $table->save($data);
 * @param $name
 *   String that identifies the data table. It is recommended to use 
 *   data_name() to generate a table name in the data namespace. For
 *   example: $table = data_get_tabe(data_name('my_table'));
 * @param $schema
 *   Schema for the table.
 * @param $title
 *   A natural title for the table.
 * @return 
 *   A DataTable object if init could create one,
 *   FALSE if not.
function data_create_table($name, $schema, $title = NULL) {
  if (_data_get_table($name, NULL, NULL, TRUE)) {
    return FALSE;
  return _data_get_table($name, $schema, $title);
 * Get a table if it exists.
 * @param $name
 *   Unique name of the table.
 * @return
 *   A DataTable object if there is a table with this name,
 *   FALSE if not.
function data_get_table($name) {
  if ($table = _data_get_table($name)) {
    return $table;
  }
  return FALSE;
/**
 * Drop a table - use this instead of $table->drop().
 */
function data_drop_table($name) {
  if ($table = data_get_table($name)) {
    $table->drop();
    _data_get_table($name, NULL, NULL, TRUE);
  }
}

/**
 * Load all data tables.
 */
function data_get_all_tables() {
  $tables = array();
  if ($tables = _data_load_table()) {
    foreach ($tables as $table_name => $table) {
      if ($table = data_get_table($table_name)) {
        $tables[$table_name] = $table;
      }
  }
  return $tables;
}

/**
 * Get a list of supported field definitions.
 * 
 * This list is a sub set of Schema API data types 
 * http://drupal.org/node/159605
 * The keys are simplified handles.
 */
function data_get_field_definitions() {
  return array(
    'int' => array(
      'type' => 'int',
      'not null' => FALSE,
    ),
    'unsigned int' => array(
      'type' => 'int',
      'unsigned' => TRUE,
      'not null' => FALSE,
    ),
    'varchar' => array(
      'type' => 'varchar',
      'length' => 255,
      'not null' => FALSE,
    ),
    'text' => array(
      'type' => 'text',
      'not null' => FALSE,
    ),
  );
}

/**
 * Get a definition key into a schema API type definition.
 * If no type can be found, FALSE will be returned.
function data_get_field_definition($key) {
  $definitions = data_get_field_definitions();
  if (isset($definitions[$key])) {
    return $definitions[$key];
/**
 * Get schema API field types supported by Data module.
 */
function data_get_field_types() {
  $definitions = data_get_field_definitions();
  $types = array();
  foreach ($definitions as $def) {
    $types[$def['type']] = $def['type'];
  }
  return $types;
}

/**
 * Get a Schema API PK definition for a given field type.
 */
function data_get_pk_definition($name, $type) {
  if ($type == 'text') {
    return array($name, 255);
  }
  else {
    return $name;
  }
}

/**
 * Get a Schema API index definition for a given field type.
 * @todo: support multiple name/type combinations.
 */
function data_get_index_definition($name, $type) {
  if ($type == 'text') {
    return array(array($name, 255));
  }
  else {
    return array($name);
  }
}

/**
 * Create a table name in the data namespace.
 * @todo: make overridable.
 */
function data_name($table) {
  return 'data_table_'. $table;
}

/**
 * Create a safe name for MySQL field or table names.
 * 
 * @todo: IMPROVE.
 * 
 * - make sure all unsafe characters are removed.
 * - filter magic words.
 * - test pgsql.
 */
function data_safe_name($name) {
  $map = array(
    '.' => '_', 
    ':' => '', 
    '/' => '', 
    '-' => '_', 
    ' ' => '_',
    ',' => '_',
  );
  $simple = trim(strtolower(strip_tags($name)));
  // Limit length to 64 as per http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
  $simple = substr(strtr($simple, $map), 0, 64);

  if (is_numeric($simple)) {
    // We need to escape numerics because Drupal's drupal_write_record() 
    // does not properly escape token MYSQL names.
    $simple = '__num_'. $simple;
  }
  return db_escape_table($simple);
}

/**
 * Helper function to create a natural name.
 * underscored_name -> Underscored name
 */
function data_natural_name($name) {
  return ucfirst(strtolower(str_replace('_', ' ', $name)));
}

/**
 * Helper function to generate a schema.
 * 
 * Example:
 * $table->create(data_build_schema($keys));
 * 
 * @todo: check for table name collisions
 * @todo: add type detection
 * @todo: add meta info handling
 * @todo: add primary key handling
 * @todo: may be add option to add a full fledged schema here?
 */
function data_build_schema($keys) {
  // Build the table definition.
  // Fall back to varchar if no valid type is given.
  $fields = $schema = array();
  foreach ($keys as $k => $key) {
    if ($definition = data_get_field_definition($key)) {
      $fields[data_safe_name($k)] = $definition;
      $fields[data_safe_name($k)] = data_get_field_definition('varchar');
  $schema['fields'] = $fields;
  $schema['indexes'] = array();
  return $schema;
}

/**
 * Build a full schema api field definition.
 * 
 * @param $stub
 *   Array with at least one key 'type'.
 */
function data_build_field_definition($stub) {
  $spec = array();
  $spec['type'] = $stub['type'];
  if ($spec['type'] == 'int') {
    $spec['unsigned'] = empty($stub['unsigned']) ? FALSE : TRUE;
  }
  if ($spec['type'] == 'varchar') {
    $spec['length'] = 255;
  }
  return $spec;
}

/**
 * Helper for building a schema API conform index array.
 */
function data_build_index_array($field_name, $spec) {
  // Default to 255 for now.
  if ($spec['type'] == 'text') {
    return array(array($field_name, 255));
  }
  else {
    return array($field_name);
  }
}

/**
 * Export a data table. This does not export the content of a table - only its schema 
 * and any meta information (title, name, meta...).
 * 
 * @param $name
 *   The name of the table to be exported.
 * 
 * @return 
 *   Exportable code. 
 * 
 * Only available if ctools is installed.
 */
function data_export($name, $indent = '') {
  if (module_exists('ctools')) {
    ctools_include('export');
    $result = ctools_export_load_object('data_tables', 'names', array($name));
    if (isset($result[$name])) {
      return ctools_export_object('data_tables', $result[$name], $indent);
    }
  }
  return t('Export requires CTools http://drupal.org/project/ctools');
}

/**
 * Internal singleton/factory function for creating a single instance of a DataTable class.
 * 
 * Don't use this function directly. Call data_create_table() or data_get_table() instead.
 * 
 * If a schema is given, _data_get_table() creates the table objects DB structure.
 * 
 * The purpose of this function is to make sure that 
 * 
 * a) there is only a single DataTable object for accessing a specific DataTable.
 * b) there is no DataTable object that does not have an existing table.
 */
function _data_get_table($name, $schema = NULL, $title = NULL, $reset = FALSE) {
  _data_include();

  static $tables;
  // Simple way of having a way to override the class being used. 
  // This could be refined with a $type parameter in _data_get_table() and depending
  // functions.
  $class = variable_get('data_table_class', 'DataTable');
  if ($reset) {
    unset($tables[$name]);
  }

  if (!isset($tables[$name])) {
    // Try whether we can load table, then instantiate. Object will then load itself.
    if (_data_load_table($name, $reset)) {
      $tables[$name] = new $class($name);
    }
  }
  if ($schema) {
    $tables[$name] = new $class($name, $schema, $title);
  }
  return isset($tables[$name]) ? $tables[$name] : FALSE;
}

/**
 * Loads data table info from the database. Uses CTools if available.
 */
function _data_load_table($name = NULL, $reset = FALSE) {
  if (module_exists('ctools')) {
    ctools_include('export');
    if (empty($name)) {
      return ctools_export_load_object('data_tables', 'all', array(), $reset);
    }
    else {
      $tables = ctools_export_load_object('data_tables', 'names', array($name), $reset);
      if (isset($tables[$name])) {
        return $tables[$name];
      }
      return FALSE;
    }
  }
  // If CTools is not available, load directly from DB.
  if (empty($name)) {
    $result = db_query('SELECT * FROM {data_tables}');
    $tables = array();
    while ($row = db_fetch_object($result)) {
      foreach (array('table_schema', 'meta') as $key) {
        $row->$key = unserialize($row->$key);
      }
      // No export type.
      $row->export_type = DATA_EXPORT_UNDEFINED;
      $tables[$row->name] = $row;
    }
    return $tables;
  }
  if ($table = db_fetch_object(db_query('SELECT * FROM {data_tables} WHERE name = "%s"', $name))) {
      // No export type.
      $table->export_type = DATA_EXPORT_UNDEFINED;
      $table->table_schema = unserialize($table->table_schema);
      $table->meta = unserialize($table->meta);
      return $table;
  }
  return FALSE;
}

/**
 * Starts overriding a data table by copying it from the default definition into the DB.
 * This function does not have any effect if called on a table that does already exist in
 * data_tables.
 */
function _data_override($name) {
  if (!db_result(db_query('SELECT name FROM {data_tables} WHERE name = "%s"', $name))) {
    if ($table = _data_load_table($name)) {
      drupal_write_record('data_tables', $table);
    }
  }
}

 * Include class file.
function _data_include() {
  static $included;
  if (!$included) {
    include drupal_get_path('module', 'data') .'/data.inc';