2

I have a multi-site setup with some shared tables using db_prefix. The variables table is NOT shared. Sharing ALL variables would cause lot of trouble, but sharing only some selected variables wont.

The picture below visualizes my problem: Shared download method

The entries in the green box are shared and available on all multi-sites. Whereas the selected entry information is part of the config and stored in variables. Changes in the red box are not shared and lead to inconsistency when changed on one multi-site.

At the moment I use "drush @sites variable-set ..." to make consistent changes on all multi-sites. But to keep it more simple, I want to integrate it into Drupal's backend.

I found out that there was a failed patch request already. See issue here. The patch modifies variable_get() and variable_set() with an extended query-statement, if a defined variable is set in an array. I am aware of the fact, that the patch is for drupal 5, but it could be easily adapted to drupal 7.

The workaround in the patch modifies the core, but i cannot touch it. Overwriting variable_get() functionality is quite an easy job. You just have to hook_boot() or hook_init() and replace the desired values in $conf array with your custom values.

Whats the deal?

I need to extend variable_set() and variable_del() functionality without touching core. Any ideas how i get into that?

1

As pointed out by Mołot trying to get variable_set to write into a different database would be complicated - it may be achievable by implementing your own database adapter, but there are simpler alternatives.

After some thoughts I realised that while Drupal did not offer a way to hook into the changes made by variable_set, PHP itself did.

This relies on the fact variable_get, variable_set and variable_del all use a global variable (on top of the database) to store the variable values - $conf. Whenever a value is read, added, changed or removed from the variables, the global $conf is modified accordingly. A solution would be to replace the $conf global variable by a custom object that implements ArrayAccess. This way you could monitor every time $conf is accessed or modified. Here is an example:

class MyGlobalConf implements ArrayAccess{
  private $conf;

  function __construct($conf = array()) {
    $this->conf = $conf;
    $this->loadMySharedVariables();
  }
  public function offsetExists($offset) {
    return isset($this->conf[$offset]);
  }
  public function offsetGet($offset) {
    return $this->conf[$offset];
  }
  public function offetSet($offset, $value) {
    if ($this->isASharedVariable($offset)) {
      $this->saveSharedVariable($offset, $value);
    }
    $this->conf[$offset] = $value;
  }
  public function offsetUnset($offset) {
    if ($this->isASharedVariable($offset)) {
      $this->unsetSharedVariable($offset);
    }
    unset($this->conf[$offset]);
  }
  private function loadMySharedVariables() {
    // Populate $this->conf with the shared variables
  }
  private function isASharedVariable($offset){
    // Return TRUE if $offset is a shared variable
  }
  private function saveSharedVariable($offset, $value) {
    // Save the shared variable
  }
  private function unsetSharedVariable($offset, $value) {
    // Unset the shared variable
  }

}

Then in settings.php you could replace $conf with your own:

$conf = new MyGlobalConf($conf);

While I like the creativity of this approach, I fear the performance impact would be noticeable. So if I was to implement a solution to this problem, I would probably try to synchronize the variables rather than share them.

You could do this, for instance, by implementing hook_exit :

function mymodule_exit($destination = NULL) {
  global $conf;
  // The variables to share
  $shared_vars = array('shared_var_1', 'shared_var_2');
  // The table prefix for each of the site we want to share with.
  $all_prefix = array('myprefix_1', 'myprefix_2');
  // Synchronize...
  foreach ($shared_vars as $var) {
    foreach ($all_prefix as $prefix) {
      $table_name = $prefix . "variable";
      db_query("
        UPDATE $table_name
        SET value = :value
        WHERE name = :name
      ", array(
        ':value' => serialize($conf[$var]),
        ':name' => $var
      ));
    }
  }
}

This should probably be improved:

  • If only logged in users could cause variables to be changed, then add a test to not do anything for anonymous users:

    global $user; if ($user->uid == 0) {return;}

  • Cache the shared variables in hook_boot, so that you only update them when they change.

  • Good work. I like the creativity of the ArrayAccess approach too, but i chose the hook_exit() way! Still working on it, but its very promising! – larrydahooster Jun 20 '13 at 15:01
  • I'd like to mention, that hook_exit() is not called immediately after form submits (POST Request). Instead you are redirected via drupal_goto() to enforce a GET Request. Here is the flow: hook_boot(); [Missing hook_exit();] hook_boot(); hook_exit(); So the hook_exit() is missing, excactly where it is required to write the shared variables to database. But you can use hook_drupal_goto_alter() to compensate the missing hook_exit() call and it is done. – larrydahooster Jun 27 '13 at 8:20
1

Reading is possible, but may be costly

Code for variable_get() is:

function variable_get($name, $default = NULL) {
  global $conf;
  return isset($conf[$name]) ? $conf[$name] : $default;
}

So you need to set:

global $conf;
$conf['shared_variable'] = your_function_to_obtain('shared_variable');

Where to put it?

If your function uses other site variables, including ones provided via UI, best place seems to be a hook_boot(). You need to make sure your hook_boot() is called first. To get your module to run first, use hook_module_implements_alter() or easier and less reliable way - Modules weight module.

If you don't use any values provided via UI, you can (maybe should) put it in settings.php instead. You don't need to use global in settings.php.

Writing is way more trickier

Code for variable set() is:

function variable_set($name, $value) {
  global $conf;

  db_merge('variable')->key(array('name' => $name))->fields(array('value' => serialize($value)))->execute();

  cache_clear_all('variables', 'cache_bootstrap');

  $conf[$name] = $value;
}

As you can see, there are no hook calls, nothing. It just saves data altogether. variable_del() shares that problem.

What you can do is to override very forms, providing your own save handler that will copy data to shared table. This will not solve the issue of saves and deletes outside form submits, if they happen in module in question. Other approach would be to use triggers directly in RDBMS to synchronize tables - more reliable, but harder to maintain.

Advanced PHP debugger

A bit hardcore approach is to override built-in functions. Yes, you can do it without touching core.

You need PECL apd >= 0.2

In your settings.php (or in your module's *.inc, but include in settings.php) implement override_function($function_name, $function_args, $function_code) for variable_del and variable_set. Do not attempt this for reads, unless you want huge performance hit.

  • Thanks for your answer, i really appreciate it and was almost sure, that you answer to my question. However, Alice Heaton got that point! ;) – larrydahooster Jun 20 '13 at 15:04
  • hook_module_implements_alter() has no effect on hook_boot(), due to the fact that it is called before modules or most include files are loaded into memory. Does Module weight has effect on it? Problem of Module weight is that I need to share the weight information as well between all multi-sites, but it is stored in system table ;) This would create a new working item ;) – larrydahooster Jun 20 '13 at 16:02
  • @larrydahooster module weight alter the weight values in table. I'm almost sure it works. Anyway, index.php is faster and safer way if you can afford to use it. – Mołot Jun 21 '13 at 6:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.