SitePoint Sponsor

User Tag List

Page 1 of 2 12 LastLast
Results 1 to 25 of 26
  1. #1
    SitePoint Wizard DougBTX's Avatar
    Join Date
    Nov 2001
    Location
    Bath, UK
    Posts
    2,498
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    Action methods style MVC

    Dr Livingston asked me to expand on an MVC idea I wrote, though I didn't want to hijack the thread, so moving to a new one. Old thread: http://www.sitepoint.com/forums/show...=239167&page=3

    This post expands on code found here: http://www.sitepoint.com/forums/show...97&postcount=9

    You might want to read that first.

    Basically, there are controller classes. Each class has a set of action methods. Routing calls a controller based on the URL. The controller calls the model. The model interacts with the relaional database managment system, here, just an array Routing then calls the view, using the data stored from the model by the controller.

    I've attached the demo code, so if you want to play as I write, get it from there instead of trying to patch things together

    Routing handles getting from a URL to the code we want to run. To avoid any confusion, I'm using the language from ActionPack. Here's what the important bit looks like for the demo:

    PHP Code:
    $route = array(
        
    "controller" => "products",
        
    "action" => "list",
    ); 
    This data is used to call a Runner class. The Runner class is quite special. It acts a bit like a mop. It is a bit stupid, but it absorbs stuff. The stuff it absorbs is the data saved to $this by the controller, but by a trick of static access, $this is an instance of Runner, not an instance of the controller. We will be passing this data to the view, but we don't want to give the view access to the controller, so the mop idea works quite well.

    We take advantage of the fact that classes in PHP don't need to have variables defined before we can assign to them.

    PHP Code:
    class Runner {
        
        function 
    Runner($controller_name$action$params) {
            
    $this->template $action;
            
    $this->params   $params;
                    
            
    // There needs to be error handling here for
            // when the action does not exist.
                    
            
    eval("$controller_name::pre_filters();");
            eval(
    "$controller_name::act_$action();");
            eval(
    "$controller_name::post_filters();");
        }
            

    (I'm not sure how to remove the evals, I'm not sure it is a problem. If you've got ideas, please post!)

    The $controller_name is made by adding "controller" onto the end of $route['controller']. "act_" is added to the name of the action so that we can use actions called "list" without running into naming conflicts. By setting $this->template, we can change which template is rendered inside the actions. $this->params contains the data from $route, and from $_GET and friends. The code to call Runner looks like this:

    PHP Code:
    // Append 'Controller' so we can avoid any possible
    // naming conflicts with model classes.
    $controller  $route['controller'] . 'Controller';

    $action_name $route['action'];

    $params array_merge($route, array(
        
    // Add others you'd like to use framework wide.
        // Also do any framework wide manipulation here,
        // for example killing magic_quotes.
        
    'get' => $_GET,
        
    'post' => $_POST,
        
    'request' => $_REQUEST
    ));

    $data = new Runner($controller$action_name$params); 
    Next, we need to take a look at the code that is being called in the ProductsController. There is a higherarchy of controllers using inheritance. At the root, is the "ControllerBase". At the moment it just defines default pre and post filters which don't do very much. It is part of the framework.

    Next in the chain is the "ApplicationController". This is where you would add pre filters which you want run application wide. Then, we have the ProductsController itself. You could add a pre filter here to add authentication checks for example. This also has our actions. Here, we have two: one to list all out products, and one to show one product. Here is the code:

    PHP Code:
    class ControllerBase {
        function 
    pre_filters()  {
            
    session_start();
        }
        function 
    post_filters() { }
    }

    /* controllers */

    class ApplicationController extends ControllerBase {
        
    }

    class 
    ProductsController extends ApplicationController {

            function 
    act_list() {
                    
    $this->products Products::find_all();
            }
            
            function 
    act_show() {
                    
    $this->product Products::find_by_id($this->params['id']);
            }


    Products is the model class. Here, I'm using a syntax which looks like an odd combination of ActiveRecord and Propel - you might want to access your models in a different way because Propel is only half great, and ActiveRecord (which is great) isn't written in PHP. Regardless, here is what my Products Model class looks like:

    PHP Code:
    /* models */

    class Products /* extends ModelBase */ {
        
        
    // Put this table in a database
        
    function database_table() {
            return array(
                
    => 'Shopping cart',
                
    => 'Forum software',
                
    => 'Blog manager'
            
    );
        }
            
        function 
    find_all() {
            
    // Replace with calls to PDO or Propel.
            
    return Products::database_table();
        }
        
        function 
    find_by_id($id) {
            
    // Replace with calls to PDO or Propel.
            
    $database_table Products::database_table();
            return 
    $database_table[$id];
        }
        

    Now we have an M and a C, and the code to call it. All that is left is a V. This is done using a PHP template, and an ultra basic template class. You might like to use something with caching support and the like. Grab a copy of the excelent SimpleT if you want these features.

    Here it is in all its glory:

    PHP Code:
    /* Template engine. Replace with SimpleT */

    class Template {
        
        var 
    $template;
        
        function 
    __construct($template_file) {
            
    $this->template $template_file;
        }
        
        function 
    render($data) {
            include(
    $this->template);
        }
        

    And a sample template:

    HTML Code:
    <table> 
    <?php foreach($data->products as $product) { ?> 
    <tr><td><?php echo $product ?></td></tr> 
    <?php } ?> 
    </table>
    This is what you have to call to make the template class render the page:

    PHP Code:
    $data = new Runner($controller$action_name$params);

    $t = new Template($data->template ".phtml");
    $t->render($data); 
    And that's all there is to it You've seen how the controller is called statically, how the data retrieved by the controller from the models is stored in a "Runner" class before it is sent to the template. The only thing that you've not seen, is how to write the URL parsing class. I'll leave that as an exerscise for the reader

    If anyone has suggestions for he code, I'll be glad to update this post (This includes a working routeing.rb file. For documentation, see: http://manuals.rubyonrails.com/read/chapter/65 )

    Removethe .txt extension from the attachments before use.

    Happy coding,
    Douglas
    Attached Files Attached Files
    Last edited by DougBTX; Apr 13, 2005 at 07:16.
    Hello World

  2. #2
    Non-Member
    Join Date
    Jan 2003
    Posts
    5,748
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Thanks a lot for the additional explaining, going to have a read of your link you've posted and see what I can make of this

  3. #3
    SitePoint Wizard DougBTX's Avatar
    Join Date
    Nov 2001
    Location
    Bath, UK
    Posts
    2,498
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by Dr Livingston
    Thanks a lot for the additional explaining, going to have a read of your link you've posted and see what I can make of this
    The only new link is one to the Rails site - you won't be able to use PHP with the code there - though if you stay there too long you might not want to use PHP again
    Hello World

  4. #4
    SitePoint Enthusiast
    Join Date
    Oct 2004
    Posts
    88
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    But iím asking me, how you would proceed when displaying more than one item in one template. Here you have only to display products in a template. Example: I need the products, navigation nodes, latest news, may related links , Ö..


    _______

  5. #5
    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)
    What's the idea of the Runner class ? It seems as a source of eternal confusion to call the action's methods statically from within another object!

    Anyway - The basic idea of routing /module/action/ to $module->action() isn't that controversial, is it ? I mean - that's more or less what you get with Mojavi.

    As I see it, rails is a neat framework because of two things : a) it is complete in the sense that it provides both data-abstraction and routing, and they are tightly integrated, and b) it utilizes the full strength of the language. But since php isn't ruby, a port is bound to be less perfect than the original. It's kind of the same story as with java-is-not-php, except perhaps that ruby and php are closer related than php and java.

    just my five cents.

  6. #6
    SitePoint Wizard DougBTX's Avatar
    Join Date
    Nov 2001
    Location
    Bath, UK
    Posts
    2,498
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by atu
    Example: I need the products, navigation nodes, latest news, may related links
    Each of those would have a model. The controller would explicitly call each model that you need to get information about. Think of it as the mediator pattern rather than a composite view. Because you might want navigation nodes for each of your actions, you would probably want to call them from a prefilter rather than from every action.

    Quote Originally Posted by kyberfabrikken
    What's the idea of the Runner class?
    It enforces decoupling between the view and the controller - it might be too much decoupling. You could pass a controller instance to the view instead. There are other people working to port rails, I'm not doing that. Just throwing ideas around Can anyone explain why you would want a class per action vs a class per controller?

    Douglas
    Hello World

  7. #7
    Non-Member
    Join Date
    Jan 2003
    Posts
    5,748
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    though if you stay there too long you might not want to use PHP again
    Not so fast

    I have no plans to give up on PHP at the moment, as for another language I've installed Python and plan to look at this some more over the next few weeks...

  8. #8
    SitePoint Wizard DougBTX's Avatar
    Join Date
    Nov 2001
    Location
    Bath, UK
    Posts
    2,498
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by Dr Livingston
    Not so fast
    There is past precident Python is nice too though.

    Douglas
    Hello World

  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)
    Quote Originally Posted by DougBTX
    Can anyone explain why you would want a class per action vs a class per controller?
    You end up having rather few actions anyway, so it doesn't matter too much I think. The complicated part is the view.

  10. #10
    SitePoint Wizard DougBTX's Avatar
    Join Date
    Nov 2001
    Location
    Bath, UK
    Posts
    2,498
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by kyberfabrikken
    You end up having rather few actions anyway, so it doesn't matter too much I think. The complicated part is the view.
    What would you put in the view beyond a simple PHP template and some code to handle caching? You don't even have to write your own caching lib, just use the PEAR one.

    The number of actions will obviously depend on the scope of the web site/web application.

    Douglas
    Hello World

  11. #11
    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 DougBTX
    What would you put in the view beyond a simple PHP template and some code to handle caching?
    As numerous threads here on sitepoint have demonstrated, rendering views isn't trvial.

  12. #12
    SitePoint Wizard DougBTX's Avatar
    Join Date
    Nov 2001
    Location
    Bath, UK
    Posts
    2,498
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by kyberfabrikken
    As numerous threads here on sitepoint have demonstrated, rendering views isn't trvial.
    From reading threads here, I've often gotten the impression that people often over-complicate the problem

    Douglas
    Hello World

  13. #13
    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 DougBTX
    From reading threads here, I've often gotten the impression that people often over-complicate the problem
    Hehe ... point taken.

    You still need to deal with compositing views somehow - Ofcourse you can decide that it's simply out of the scope of your example, but I think that routing and composition of views is very closely related, and therefore should be dealt with as one.

  14. #14
    SitePoint Wizard DougBTX's Avatar
    Join Date
    Nov 2001
    Location
    Bath, UK
    Posts
    2,498
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by kyberfabrikken
    You still need to deal with compositing views somehow - Ofcourse you can decide that it's simply out of the scope of your example, but I think that routing and composition of views is very closely related, and therefore should be dealt with as one.
    That's my working definition of a controller Something which manages the composition of a view using the models.

    To break the templates down to increase reuse, ActionPack uses "partials" as sub templates. Generally there is a template for each action, which can include any sub templates (just plain PHP+HTML files) it needs to render. Partials are just like any other template, except they are prefixed with "_". You would just use a normal PHP "include", or add a wrapper function if you don't want to type the _ each time

    Each controller also assigns itself a "layout", which my example doesn't deal with. It would extend the Template class to something like this:

    PHP Code:
    class Template {
        
        var 
    $template;
        var 
    $layout;
        
        function 
    __construct($template_file$layout_file) {
            
    $this->template $template_file;
            
    $this->layout $layout_file;
        }
        
        function 
    render($data) {
            
    ob_start();
            include(
    $this->template);
            
    $content ob_get_clean();
            include(
    $this->layout);
        }
        

    Where the layout.phtml file looks like this:

    HTML Code:
    <html>
    <body> 
    <?php echo $content ?>
    </body>
    </html>
    Cheers,
    Douglas
    Hello World

  15. #15
    SitePoint Guru dbevfat's Avatar
    Join Date
    Dec 2004
    Location
    ljubljana, slovenia
    Posts
    684
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    just a minor thing - about your evals

    eval("$controller_name::pre_filters();");

    is a call to a static method 'pre_filters' in a class named $controller_name

    by using call_user_func (call_user_func_array), this could be called as

    call_user_func(array($controller_name, 'pre_filters'));

    call_user_func(_array) can take a function name (string) as the first parameter. For object methods, the first parameter must be array($object, 'method name'), for static calls, it must be array('class name', 'method name')

    See php.net manual for more details.

  16. #16
    SitePoint Wizard Ren's Avatar
    Join Date
    Aug 2003
    Location
    UK
    Posts
    1,060
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by dbevfat
    just a minor thing - about your evals

    eval("$controller_name:re_filters();");

    is a call to a static method 'pre_filters' in a class named $controller_name

    by using call_user_func (call_user_func_array), this could be called as

    call_user_func(array($controller_name, 'pre_filters'));

    call_user_func(_array) can take a function name (string) as the first parameter. For object methods, the first parameter must be array($object, 'method name'), for static calls, it must be array('class name', 'method name')

    See php.net manual for more details.
    Hmm, but the method then has to be defined as static, which then prevents using $this (Fatal error).

    Not sure there is another way to achieve this, via aggregate_properties() possibly.
    Edit:

    Scratch that, doesnt exist in PHP5.. and the replacement classkit, is an extension and doesnt implement anything similar

  17. #17
    SitePoint Wizard DougBTX's Avatar
    Join Date
    Nov 2001
    Location
    Bath, UK
    Posts
    2,498
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by Ren
    Hmm, but the method then has to be defined as static, which then prevents using $this (Fatal error).
    Looks like call_user_func calls the function in global context rather then the Runner's context: "Fatal error: Using $this when not in object context".

    Is there another way to do it without using eval?

    As only the function calls are made via the eval, rather than larrge blocks of code, I don't think eval is a problem. It doesn't mask errors in other parts of the code as you sometimes see with evaled code, and because it is calling "real" PHP, there shouldn't be any problems using bytecode encoders.

    When thinking about alternatives, I'd rather have the eval in there than loose the $this-> assignment in the controllers because it keeps the code base so much simpler than trying to manage the data by hand in arrays and such.

    Douglas
    Hello World

  18. #18
    SitePoint Guru dbevfat's Avatar
    Join Date
    Dec 2004
    Location
    ljubljana, slovenia
    Posts
    684
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Well, it depends. It doesn't have to be static, but then it must be called as such, which means that the runner has to be aware whether to run the controller functions as static method or as object method.

    But in the code posted by DougBTX, the $controller_name is obviously a string and the methods are called on that class as static.

    The other aproach would be to pass the controller object to the runner or it should create an instance by itself.

    Regards
    eval("$controller_name:re_filters();");

  19. #19
    SitePoint Guru dbevfat's Avatar
    Join Date
    Dec 2004
    Location
    ljubljana, slovenia
    Posts
    684
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by DougBTX
    Looks like call_user_func calls the function in global context rather then the Runner's context: "Fatal error: Using $this when not in object context".
    How did you use it?

    Quote Originally Posted by DougBTX
    As only the function calls are made via the eval, rather than larrge blocks of code, I don't think eval is a problem. It doesn't mask errors in other parts of the code as you sometimes see with evaled code, and because it is calling "real" PHP, there shouldn't be any problems using bytecode encoders.
    Yes, but some day eval might disappear

    Quote Originally Posted by DougBTX
    When thinking about alternatives, I'd rather have the eval in there than loose the $this-> assignment in the controllers because it keeps the code base so much simpler than trying to manage the data by hand in arrays and such.
    Douglas
    There is some misunderstanding here, I guess.

    If you have an object $A (for example, an existing instance of a class A), you can call it's methods like this:
    PHP Code:
    call_user_func(array($A'method_in_A')); 
    it is the same as
    PHP Code:
    $A->method_in_A(); 
    In this case, $this exists inside the method_in_A() and it refers to the same object as $A.

    In your case, you have a string:
    PHP Code:
    $class_name 'A';
    call_user_func(array($class_name'method_in_A')); 
    this is the same as
    PHP Code:
    A::method_in_A(); 
    which is a static method call which means that $this doesn't exist in the method.

  20. #20
    eschew sesquipedalians silver trophy sweatje's Avatar
    Join Date
    Jun 2003
    Location
    Iowa, USA
    Posts
    3,749
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    dbevfat, I believe the point is that DougBTX is using the static call to another object as a "mixin" to the current object scope. See http://advogato.org/article/470.html for some experiments in this area.
    Jason Sweat ZCE - jsweat_php@yahoo.com
    Book: PHP Patterns
    Good Stuff: SimpleTest PHPUnit FireFox ADOdb YUI
    Detestable (adjective): software that isn't testable.

  21. #21
    SitePoint Wizard DougBTX's Avatar
    Join Date
    Nov 2001
    Location
    Bath, UK
    Posts
    2,498
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by dbevfat
    How did you use it?
    PHP Code:
    call_user_func(array($controller_name'pre_filters'));
    call_user_func(array($controller_name"act_$action"));
    call_user_func(array($controller_name'post_filters')); 
    Quote Originally Posted by dbevfat
    Yes, but some day eval might disappear
    Hope not!

    Quote Originally Posted by dbevfat
    PHP Code:
    $class_name 'A';
    call_user_func(array($class_name'method_in_A')); 
    this is the same as
    PHP Code:
    A::method_in_A(); 
    I would have thought so, but actually it isn't. Take this example:

    PHP Code:
    class Alpha {
        function 
    store($data) {
            
    $this->data $data;
        }
    }
    class 
    Beta{
        function 
    Beta($data) {
            
    Alpha::store($data);
        }
    }

    $b = new Beta('Hello!');
    echo 
    $b->data
    If you replace Alpha::store($data); with call_user_func(array('Alpha', 'store'), $data); you'll get an error. (Unless I'm not using call_user_func properly.)

    I'm half thinking that the disjointed behaviour of $this is just buggy OO in PHP, but it does start to make up for flexibility that would otherwise be lacking. Is this anywhere in the docs? I've not seen it...

    Douglas
    Hello World

  22. #22
    SitePoint Wizard DougBTX's Avatar
    Join Date
    Nov 2001
    Location
    Bath, UK
    Posts
    2,498
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by sweatje
    dbevfat, I believe the point is that DougBTX is using the static call to another object as a "mixin" to the current object scope. See http://advogato.org/article/470.html for some experiments in this area.
    For people still doing procedural PHP, I think classes could come in quite useful as basic namespaces using this method. You wouldn't be able to assign to $this, but you could still use normal procedural functions and access them as Lib::my_function().

    Douglas
    Hello World

  23. #23
    SitePoint Guru dbevfat's Avatar
    Join Date
    Dec 2004
    Location
    ljubljana, slovenia
    Posts
    684
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Oh I see now

    I was aware that it's possible to call static functions from within a class which would usually call the superclasses' original method. But I didn't know things work this way.

    I think that it's arguably incorrect - a static call from a class should either invoke a method in one of the superclasses or actually be a real static method - no $this available.

    I feel that there are possible uses of this, but it gets unclear of where did that $this came from into the called method. If that method was called from within another instance, $this refers to the caller and thus introduces a completely another interface for the method. When that method is called from it's own class, the interface is the 'correct' one - the one of the method's class.

    This results in a confusion - who or what is $this in that method? If there is a $this? It is not necessary, if it's called like static method from outside of any instance.

    In cases like these, I'd pass $this as a parameter to the call and use that parameter in the target method. By doing this, the method could be called statically, it would never refer to $this and it would always excpect a valid parameter (perhaps some interface with typehint).

    Regards.

  24. #24
    SitePoint Wizard stereofrog's Avatar
    Join Date
    Apr 2004
    Location
    germany
    Posts
    4,324
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I did some experiments with mixins and ended up with following:

    PHP Code:
    //-----
    // core part

    class Mixed 
    {
        function 
    call_mixin($cb) {
            
    $p "";
            if(
    $n func_num_args() - 1) {
                
    $a func_get_args();
                
    $p '$a['.implode('],$a['range(1$n)).']';
            }
            return eval(
    "return {$cb[0]}::{$cb[1]}($p);");
        }
    }

    //-----
    // test

    class Mixin
    {
        function 
    foo($one 'one'$two 'two') {
            echo 
    $one; echo $two;
            echo 
    $this->quux;
            return 
    999;
        }
        
    }

    class 
    Main extends Mixed
    {
        
        function 
    bar() {
            
    $this->quux 555;
            
            echo 
    $this->call_mixin(array('Mixin''foo')); 
            
            echo 
    $this->call_mixin(
                array(
    'Mixin''foo'), 
                
    'aa'
                
    fopen("nul""w")
            );
        }
    }

    $a = new Main();
    $a -> bar(); 
    call_mixin resembles call_user_func and, unlike eval, can take arguments, even binary ones. All you need to use it is to inherit from Mixed.

  25. #25
    SitePoint Wizard DougBTX's Avatar
    Join Date
    Nov 2001
    Location
    Bath, UK
    Posts
    2,498
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by dbevfat
    This results in a confusion - who or what is $this in that method? If there is a $this? It is not necessary, if it's called like static method from outside of any instance.
    So you think it would be better if a $data (object or array? I'm thinking array) is passed to the action by reference, the action adds whatever it needs to the $data, then the $data is passed to the view class by the framework.

    This would still be better than having each action try and return something. It would be a bit like a light visitor pattern. It wouldn't be a problem having an intance of the controller this way, so the evals could be replaced with this I think:

    PHP Code:
    $data = array();
    $controller = new $controller_name;
    $controller->template $template;
    $controller->params $params;

    $controller->pre_filters($data);
    $controller->"act_$action"($data);
    $controller->post_filters($data); 
    Cheers,
    Douglas
    Hello World


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
  •