terminal-auspicious

Topics

programming

php drupal scheme scheming macros design patterns da la

design

design css

random thoughts

scribbles

alter ego

other me 'em that link us my space me linked in

Collections

Programmable web
PHP design patterns

aspects and honey in php

Submitted by vlado on Wed, 2006-06-14 15:06.drupal | ideas | php | programming

The Drupal way in programming is 60% about hooks 10% about nodeapi and the rest is ingenuity. That's allright, but one fact has always troubled me. And that is - how much effort is spent on discovering which module implements the hook I need. Far enough, the old caching/memoizing trick does the job to ensure that the cpu time is not expensive. Yeah, but still it's outside simplicity might be, just might be implemented in a more elegant fashion.

Drupal hooks implement a nearly Aspect Oriented API. The cross-cut is the point where the hook is invoked. aspect(cross-cut) === hook. The different aspects are declared as moduleName_hook. The disatvantage is that these are indiscriminate. Hooks are always fired. Pros - simplicty, cons - wasted cpu cycles.

I wonder. Is it possible to achieve a similarly simple syntax, but with the added advantage of not firing unnessesary hook calls.

Problem (1st iteration)

We need to call all functions interested in this particular execution point. The current context and the run history should determine which functions are actually interested in this particular point/label/crosscut.

So what if: ..... hook('view','a callback); ..... //in a code piece far away invoke('view', .... );

In invoke() we can have something similar to the current code

function invoke($hook,$args) { ... while($call = next($hooks[$hook])) { $call($args); } ... }

How to implement hook(...)? function hook($hook,$callback) { invoke( $hook, $callback, 'add'); }

We would need an unhook() though, so that the interest is fully managed. function unhook($hook,$callback) { invoke( $hook, $callback, 'remove'); }

And finally the modified invoke: function invoke($hook, $args, $op) { static $hooks; if($op == 'add') { //memory waste, but can do for now $hooks[$hook][$args] = $args; return; } elseif($op == 'remove') { unset($hooks[$hook][$args]); return; } while($call = next($hooks[$hook])) { $call($args); } }

Order please

Partial order that is. It would be handy to be able to have before($hook) and after($hook) calls. This will allow manipulating the sequence of calls, without modifying the grand plan request workflow.

before($hook) adds the callback before the hook point. The order of the other 'before' callbacks for this hook is unknown.

after($hook) does the same, but after the execution of the hook

The arguments of the callbacks added with before and after are the same as for hook for a given cross-cut/hook point

The implementation is similar to hook function before($hook,$callback) { invoke( $hook, $callback, 'add-before'); } function not_before($hook,$callback) { invoke( $hook, $callback, 'remove-before'); } function not_before($hook,$callback) { invoke( $hook, $callback, 'add-after'); } function not_after($hook,$callback) { invoke( $hook, $callback, 'remove-after'); } .... //in invoke, the rest is similar if($op == 'add-before') { unset($hooks[$hook]['before'][$args]); return; } ....

An interesting 'theoretical' consequence is that this is a kind of a higher-order functions based implementation of aspect style programming in an imperative language. That is if you are into such words. It is more interesting thing to notice is that the $var() syntax allow you to code wanted patterns and execute them later on by simply passing new $var to the pattern function. This technique parallells lisp and scheme style macros, you just can't easily manipulate them.

Second iteration or cleaning up the nest

There are a few problems with the above code. It doesn't reflect faithfully the problem, that is the treatment of arguments and results. It is too verbose and can do with some optimisation.

Let's start with the model. The type of an aspect function is ($args→$result). We are not interested in actual shape of either, but their correct treatment. It suffice to say that the only requirement to both is that they are the same for each separate hook. The shape of the aspect computation is (∥($args→$result))→(∥($args→$result))→(∥($args→$result)), where ∥ denotes parallell or unordered behaviour of a bunch of functions. The last remark is important, since we are introducing order or synchronisation with the before and after operations, but that order is only partial, relevant to the bigger picture. In each phase the order of execution should remain unknown. This doesn't break the abstraction boundaries, which is good. This as well means that the functions in each phase must not change $args and must not overwrite the same part of $result. This restriction won't be enforced for both simplicty and performance, it should be tested with unit tests in real life code. Failing to obey this discipline will cause an unexpected behaviour.

Let's change the code then. What is the shape of a typical before function? function a_before($args, &$result) { .... }As it happens the shapes of the at and after functions are the same. We are enforcing a partial order of evaluation and enhancing a function, not changing it's signature.

The evaluator can be written out as looping over each of the different phases .... while(current( $hooks[$hook]['before'] )) { $cb = key( $hooks[$hook]['before'] ); $cb($args); next( $hooks[$hook]['before'] ); } while( current( $hooks[$hook]['during'] ) ) { $cb = key( $hooks[$hook]['during'] ); $cb( $args, $result ); next( $hooks[$hook]['during'] ); } while( current( $hooks[$hook]['after']) ) { $cb = key( $hooks[$hook]['after'] ); $cb( $result ); next( $hooks[$hook]['after'] ); } return $result; ....

This code can be optimised by parametrising the hook selection, add and remove operations, so that we end up with the following short hook invoke function function invoke($hook, &$args, $op = 'run', $phase = '' ) { static $hooks = array(); switch($op) { case 'run': foreach( array('before', 'at' ,'after') as $phase ) { while( current( $hooks[$hook][$phase] )) { $cb = key( $hooks[$hook][$phase] ); $cb( $args, $result ); next( $hooks[$hook][$phase] ); } } return $result; case 'add': $hooks[$hook][$phase][$args] = true; break; case 'remove': unset( $hooks[$hook][$phase][$args] ); break; } }

We can sweeten the syntax by adding explicit hook_before, after, at and the respective unhook functions, to hide the message passing notation of the above function. For example function hook_after( $hook, $cb ) { invoke( $hook, $cb, 'add', 'after'); } function unhook_before( $hook, $cb ) { invoke( $hook, $cb, 'remove', 'before'); }

Adding honey to the pud

The invoke function does too many things, it is unfortunate. It is a natural 'object', purists of any kind don't go for me, that is not a flame bate. We can use php's object oriented features to make this a bit more abstract and enforce separation of concerns, rather than a static and a switch. class hook_namespace { var $hooks; function run( $hook,$args ) { foreach( array('before', 'at' ,'after') as $phase ) { while( current( $this->$hooks[$hook][$phase] )) { $cb = key( $this->$hooks[$hook][$phase] ); $cb( $args, $result ); next( $this->$hooks[$hook][$phase] ); } } return $result; } function hook($hook, $phase, $cb) { $this->$hooks[$hook][$phase][$cb] = true; } function unhook($hook, $phase, $cb) { unset($this->$hooks[$hook][$phase][$cb]); } }

While I could have gone oveboard abstracting further and further, the above class is good enough for my personal taste. It is concise. It does what it says on the box. Ok, fair enough, there are bits to keep in your head, but that is (hopefully) allright. The last two versions practically implement the drupal hook wiring without the broadcast effect for trying out all possible hook definitions. On an abstract level it is a tad more powerful. It has the disadvantage that you need to hook up at runtime, not definition time.

If php had more abstract features we could have done more. With a proper macro facility, we could have shifted the work further towards compile time, now there are extra operations to be performed.

To killes - you are more right than you ever thought. The above code is monadic for all practicall purposes. Of course in haskell it will look very differently, but hey it is a step.

Update I realised that it is better to name the class hook_namespace, since the instance of that class is providing a encapsulation of hook in a single scope, it's a bit verobose, but at least a better name. This has the dubious benefit of renaming add and remove to hook and unhook.

AttachmentSize
aspect.php.txt1.71 KB
read more | vlado's blog | add new comment | 1 attachment

Reply

Please solve the math problem above and type in the result. e.g. for 1+1, type 2
The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <br /> <br> <div> <a> <em> <strong> <cite> <pre> <code> <ul> <ol> <li> <dl> <dt> <dd> <h3> <img> <blockquote> <q> <strike> <small> <h4> <h5> <h6>
  • Link to content with [[some text]], where "some text" is the title of existing content or the title of a new piece of content to create. You can also link text to a different title by using [[link to this title|show this text]]. Link to outside URLs with [[http://www.example.com|some text]], or even [[http://www.example.com]].
  • Lines and paragraphs break automatically.
More information about formatting options
Home » aspects and honey in php

dikini.net

spreading confusion by accident since 1970