SitePoint Sponsor

User Tag List

Results 1 to 22 of 22
  1. #1
    Where's my title at? dreaz's Avatar
    Join Date
    Apr 2004
    Posts
    144
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    My bare bones approach to MVC

    Let me start be clearing up a few things:

    • I know MVC has been discussed many, many times here, but most approaches to it I've seen are way too bloated with stuff I won't use, or they simply don't tie up as it should (IMHO). I'll probably be using Zend when it's stable, so I'm doing this mostly for the sake of learning it.
    • I'm a single developer and I use PHP for simple things. I don't need my code to be highly flexible, therefore I'm not using class abstraction or interfaces. All I need is a simple, yet powerful way to develop simple applications faster. That's it.
    • Most of this code is based on Zend and Code Igniter, so similarities are not coincidences.


    As the title says, this is just a bare bones approach to the MVC pattern. I haven't given the view much thought (I wont use any template system, just php syntax) and the model should be kind of standalone, so it's pretty much only the controller.
    I divided the controller in 4, very simple components: Front Controller => Router => Dispatcher => Page Controller. Those should be able to handle the main model-view-controller division and allow me to have a decent and easy URL routing system.

    I know there's not much to be reviewed here, but I want to know what is your opinion on this approach (as I'm stuck on the MVC paradigm for quite a few weeks now), and I'm really hoping to get some suggestions to improve it.
    Enough talk, here's the zipped code with a working example (edit paths on config first) and bellow is the most important code snippets:

    My bootstrap file (all requests are redirected to it using .htaccess):
    PHP Code:
    <?php
    require 'app/config/config.php';
    require 
    'lib/z.php';

    function 
    __autoload($class)
    {
        
    Z::loadClass($class);
    }

    $FrontController = new FrontController;
    $FrontController->run();
    ?>
    Front controller:
    PHP Code:
    <?php
    class FrontController
    {
        function 
    __construct()
        {
        }

        public function 
    run()
        {
            
    $Router = new Router;
            
    $route  $Router->getRoute();
            
            
    $Dispatcher = new Dispatcher;
            
    $Dispatcher->dispatch($route);
        }
    }
    Router:
    PHP Code:
    <?php
    class Router
    {
        protected 
    $segments;

        protected 
    $controller;
        protected 
    $action;
        protected 
    $params;

        function 
    __construct()
        {
            
    $this->parseSegments();
        }

        public function 
    getRoute()
        {
            
    $this->route();

            
    $route = array(
                
    'controller' => $this->controller,
                
    'action'     => $this->action,
                
    'params'     => $this->params
                
    );

            return 
    $route;
        }

        public function 
    route()
        {
            
    /*
             * TODO:
             * First call a custom router, and if it can't find a custom route calls the default router.
             */
            
    $this->routeDefault();
        }

        public function 
    routeDefault()
        {
            
    $segments $this->segments;
            
            if (!
    $segments[0]) {
                if (
    CFG_DEFAULT_CONTROLLER) {
                    
    $controller CFG_DEFAULT_CONTROLLER;
                    
    $action     'index';
                } else {
                    
    $controller 'index';
                    
    $action     'index';
                }
            } elseif (
    $segments[0] && !$segments[1]) {
                
    $controller $segments[0];
                
    $action     'index';
            } elseif (
    $segments[0] && $segments[1]) {
                
    $controller $segments[0];
                
    $action     $segments[1];
            }
            
            if (
    $segments[2]) {
                
    $params array_slice($segments2);
            }
            
            
    $this->controller $controller;
            
    $this->action     $action;
            
    $this->params     $params;
        }
        
        public function 
    routeCustom()
        {
        }
        
        protected function 
    parseSegments()
        {
            
    $uri $_SERVER['REQUEST_URI'];
            if (
    strstr($uri'?')) {
                
    $uri substr($uri0strpos($uri'?'));
            }

            
    $segments explode('/'trim($uri'/'));
            
    $this->segments $segments;
        }
    }
    ?>
    Dispatcher:
    PHP Code:
    <?php
    class Dispatcher
    {
        function 
    __construct()
        {
        }

        public function 
    dispatch($route)
        {
            
    $file       CFG_APP_PATH 'controllers/' strtolower($route['controller']) . '.php';
            
    $controller ucfirst($route['controller']) . 'Controller';
            
    $action     strtolower($route['action']);

            @
    Z::loadFile($file);

            if (@
    class_exists($controller) && is_subclass_of($controller'PageController')) {
                
    $controller = new $controller;

                if (
    method_exists($controller$route['action'])) {
                    
    $controller->run($route['action'], $route['params']);
                } else {
                    if (
    method_exists($controller'noAction')) {
                        
    $controller->run('noAction');
                    } else {
                         
    /*
                          * TODO:
                          * Throw exception or raise error
                          */
                         
    die('invalid action');
                    }
                }
            } else {
                
    /*
                * TODO:
                * Throw exception or raise error
                */
                 
    die('invalid controller');
            }
        }
    }
    ?>
    Sample controller:
    /test/ or /test/index/
    /test/testing/
    /test/anything => /test/noAction/
    PHP Code:
    <?php
    class TestController extends PageController
    {
        public function 
    index()
        {
            echo 
    'This is the test controller, index action.';
        }
        
        public function 
    testing()
        {
            
    $data = array(
                
    'title' => 'View Test',
                
    'name'  => 'John'
                
    );

            
    $View = new View('test'$data);
            
    $View->render();
        }
        
        public function 
    noAction()
        {
            echo 
    '404';
        }
    }
    ?>
    Enlighten me

  2. #2
    simple tester McGruff's Avatar
    Join Date
    Sep 2003
    Location
    Glasgow
    Posts
    1,690
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    You'll often hear people talk about presentation (UI) / domain / data access layers. MVC finesses this by separating presentation into view and controller. However, if controller and view are to be separate, controllers can't pass data to views as in TestController: views are responsible for gathering their own data. (A controller could tell the view to render itself though). A view is more than just an html template. The question to ask of the design is can you create a different view without also changing the controller?

    Of course you don't have to use an MVC layering scheme. If views and controllers don't vary indepedently, there would be no need to do this. I find that they do. For example, I'm working on something where I haven't decided what kind of messages to put in form processing scripts yet. While I'm developing, I'm just sticking in low-level error etc messages (which is handy to check everything's working) but these would not be appropriate for the client who will need something less technical. I'll change that later - and may have to change it again once he's had a chance to use it. When I start getting some feedback I might need to make several UI changes. Some of these might require fine tuning the options available in the UI (buttons, links and the actions associated with them). That'll require new controllers and views. Some may simply mean attaching a different view to the same domain manipulation - eg different kinds of reports as above. If controllers and views are well separated, I won't need to edit the controller as well as the view to achieve this.

  3. #3
    SitePoint Addict
    Join Date
    Jan 2005
    Location
    United Kingdom
    Posts
    208
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    How is this different to Zend Framework? It looks practically the same to me. I know you've said you base you work on ZF but copying it tit for tat is probably not going to teach you much

  4. #4
    Where's my title at? dreaz's Avatar
    Join Date
    Apr 2004
    Posts
    144
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    McGruff,
    that makes a lot of sense, but I'm not sure if my views/controllers will change that much. Anyway, could you provide an example with the view getting the data from the controller instead of the controller sending to the view?
    From other frameworks I've seen the view part where it is an actual class instead of a simple template, and while that might be very welcome on a big application that changes a lot it's certainly does not fit my case.

    Shrike,
    for this part of the code it really is pretty much zend, except for the added simplicity, but as I said, this is just the bare bones. I'll be implementing more features (common helpers, custom and simple URL routing) as soon as I'm confident with what I have.
    The point here was really gather opinions on that base structure, and looking at McGruff comments it's pretty obvious that it's not a perfect solution.

  5. #5
    simple tester McGruff's Avatar
    Join Date
    Sep 2003
    Location
    Glasgow
    Posts
    1,690
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by dreaz
    could you provide an example with the view getting the data from the controller instead of the controller sending to the view?
    The view would get its data either from domain objects or direct from a data access layer if there is no domain logic to be done. The latter should be pretty straightforward - when you instantiate a view just pass in whatever it needs. With a form processing script you'll want to manipulate some domain objects (the controller makes these calls) and - somehow - report on the processing results. The Observer pattern will be worth looking at. This allows a third party to monitor an interaction between other objects without interfering with the interaction. The view can listen in to the processing and, when it comes time to print, it can spit out whatever data it has gathered while observing the domain.

    Something I call a "Wiretap" pattern can be useful. It's a variation on Observer which allows you to add in the observation without making any changes to either of the objects involved in the observed interaction. For example, suppose you have an ActiveRecord and in a controller you make a call:

    PHP Code:
                       $record->foo(); 
    To set up a "wiretap" you need a wiretap object which duplicates the record object interface (or at least the part which is called by the client ie the foo() method above).

    PHP Code:
                  class Wiretap {
                  
                      function 
    Wiretap(&$record, &$listener) {
                          
    $this->_record =& $record;
                          
    $this->_listener =& $listener;
                      }
                      function 
    foo() {
                          
    $result $this->_record->foo();
                          
    $this->_listener->receiveFooResult($result);
                          return 
    $result;
                      }
                  } 
    The wiretap passes the return value of the $record->foo() call through to the client. When you're stitching everything together the client gets the wiretap object rather than the $record. The wiretap presents the same interface and behaves in exactly the same way so the client isn't aware that we've interposed something. The record object isn't aware of the wiretap either: observation can be added without making any code changes to either object involved in the observed interaction. The listener is a view object. When it gets a receiveFooResult() message it can decide what kind of information to display based on the $result parameter.

    You could edit the domain objects themselves, simply adding an addObserver() method. However, you might use Wiretap if you don't want to make these kind of changes. In the example I gave above, I'm expecting to want to change the messages displayed in the report. A very simple "processing failed" message can maybe pick up a simple boolean from somewhere. A detailed report - with information on affected database rows, files written and so on - may need to observe a number of different interactions. I don't want to keep editing domain objects every time I change this. Setting up different wiretaps allows me to change the information gathered by the view without affecting either the controller or any of the domain objects which it manipulates.

    I should say the Wiretap idea is experimental and I haven't been using it for long enough to discover any pitfalls. You may need to be careful with the listener interface. For example, there might be a dozen different record objects each with a foo() method. If you're listening in to all of these, you may not be able use the same $listener->receiveFooResult() message each time. If you need to tell them apart, you could pass some kind of identifier as a parameter or (as I think I'd prefer) send different messages "blueThingHasBeenFooed", "redThingHasBeenFooed" and so on. That means a different wiretap class for each domain object.

  6. #6
    Non-Member
    Join Date
    Jan 2003
    Posts
    5,748
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Umm...

    PHP Code:
    class View // listener?
    private $strategy;

    public function 
    setStrategy$strategy ) {
    $this -> strategy $strategy;
    }

    public function 
    recieveFooResult$result) {
    return 
    $this -> strategy -> foo$result );
    }
    // ...

    You could therefore have different strategies, ie

    PHP Code:
    // different strategies
    class BlueThingHasBeenFooed {}
    class 
    RedThingHasBeenFooed {}
    class 
    GreenThingHasBeenFooed {}
    // et al 
    Not sure how you would implement the Strategy giving McGruffs example, but I've taken a guess anyways.

  7. #7
    SitePoint Wizard silver trophy kyberfabrikken's Avatar
    Join Date
    Jun 2004
    Location
    Copenhagen, Denmark
    Posts
    6,157
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I'm not sure I completely grasped where you're getting at with Wiretrap, but it reminds me somewhat of Deferred.

  8. #8
    simple tester McGruff's Avatar
    Join Date
    Sep 2003
    Location
    Glasgow
    Posts
    1,690
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    It sounds like Deferred is dealing with a similar problem "interleaving several tasks". Specifically, the idea of Wiretap is to weave in a secondary task which depends on the result of an interaction between another pair of objects but without affecting this in any way. The interacting objects aren't aware of the wiretap (hence the name) and their classes don't need to be edited. I'm not familiar with Deferred but it sounds like it could be implemented with a Wiretap although not necessarily so. Another option is to add in observer methods (eg addObserver, notify) to the observable but if that's not a good choice you can do it with a wiretap and don't have to change any existing code.

    Almost. The only change is in the way you stitch everything together. Normally a reference to the service object at one end of the observed interaction would be passed to the client at the other. With Wiretap, the client instead gets a reference to the wiretap object and the wiretap object gets a reference to the service object and a listener. The wiretap will basically just pass through method calls from the client to the service object and at the same time send any relevant messages to the listener.

    With the view example, you can chop and change the "listening points" and the information which is being gathered without having to make any changes to the controller or domain code.

  9. #9
    SitePoint Wizard silver trophy kyberfabrikken's Avatar
    Join Date
    Jun 2004
    Location
    Copenhagen, Denmark
    Posts
    6,157
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Thanks for the explanation, I think I tossed in the wrong buzz-word - AOP would have been more appropriate.
    While I think I understand the technicalities, I'm not sure I see how you would use it in context. Could you show a mock of how you'll use it for a real-world situation, say for controlling a form ?

  10. #10
    Where's my title at? dreaz's Avatar
    Join Date
    Apr 2004
    Posts
    144
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Ok,
    I have looked over the observer and strategy pattern and while I can very much see it being used on a big, ever-changing application, it looks a bit too overwhelming for my needs.

    An observer/wiretap makes a lot of sense to me, so does the strategy, but I'll end up with an entire class for something that could be easily handled by a simple template and a couple of if's (if that). Is it that wrong to use the controller to get the data from the model and send it to the view?

    I don't want to sound like I'm just too lazy to learn new things, but I really think that for what I'm doing I can keep it simple without compromising too much of the maintainability of my code. That's why I looked into MVC in the first place, being able to separate view from the logic really appeals to me, but I don't want to go so far way that it will stop making sense to me.

    What do you suggest?

  11. #11
    SitePoint Evangelist
    Join Date
    Jun 2003
    Location
    Melbourne, Australia
    Posts
    440
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    McGruff's Wiretap also seems quite like WACT's Delegate. (Well, it was called Delegate in release 0.2a, but has since been renamed Callback with various 'Event' classes extending it... I think -- it's been a while since I looked in CVS.)

    Wiretap calls the listener's "receiving" method, whereas Wact's Delegate/Callback doesn't have a receiving method. Rather, the "listener" just has to know the Delegate's invoke() method. It's nice because the method wrapped by the Delegate/Callback can be called anonymously.
    Zealotry is contingent upon 100 posts and addiction 200?

  12. #12
    simple tester McGruff's Avatar
    Join Date
    Sep 2003
    Location
    Glasgow
    Posts
    1,690
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by kyberfabrikken
    Thanks for the explanation, I think I tossed in the wrong buzz-word - AOP would have been more appropriate.
    It did cross my mind but I only half-understand AOP so I'm not sure if it's doing the same kind of thing.

    Quote Originally Posted by kyberfabrikken
    Could you show a mock of how you'll use it for a real-world situation, say for controlling a form ?
    Wiretaps wouldn't be used to control a form. The specific problem I had (see first post) was that I wanted to vary the way the app reports on some form processing but the processing itself would not change (well it might if I need to make a big change to the UI after some user feedback but that's another problem). I haven't decided how much technical detail to give to the end user (not much probably since it's all greek to them) but I was quite enjoying more verbose reports while developing. I can pop "wiretaps" in and out anywhere I like to gather as much or as little information as I want without changing the controller or the domain objects. So the wiretap objects don't control the form they just provide "listening points" to report back to the view what's going on.

    Does that help explain it better? I'll post more code if need be.

  13. #13
    simple tester McGruff's Avatar
    Join Date
    Sep 2003
    Location
    Glasgow
    Posts
    1,690
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by dreaz
    Ok,
    I have looked over the observer and strategy pattern and while I can very much see it being used on a big, ever-changing application, it looks a bit too overwhelming for my needs.

    An observer/wiretap makes a lot of sense to me, so does the strategy, but I'll end up with an entire class for something that could be easily handled by a simple template and a couple of if's (if that). Is it that wrong to use the controller to get the data from the model and send it to the view?
    There's a saying "make things as simple as you can but no simpler". Don't add anything unless you've identified a real need. Since controllers will already have references to domain objects within their class scope, I can see why it's tempting just to hand them straight over to the view. If that doesn't cause you any problems just do it. OOP is all about choices and consequences. If there are no consequences you've got more choices. You can always refactor later.

    What I originally meant to do was give an example of how views can be varied independently of controllers (which I'd see as a prerequisite for MVC). If the controller passes all the view data on to the view, a change to the information displayed in a view will also require a change to the controller. If you don't think that will be a problem I'd just ignore it.

  14. #14
    Where's my title at? dreaz's Avatar
    Join Date
    Apr 2004
    Posts
    144
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by McGruff
    There's a saying "make things as simple as you can but no simpler". Don't add anything unless you've identified a real need. Since controllers will already have references to domain objects within their class scope, I can see why it's tempting just to hand them straight over to the view. If that doesn't cause you any problems just do it. OOP is all about choices and consequences. If there are no consequences you've got more choices. You can always refactor later.

    What I originally meant to do was give an example of how views can be varied independently of controllers (which I'd see as a prerequisite for MVC). If the controller passes all the view data on to the view, a change to the information displayed in a view will also require a change to the controller. If you don't think that will be a problem I'd just ignore it.
    I understand where you go with the view should not depend from the controller, but isn't the view directly related to the controller anyway? You posted how you'd go about making this independence, but when exactly would you do it?
    What I mean is, why is the view supposed to be independent from the controller, if they're made to be used with it. I just don't see the case where this would be needed, but please, show me.

    Does it helps if I tell you that pretty much all the data I pass to the view from the controller will come from the model? That's how I see it being done on the MVC examples and frameworks all around (including rails and zend).

  15. #15
    SitePoint Wizard silver trophy kyberfabrikken's Avatar
    Join Date
    Jun 2004
    Location
    Copenhagen, Denmark
    Posts
    6,157
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by McGruff
    Does that help explain it better? I'll post more code if need be.
    I think I'm getting it, but I'd love to see some code.

    Quote Originally Posted by McGruff
    It did cross my mind but I only half-understand AOP so I'm not sure if it's doing the same kind of thing.
    I think I'm getting at 3/4 myself. If you are at home in javascript, dojo's event system might help you getting it to "click". Did for me anyway.
    http://dojotoolkit.org/docs/dojo_eve...seeking-advice
    http://alex.dojotoolkit.org/wp-conte...d_FP_in_JS.pdf

  16. #16
    SitePoint Guru dagfinn's Avatar
    Join Date
    Jan 2004
    Location
    Oslo, Norway
    Posts
    894
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Maybe I missed something, but it seems to me the Wiretap is basically Observer implemented in a Decorator instead of in the observed class itself. That's clever. The observed class knows nothing about what's going on.

    Another alternative I've been thinking about (but never actually implemented) is to replace the Observer pattern with notification to a Composite. More intrusive than the Wiretap, but less so than the classical observer:

    PHP Code:
    class ActiveRecord {
        public function 
    __construct($compositeListener) {
            
    $this->listener $compositeListener;
        }
       
        public function 
    foo() {
            
    // do stuff
            
    $this->listener->update($something);
        } 
    To add a listener, you would have to get the composite listener and add it:
    PHP Code:
    $record->getCompositeListener->add($listener); 
    At least the observed class doesn't have to have the complete mechanism for notifying several listeners. (This is not a problem in languages with multiple inheritance, and that's probably why the pattern was formulated the way it was.)
    Dagfinn Reiersøl
    PHP in Action / Blog / Twitter
    "Making the impossible possible, the possible easy,
    and the easy elegant"
    -- Moshe Feldenkrais

  17. #17
    Non-Member
    Join Date
    Jan 2003
    Posts
    5,748
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    > but isn't the view directly related to the controller anyway?

    Well no... I would say that the View and the Model have more of a relationship within the MVC mould, rather than the View and the Controller

    Try to understand that with Views, there may well be multiple Views for example, for a given request; Note the singular tone in reference to the Request - with those multiple Views, you may have numerous Models as well, or vice versa, ie The same data model, but different ways of showing the data.

  18. #18
    simple tester McGruff's Avatar
    Join Date
    Sep 2003
    Location
    Glasgow
    Posts
    1,690
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by dagfinn
    Maybe I missed something, but it seems to me the Wiretap is basically Observer implemented in a Decorator instead of in the observed class itself.
    Yes: it's a way to implement Observer. I've been shying away from mentioning Decorator in case that might be confusing. Although you could look at the wiretap as decorating the observable - adding a message send to a third party - the behaviour from the perspective of the client doesn't change. Observer adds a secondary, orthogonal action but doesn't alter the primary behaviour.

    Wiretap observes method calls and their return values. It listens in to messages rather than objects. Normally, listener objects are known to an observable object and they can receive anything from within the observable's class scope including results from private method calls. Wiretap is a little bit more limited in what it can see. It listens in to messages (public methods) not objects.

    Quote Originally Posted by dreaz
    isn't the view directly related to the controller anyway?
    It doesn't have to be. Kyberfabrikken asked for some code examples so maybe this will help explain.

    Suppose we have a controller:

    PHP Code:
     class DrinkOrder {
     
         
    /* param (object) $input encapsulates user input
             param (object) $customer
             param (object) $barman
             param (object) $view
         */
         
    function DrinkOrder(&$input, &$customer, &$barman, &$view) {
             
    $this->_input =& $input;
             
    $this->_customer =& $customer;
             
    $this->_barman =& $barman;
             
    $this->_view =& $view;
         }
         
    /*
         */
         
    function try() {
             
    $this->_barman->serve(
                 
    $this->_customer
                 
    $this->_input->getDrinkType(), 
                 
    $this->_input->getPayment());
             
    $this->_view->render();
         }
     } 
    The barman might look something like this:
    PHP Code:
     class Barman {
     
         
    /*  param (object) $bar
             param (object) $till
             param (object) $price_list
         */
         
    function Barman(&$bar, &$till, &$price_list) {
             
    $this->_bar =& $bar;
             
    $this->_till =& $till;
             
    $this->_price_list =& $price_list;
         }
         
    /*  param (object)  $customer
             param (string)  $drink_type
             param (integer) $payment
             return (bool)
         */
         
    function serve(&$customer$drink_type$payment) {
             if( !
    $this->_bar->has($drink_type)) {
                 return 
    false;
             }
             
    $cost $this->_price_list->getCost($drink_type);
             if( !(
    $payment $cost)) {
                 return 
    false;
             }
             
    $this->_till->enter($payment);
             
    $customer->receiveMoney($this->_till->remove($payment $cost));
             
    $customer->receiveDrink($this->_bar->getDrink($drink_type));
             return 
    true;
         }
     } 
    A factory class hooks everything up:
    PHP Code:
     class DrinkOrderFactory {
     
         function &
    getController() {
             require_once(...);
             
    $controller =& new DrinkOrder(
                 
    $this->_Input(), 
                 
    $this->_Customer(), 
                 
    $this->_Barman(), 
                 
    $this->_View());
             return 
    $controller;
         }
         function &
    _Bar() {
             if( !isset(
    $this->_bar) {
                 require_once(...);
                 
    $this->_bar =& new Bar;
             }
             return 
    $this->_bar;
         }
         function &
    _Till() {
         }
         function &
    _PriceList() {
         }
         function &
    _Barman() {
         }
         function &
    _Customer() {
         }
         function &
    _View() {
         }
         
    // etc
     

    Let's say the requirement is for a very simple success/fail view. You could listen in to Barman with:
    PHP Code:
     class BarmanWiretap
     
    {
         
    /*  param (object) $barman
             param (object) $listener
         */
         
    function BarmanWiretap(&$barman, &$listener) {
             
    $this->_barman =& $barman;
             
    $this->_listener =& $listener;
         }
         
    /*  param (object)  $customer
             param (string)  $drink_type
             param (integer) $payment
             return (bool)
         */
         
    function serve(&$customer$drink_type$payment) {
             
    $is_served $this->_barman->serve(
                 
    $customer
                 
    $drink_type
                 
    $payment);
             
    $this->_listener->newDrinkOrder($is_served);
             return 
    $is_served;
         }
     } 
    The wiretap object is passed to DrinkOrder in place of the standard Barman:
    PHP Code:
     class DrinkOrderFactory {
     
         function &
    getController() {
             require_once(...);
             
    $controller =& new DrinkOrder(
                 
    $this->_Input(), 
                 
    $this->_Customer(), 
                 
    $this->_BarmanWiretap(), # wiretap replaces the std Barman
                 
    $this->_View());
             return 
    $controller;
         }
         function &
    _BarmanWiretap() {
             if( !isset(
    $this->_barman_wiretap) {
                 require_once(...);
                 
    $this->_barman_wiretap =& new BarmanWiretap(
                     
    $this->_Barman(), 
                     
    $this->_View());
             }
             return 
    $this->_barman_wiretap;
         }
     
         
    //etc 
    The view object will monitor serve() method calls made to the Barman by the controller.
    PHP Code:
     class DrinkOrderView {
     
         function 
    newDrinkOrder($is_served) {
             if(
    $is_served) {
                 
    $this->message 'Enjoy your drink :)';
             } else {
                 
    $this->message 'Cannot serve drink :(';
             }
         }
         function 
    render() {
             
    // print the message
         
    }
     } 
    Now, suppose you wanted to display a different kind of message in the view: "Here is your beer and 50p change". If you look back at the Barman class we can get the required data by listening in on calls to $customer->receiveMoney() and $customer->receiveDrink(). A CustomerWiretap class would follow the same general pattern as BarmanWiretap and DrinkOrderFactory would instantiate Barman objects with a CustomerWiretap in place of the standard Customer. This would be monitoring only one of three possible cases (the other two are drink not in stock and insufficent payment - see Barman) so you'd also need to take account of these.

    Changing the view like this will not require any changes to the controller or domain objects (I'd really rather not muck about with the latter if I can avoid it). The domain manipulation defined in DrinkOrder remains exactly the same regardless of the view. However, with the controller passing data to the view, DrinkOrder would have had to be edited - it needs to hand over a different set of data. You might end up passing values through a whole chain of domain objects all the way back to the controller before finally being able to pass them to the view. Setting up "listening points" (with one flavour of Observer or another) means you can get them directly.

  19. #19
    SitePoint Guru dagfinn's Avatar
    Join Date
    Jan 2004
    Location
    Oslo, Norway
    Posts
    894
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Duh...I love it , but couldn't the Controller just pass the domain objects to the View? Give the Barman and the Customer to the View for interrogation, so to speak? It does assume that the Barman and the Customer remember what's happened, but it's a heck of a lot simpler in principle, and there's still no need to change the domain objects or the Controller if you want to display something else in the View.

    It's not clear to me what is the actual benefit of your setup.
    Dagfinn Reiersøl
    PHP in Action / Blog / Twitter
    "Making the impossible possible, the possible easy,
    and the easy elegant"
    -- Moshe Feldenkrais

  20. #20
    simple tester McGruff's Avatar
    Join Date
    Sep 2003
    Location
    Glasgow
    Posts
    1,690
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I'd agree you shouldn't make things more complicated than you have to. The benefit in the kind of scenario I've sketched out is that views become well-separated.

    Generally, controllers might make a few calls to Barman-like objects before telling the view to render itself. The Barman-like objects may provide a simpler front to other objects, and so on. Depending on the complexity of the domain, you may have a long route to travel to get information - a boolean, an error message, etc - back to the top-level controller from an object much lower down in the structure. That, I'd suggest, is a smell.

    Altering domain objects so that they record state required solely by the view might also be a smell. If they need to know this anyway fine but if not I might be adding baggage to some nice, clean domain objects. A traditional Observer implementation does something similar: although it keeps the view nicely separated I've still got to add some message sending code directly into a domain object.

    If the controller already knows about the objects which the view wants to interrogate it could pass them straight to the view - sure. Often it will and I can see how tempting that is. However, if they're buried away down in the hierarchy it won't and once again you'll have to travel the scenic route all the way back to the controller rather than communicate directly with the view using Observer.

    The question to ask is: how well does the design cope if I decide to change the information displayed in the view? If you need this kind of separation it can be done - but maybe you don't.

  21. #21
    SitePoint Guru dagfinn's Avatar
    Join Date
    Jan 2004
    Location
    Oslo, Norway
    Posts
    894
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by McGruff
    I'd agree you shouldn't make things more complicated than you have to. The benefit in the kind of scenario I've sketched out is that views become well-separated.

    Generally, controllers might make a few calls to Barman-like objects before telling the view to render itself. The Barman-like objects may provide a simpler front to other objects, and so on. Depending on the complexity of the domain, you may have a long route to travel to get information - a boolean, an error message, etc - back to the top-level controller from an object much lower down in the structure. That, I'd suggest, is a smell.
    Altering domain objects so that they record state required solely by the view might also be a smell. If they need to know this anyway fine but if not I might be adding baggage to some nice, clean domain objects.
    That seems pretty theoretical for a Web application. Do you have some real-world scenario in which you would want to display information that's already been forgotten by a domain object?
    Quote Originally Posted by McGruff
    If the controller already knows about the objects which the view wants to interrogate it could pass them straight to the view - sure. Often it will and I can see how tempting that is. However, if they're buried away down in the hierarchy it won't and once again you'll have to travel the scenic route all the way back to the controller rather than communicate directly with the view using Observer.
    If they're buried away down in the hierarchy, how is it easier or more flexible to dress them up in Wiretaps rather than to give them to the View directly? Seems like the equivalent operation to me.
    Quote Originally Posted by McGruff
    The question to ask is: how well does the design cope if I decide to change the information displayed in the view? If you need this kind of separation it can be done - but maybe you don't.
    Your approach intrigues me, but you still haven't made it clear enough to allow me to know whether there's something useful to be learned from it. I understand your requirement to avoid having to change the Controller. I also understand how your Wiretap system works, except you didn't tell us how it gets wired up in the first place.

    I still don't get how the Wiretap is necessary to avoid having to change the Controller. And when you say the Controller shouldn't pass data to the View, I'm not sure what you mean by data.

    What I would say in answer to your stated requirements is that whatever the Controller passes to the View should be coarse-grained objects rather than single data items. If there's enough information in there for the View to serve its varying needs, you won't have to change the Controller every time there is a new UI requirement. That much seems reasonable and should be sufficient for the separation you're talking about.

  22. #22
    simple tester McGruff's Avatar
    Join Date
    Sep 2003
    Location
    Glasgow
    Posts
    1,690
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by dagfinn
    when you say the Controller shouldn't pass data to the View, I'm not sure what you mean by data
    Anything the view needs to do its job.

    Quote Originally Posted by dagfinn
    you didn't tell us how it gets wired up in the first place
    The factory class wires everything up together - compare the two getController() methods in DrinkOrderFactory, above. One passes in a Barman, the other a BarmanWiretap.

    Quote Originally Posted by dagfinn
    That seems pretty theoretical for a Web application
    OOP is chock full of theory albeit with some very pragmatic ends. Maybe some things are absolutely good or bad and others are more a matter of personal style. For me at least, it seemed to pull everything apart nicely and solved some problems I was having.

    Quote Originally Posted by dagfinn
    Do you have some real-world scenario in which you would want to display information that's already been forgotten by a domain object?
    The Till might store the total money which it contains but not each individual payment. I'd rather not have to change the Till object by adding a $last_entry property if this is irrelevant to its real job. You've also got to find a way to get the value to the view either on its own or by passing the Till object around. However, if the view changes, all that becomes redundant. It gets worse the more things there are going on and the more information you want to gather for a report. A Till wiretap would also become redundant but code changes are limited to objects in the view layer. Once I've got a fully-tested, working domain transaction I don't want to risk changing it if I can avoid it - particularly with something financial where you can't get away with the slightest error.

    The common Observer implementation would also help but, when the view changes, there's some vestigial machinery in a domain object to be cleared up.

    Quote Originally Posted by dagfinn
    What I would say in answer to your stated requirements is that whatever the Controller passes to the View should be coarse-grained objects rather than single data items. If there's enough information in there for the View to serve its varying needs, you won't have to change the Controller every time there is a new UI requirement.
    But you will - or rather might depending on the change. If the controller passes objects $foo and $bar to the view, and later you change the view such that information from $foo and $bar is not required, you'll have a controller to edit - and anything else that $foo and $bar had to be passed through to get to the controller.


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •