Saturday, September 4, 2010

Using helpers to emulate multiple inheritance in PHP

There's no multiple inheritance of classes in PHP. We all can be happy with respect to some problems, but there is others arising from that:

What we want to do is this: shared methods should be shared and not duplicated. So an action AbstractArticleAction and another one, AbstractBlogAction, both inherit from an AbstractPageAction and maybe add custom methods. And there is more special actions like ArticleDetailAction and BlogDetailAction which both do things like stripForbiddenHtmlFromText(). And here is our problem: Where should we put our shared stripForbiddenHtmlFromText method? The only class our DetailActions both inherit is the AbstractPageAction - but there's many other actions who do inherit from this one, and our method is only needed in the two special cases presented.

Now there are different approaches to a solution here:
  1. Create a TextAction so that we gain an inheritance tree like this one:
    BlogDetailAction extends AbstractBlogAction extends TextAction extends AbstractPageAction
    This won't solve any problem, since tomorrow we will need a MonetarizeAction which should be used only by articles and not by blog posts.
  2. Create Action Helpers and import/inject them into your actions when needed (plugins):
    class AbstractArticleAction extends AbstractPageAction {
        function doExecute(ourRequest $request) {
           $this->loadHelpers(array(TextActionHelper,MonetarizeActionHelper));
        }
       function __call($method, $args) {
         return $this->{helpersMethods[$method]}($args);
       }
    }
    Then the helper methods are magically added as methods to AbstractArticleAction.
    This solves our problem, but also creates new ones I'll describe later.
  3. Make all action helper methods public static and call them statically from within your actions:
    class AbstractArticleAction extends AbstractPageAction {
        function doExecute(ourRequest $request) {
           $this->article = TextActionHelper::stripForbiddenHtml(
                               $this->article,
                               $allowedHtml
                            );
        }
    }
    Erm. Let's go back to the procedural paradigm, okay? ... We don't want to do this.
  4. Work with dependecy injection and inject all helpers as services:
    class AbstractArticleAction extends AbstractPageAction {
        function doExecute(ourRequest $request) {
           $this->article = $this->_TextHelperService->stripForbiddenHtml(
                                $this->article,
                                $allowedHtml
                            );
        }
    }
    Not so nice, and leads to similar problems like 2. And: this is not quiet what a service is thought of be doing. And: you'd have public function injectStuff($stuffToInject) methods all over your actions.
  5. Use Singletons. No.
We decided for the second solution and built view helpers and action helpers which are imported when needed. As already mentioned this does solve the problem and gives our BlogDetailAction all the methods it needs while leaving everything else untouched. Also code duplication is avoided. The AbstractPageAction has a method called load() which will import an action helper. AbstractBlogAction and AbstractNewsAction both do import the text helpers.

But there's also some problems with this solution:
  • Magic: neither from within the class, nor from it's parents you can directly see where a method comes from. So doesn't your IDE of choice. Forget about clickable method names, code completion and easy access to understanding. On the other hand: knowing about the action helper system might let you grasp from the method name, that It'll quiet likely be found in an action helper.
  • Naming conflicts: This one is classical about multiple inheritance. What happens if two or more of the view helper methods have the same name? This wouldn't be a problem within the two different helper classes, but would become one when an action imports both of them. Throwing an ActionHelperMethodNameConflictException does solve this during runtime. But one can easily imagine, that there are 12 action helper classes which are used for development for about one year (method names spread all over the project) and after the year we first stumble across a conflict because we before never had a case where two the conflicting helper classes were both imported by an action. Now we have to rename one of the methods...
  • Statics: To avoid loading helpers all over in every action that inherits from f.a. AbstractBlogAction one has to make helpers a static property. I am still unsure if this is a good idea.
  • Function arguments:Since arguments are passed to the helpers via func_get_args() in the __call() method, the helpers are really hard to handle, iff they have multiple arguments, which are predefined [f.e. cropText($text, $length = 250)] and get passed the action as third argument like this:

    __call ($method, $args) {
          return $this->{helpermethods[$method]}(array_merge($args,$this));
    }
    This is, because  the helper method would, if we omit the second argument ($length), have to decide, if $length is the length or the action object.
Note, that the same problems and solution strategies apply for views and view helpers. Zend also choosed the 2nd strategy with respect to Zend view helpers.

    0 comments:

    Post a Comment