This is a 3rd proposed solution for handlers/call routing for plugins in core. It goes along with #362747: Create a call routing system (fix broken dynamic includes) and #363787: Plugins: Swappable subsystems for core.

Here is an overview of what's going on...

Concept

This solution is similar to #363787: Plugins: Swappable subsystems for core and uses the same naming conventions. The idea is to return objects for pluggable systems such as caching, sessions, and others.

There are 2 hooks modules can implement which are hook_handler_info and hook_slot_info. hook_slot_info defines the pluggable systems such as cache. hook_handler_info defines the possible pluggable systems to use in a slot.

The 3 functions used in this system are handler_set, handler_get, and handler. handler_set sets the configruation of the handler to use for a slot. handler_get retrieves the handler configuration information for a slot. The handler function returns a handler object for the pluggable system. For example:

  $cache = handler('cache');
  $data = $cache->get($id, 'cache');
  $data = handler('cache')->get($id, 'cache');

Configuration Storing

Currently the configuration for handlers is stored in the variables array so it can be defined in settings.php via the $conf array. handler_get and handler_set manage the data stored in the handlers variable.

This means that the cache and sessions systems can use this before the database is available.

I'm also considering moving the configuration from $conf to $handlers in the settings.php file, storing the configuration in a table called handlers, and caching/loading that data in a manner similar to the variable system does with variable_init. This would cut down the clutter in the variables table and still cache the configuration. Moving to this would add one cache call to most page loads.

Multi-routing

Crell wanted to do multi-routing so a call to a system could be routed to multiple plugins. With some modification that can be done here without a much extra code.

Objects vs. Functional

I implement this as a class/object based system because many of the pluggable patches currently existing against head are class based. While chx patch #362747: Create a call routing system (fix broken dynamic includes) can be used to return objects it's not intuitive. factory functions that return objects for uses like this is a common practice I've seen. I think, though I could be wrong, it would be easier from a DX point of view for developers.

This first patch is rough and a concept patch. It's quite large because the cache system has been updated to use this patch which moved it from a set of functions to a class.

Comments

chx’s picture

Assigned: Unassigned » chx

There is a lot to like here. However, the single line functions (like cache_get) need to distill target information from parameters, which for cache is going to be $table, but for fields it'll be $field->type. This approach will let the casual Drupal developer need not to bother with OOP but those who actually provide the handlers will get an interface (yes we need interfaces) and that's good. The system is simple enough. I will work on this (a lot) today.

mfer’s picture

An example of hook_handler_info and hook_slot_info:

/**
 * Implementation of hook_slot_info().
 */
function system_slot_info() {
  return array(
    'cache' => array(
      'description' => t('Drupals caching system.'),
      'interface' => 'cacheInterface',
      'default_handler' => 'drupalCache',
    ),
  );
}

/**
 * Implementation of hook_handler_info().
 */
function system_handler_info() {
  return array(
    'cache' => array(
      'drupalCache' => array(
        'description' => t('Drupals default database cache.'),
        'class' => 'drupalCache',
        'file' => 'includes/cache.inc',
      ),
    ),
  );
}
chx’s picture

Status: Active » Needs review
StatusFileSize
new25.61 KB

This is indeed a simple system, it's like 30 lines. A little in bootstrap.inc for handler which determines the necessary class and instantiates it, loading a file as necessary, a little in common.inc for the builder and a little in system.module defining the 'cache' slot. 'cache' conversion is for demonstration. The rest can be in followup patches.

Status: Needs review » Needs work

The last submitted patch failed testing.

chx’s picture

Status: Needs work » Needs review
StatusFileSize
new25.37 KB

Fixed default.settings.php which has the nice effect of making the thing working :) also fixed a bit in the API docs.

chx’s picture

StatusFileSize
new26.69 KB

This version is much closer to mfer's and has a set/get too! hook_slot_info now only provides info. I have called the other hook_handler though. If we want , we could store only the class not an array('class' => $class) for further memory savings.

mfer’s picture

Status: Needs review » Needs work

One huge thing is missing for this patch... tests. If no one gets to them before tomorrow I'll spend some time on that.

The comments to the interface drupalCacheInterface need to be updated. They look like straight copy/paste from cache_get/set/clear_all and refer to those function names.

In the return array from handler_get should it be 'defaults' or 'default'? The plural on defaults might be confusing.

We have crud functions for handler_set/get, what about handler_del to delete a configured handler. For example, handler_set is used to set apc for cache_filter. Now you want to go back to the default configuration. Shouldn't you be able to do a handler_del for a slot/target? Or, is there already a good way to do this I'm just not seeing?

chx’s picture

Status: Needs work » Needs review
StatusFileSize
new26.74 KB

Addressed mfer's concerns.

chx’s picture

StatusFileSize
new26.96 KB

Posted the wrong patch, handler_del was left out.

mfer’s picture

This looks good besides the missing tests. I'll work on those tomorrow.

neclimdul’s picture

I didn't realize this got split into another issue. So, this is kinda an updated post from the other issue because at this point this is patch is getting closer to #363787: Plugins: Swappable subsystems for core. I still have some serious concerns about storing the $handler information in the variable system. We could potentially be storing a good bit of information in this array. Handlers seem to be the sort of thing that could be used in all sorts of contrib modules. Imagine if Views, CCK and ubercart or ecommerce end up using them and you have a site using all of those handlers... on top of the core handlers... that could be a fairly large variable.

Also, I think we are also losing out by not passing the target or any other contextual information to the handler constructor. For example, if the target for the is the cache table then why would you also pass it to the functions?

handler('cache', $table)->get($cid, $table);

vs

handler('cache', $table)->get($cid);
chx’s picture

StatusFileSize
new27.56 KB

I somewhat doubt that this array will become large. These handlers are used when there are changeable implementations of one given functionality. That should be quite rare outside of core and even core has only a few (cache, session, path, mail, http, password and come Tuesday, field storage). And most of these core ones (at least cache, session, field storage and path if aliases are used) are needed on every page request anyways.

And we pass around $table because there is no point in having a separate object for every target. That said, we currently do store a separate object for every target, so here is a patch which does not do that, which, I guess, saves memory in the default case compared to the previous patch which held a separate cache object for every target. It's still mandatory to pass in $table to every caching method because in the default case you wont get a target and in such a case one object needs to be able to handle everything. Note that for example field storage will pass around $field but the $target will be $field->type so it's just coincidence that for cache an argument and target is the same. I can imagine though some handler modules (esp for field storage) which are able to handle only specific targets and those can throw an exception in their constructor now because the constructor gets the target for specific targets and nothing for the default case.

neclimdul’s picture

Trying not to push this patch into killing a horde of kittens here but really, in the context of handlers, the goal of the clearing method should just be to clear. If you want to "clear all" (which only really clears 2 caches...) then IMO it should be a function that calls 2 handler calls. Its a special case, and there shouldn't be anything keeping the system from storing those 2 caches in different handlers. It seems like the goal of the handler methods should encompass very specific functionality only, not special cases.

I'm also not convinced that passing contextual information to the constructor isn't the right way and keeping individual objects isn't the right way, or even a way that will be required for some implementations. I have to concede that it could eat a lot of memory though.

damien tournoud’s picture

I see no real difference with Crell's approach.

What I said in the other issue still stand:

  • We actually don't need hook_handler() because we could extend the registry to store the information "this class implements the following interfaces"
  • I don't believe we need a common factory function (ie. the handler()) function, because the different subsystem will have different needs:
drupal_cache($table, $cache_id)->get() // I should be able to choose a cache implementation by (table, cid), not only by table...
drupal_mail($recipient)->send($message) // I should be able to choose a cache implementation by recipient (for example by destination domain), for this I may want to implement a custom routing function, as in the call routing patch.
drupal_http($base_url)->get($path) // If we want to take advantage of HTTP/1.1 connection persistence, we need to route by $base_url, in order to reuse previous connexions to this server
drupal_user_authenticate($user)->authenticate($password) // I may have different user class (ie. LDAP vs. local users), with complex routing needs (ex. all users are authenticated with LDAP, except user 1)

I like this as a pattern, but I believe we don't actually need the common implementation. Let's agree on the general "handlers pattern" (ie. a strategy pattern based on interfaces, with a plain factory function specific to each subsystem), and convert each subsystem one by one, based on their specific needs.

neclimdul’s picture

@Damien I agree. I believe that is why Crell's patch doesn't actually include an implementation.

No really related to this issue but I agree with Crell that you're mistaken about the cid being part of the target. I think we can agree to discuss that on a different issue though.

neclimdul’s picture

I should clarify, I agree we shouldn't be arguing final implementation.

However there are several things that are quite different in these two patches. Most drastic is the storage of information and how the handlers interact with the system. And partially because of the later point and because of some design decisions , the basic implementation is somewhat important.

chx’s picture

We need hook_handler() because we want to make it possible to write an UI to pick between handlers in a followup patch. We are fine with handler() because the wrapper functions will contain the logic to find out a useful target for the given slot. Right now the logic is target == table but for fields we will use handler('field_storage', $field->type), for drupal_http_request we will use handler('http', $base_url) and so on. We are not exposing handler() itself to end users. Maybe we should rename to _handler?

Further cleaning of caching is a followup issue.

mfer’s picture

Status: Needs review » Needs work

Tests seem to find problems and writing the tests found a problem using the variable system. If you look at variable_init you can see that after the variables are loaded from either cache or the variables table it does:

foreach ($conf as $name => $value) {
  $variables[$name] = $value;
}

This overwrites the variable stored in the database with the key => value from the $conf array in the settings.php file. So, by configuring the cache in here (or anything else) which is required for it to work we overwrite the settings from the database.

Working on a solution...

mfer’s picture

Status: Needs work » Needs review
StatusFileSize
new31.24 KB

Here's an updated patch with tests and a number of fixes/changes.

Configuring the handlers via $conf['handlers'] for handlers available when the database is offline has an issue outlined in #18. To fix this I added a new variable called $handlers to the default.settings.php file to hold the handler settings. It holds the same information $conf['handlers'] used to. I added a function called handler_init which runs after variable_init and pulls $conf['handlers'] into $handlers.

There are now tests for handlers, multiple handlers, and the CRUD functions to configure the handlers.

There are a number of other changes such as the CRUD functions and handler() no longer locally caching the information in $handlers instead just grabbing it from the global scope. This fixed an issue with stale data.

Still missing is some documentation for the default.settings.php file to explain handlers.

mfer’s picture

To respond to some of the comments about the approach:

  • clear_all in the cache system may not be appropriate. But, that's not in the scope of this patch. clear_all is an existing naming convention on the cache system.
  • Does someone have a use case where we shouldn't cache a pluggable system? If so, should it be the slot that defines if it's not cachable or the handler? My initial patch had a provision to choose if something was cached. We didn't have a use case so we dropped it.
  • This is similar to crells approach but different. It's much simpler. There's no enviornment object. In crells setup handler() calls a facotry function to create the objects. In this case handler() would be used in a different factory or just a plain old function. It could be used to hide the OOP implementation.
  • We can't rely on the registry. Some of the functionality needs to be available when the registry isn't.
  • I agree we need a pattern but we need more than that. With this we have a common factory that can be used in a number of places. We have a CRUD api for storing configuration. This will be useful when a UI is built to configure these.

Status: Needs review » Needs work

The last submitted patch failed testing.

mfer’s picture

Status: Needs work » Closed (won't fix)

See #363787: Plugins: Swappable subsystems for core. Closing this one up. Crells difference in approach solves some of the problems we have here more cleanly and is now similar to this.