TIP: Use Markdown or, <pre> for multi line code blocks / <code> for inline code.
KOstache - view calling a model
  • I've been learning KOstache in the last few days and I really like it.

    The idea of keeping views more isolated by calling models directly within them makes sense.

    The problem I am seeing is that sometimes the data you retrieve from a model is not tied to a particular view. It seems to me that in cases like this gathering the model data in the controller makes more sense since you can do it one time then pass it to all the views that need it.

    Example: I have a category model with a method 'get_categories'. This method does a database query to retrieve the categories stored as MPTT, and saves the result in a public variable in the model before returning it. I also have another method 'get_breadcrumb' which uses the saved 'get_categories' result to create the breadcrumb instead of doing another database query, thus saving me one query.

    Now, if have a view class called View_Category_Menu and I call the model within it to get my categories, I won't be able to pass it back to my general controller. I'll have to do another database call when I call View_Category_Breadcrumb since I haven't saved my categories query.

    How do you deal with cases like this?
  • I have been calling a model in the controller and passing it into the view as an object. The view class then builds its datasets using the model that I gave it. It also means that the controller has access to the model to do anything else you want.

    Here's a sample controller action:


    public function action_new()
    {
    $view = new View_Admin_Post_New;
    $view->post_obj = $post = new Model_Post;

    if ($_POST)
    {
    $post->values(arr::extract($_POST, array('title', 'text')));
    $post->user_id = Auth::instance()->get_user()->id;
    $post->generate_slug();

    if ($post->check())
    {
    $post->save();
    $this->request->redirect('admin/posts');
    }
    }

    $this->request->response = $view;
    }


    And here's a sample from the view class that goes with it.


    public $post_obj;

    public function errors()
    {
    return $this->post_obj->validate()->errors();
    }

    public function post_title()
    {
    return $this->post_obj->title;
    }
  • I guess that's the limit you might reach by calling a model from the view. That is, you can't easily reuse what you grab from the model for other stuff.

    I guess in my example, it would be more flexible to simply repeat a database query, but that seems a bit hmmm... not too great.
  • You could always cache it.
  • you could.
  • @xenakis: I'm not sure if I misunderstood your example - View_Category_Menu and View_Category_Breadcrumb are partials that are both loaded together (ie part of the same page)? If so, couldn't you query the Category model in the calling view class, and pass to each of the partials view classes?
  • You can access models directly within mustache templates, just bind them to the view and then access them:

    {{#model}}
    {{! in model context }}
    {{title}} {{! is about the same as $model->title }}
    {{/model}}
  • @LordZicon - On the site I am making View_Category_Menu always appears. However, View_Category_Breadcrumb is not on every page. For example, if you are browsing user pages, you don't see the Category_Breadcrumb. You only see them both when browsing categories.
  • @shadowhand I consider that very bad practice, the only thing that should be passed to templates are arrays.

    This helps reduce bugs when someone mistakenly puts a {{delete}} into the template. Templates should be *read only* and thats what arrays give you.
  • In Kostache, although you can locally instantiate a Model from the view, the better practice is to let the Controller create/instantiate the Model and make it available as a global instance which can be reused by the Controller and the View and even by controllers through HMVC. To make this possible, I created a static Model class with 2 methods, `Model::factory()` and `Model::instance()`. Using Model::factory() will always return a newly instantiated model, Model::instance() will instantiate a model if it doesn't exist yet, but it will return the same model if it was already instantiated.

    The parameters I pass to those methods are simple string "keys" or "routes" which maps to the correct model. So I use it like this `Model::instance('modeldir/modelfile', [namespace]);`. So what happens here is that if I want to use Model_Category everywhere, it will look like this: `Model::instance('model/category')->get_list();`. If I want to prepare the Model for display by the View, the controller can just set its state.

    In my Controller, I set the model state like this:


    public function action_foo(){
    // This instantiates the Model
    Model::instance('model/foo')
    ->set_state('limit', 5)
    ->set_state('ordering', 'DESC');
    }


    Then in my Kostache view:


    class Views_Foo extends Kostache{
    public function categories(){
    // This does not instantiate the model, but gets the instance created by the controller.
    return Model::instance('model/foo')->get_list();
    }
    }


    Just remember that the Controller is not the data police http://bit.ly/bACEr2 so any filtering or manipulation of data should not be done by the controller. The Controller just set the Model's states then the Model will react based on the states. The View should just get the data that's ready to be displayed.

    Hope this helps.
  • please don't magically set some filters in some random part of your application and magically have them apply in a different, equally-random, part.
  • IMHO you should define the state of the data where you wish to use it i.e. in the view itself.
  • @xenakis: I've not been using Kostache long myself, so I'm still working out different ways of doing things, but so far I've avoided using nested views in favour of partials. In your example, I would be tempted to do something like this:

    View_Category: Queries the Category model (and contains the Category Menu logic / partial).
    View_Category_Breadcrumbs: Extends View_Category and contains the bread crumb logic (and loads the breadcrumb partial).

    You could then have your views extend View_Category_Breadcrumbs when you need to display breadcrumbs in your template. You'd only have one query for the category list then.

    In fact, I'd probably just put the breadcrumb logic etc. into the View_Category class - even if you don't use breadcrumbs in every view, I would guess that loading the partial would have negligible overhead.

    Anyone have any views on doing it this way? As I say, I've not long been playing around with Kostache, so I'm interested to see how others use it.
  • Partials are great.
  • I'd load it always as a partial and use it only when needed, or just have a nested Breadcrumb view although I have been debating this lately with myself too. I really dislike the idea of View_Category_Breadcrumbs to simply add functionality, what happens if you want Breadcrumbs and a Search bar (or something)? It's not really good code, so perhaps having a breadcrumb view returned by a method from the main view class would be the most efficient way (since it's only loaded if needed).
  • @zombor is there any decent way in adding partials to $_partials to the inherited partials from a parent view?
  • @DrPheltRight

    I have resorted to using a differently named partials array and merging it with $_partials in the render method before calling parent::render. I would definitely like to see a better solution though. Maybe we could do some trickery similar to the way $_layout and $_template work.
  • @DrPheltRight: In the constructor of your child view, couldn't you do something like this:
    $this->_partials += array('name'=>'path');
  • ^^ You got it.
  • Thanks for your help guys. I'll try to come up with something today. I agree that the model should be called from the view, just figuring out how to avoid repeated queries. I think you gave me some good ideas. Just have to work this all out.

    It would really help if there were more detailed examples of this. Seeing code in action is always the best help.
  • Thanks, @LordZicon. Should have been obvious.
  • > It would really help if there were more detailed examples of this. Seeing code in action is always the best help.

    Vendo uses mustache to it's full potential: http://github.com/zombor/Vendo
  • @LordZicon true, although I would still prefer:


    public function before()
    {
    parent::before();
    $this->_partials += array('name'=>'path');
    }


    Over this:

    public function __construct($template = null, $view = null, $partials = null)
    {
    parent::__construct($template, $view, $partials);
    $this->_partials += array('name'=>'path');
    }


    But I suppose that is more laziness rather than any real need if I am honest.
  • @raeldc - Iv'e been thinking things through and I like your suggestion. Indeed, I need my model to stay alive so I can use it elsewhere. The problem is I'm baffled on how you "created a static Model class with 2 methods". Do you extend the model class for this?
  • @DrPheltRight - sure you can change the state of the model where you're calling it, but the controller has the access to the request, and it's the controller's responsibility to change the Model's state based on the request. So the controller is the best place to do it. :)

    @xenakis - I don't use Kohana's Model class, it does nothing actually. You just have to extend it.

    Here's how I extended it.



    class Model extends Kohana_Model {
    public static function instance($path, $namespace = 'default')
    {
    static $instances;

    // If instances is null, that means we have to inialize it as an array
    if (is_null($instances))
    {
    $instances = array($namespace => array());
    }

    // Let's get the class name based on the path
    $classname = str_replace('/', '_', $path);

    if (class_exists($classname))
    {
    if (isset($instances[$namespace][$path]) AND $instances[$namespace][$path] instanceof $classname) {
    return $instances[$namespace][$path];
    }

    return $instances[$namespace][$path] = new $classname;
    }
    else
    {
    throw new Kohana_Exception('Class :classname does not exist',
    array(':classname' => $classname));
    }
    }
    }


    Then you can have a model like this:


    class Model_Test {
    protected $calls = 0;
    public function hello()
    {
    $this->calls++;
    return 'called me '.$this->calls.'x';
    }
    }


    Then you can reuse it anywhere.


    echo Model::instance('model/test')->hello();
    echo Model::instance('model/test')->hello();
    echo Model::instance('model/test')->hello();


    The factory method is already in Kohana_Model. But you can extend it anyway you want.

    :)
  • It's the controller's job to tell the view what the filters are, it is not the controller's job to decide what to display. You're just making your application incredibly difficult to understand.
  • @Zeelot3k - you may not like it, but that's MVC for you. It's backed by experience and also by some doctoral dissertations http://bit.ly/3ZfFCX. Cheers :)

    Edit: I don't disagree with you that the controller doesn't decide what to display. That's why I'm using the term "model state" and not filters. It just so happens that filters are used in webapps. Filters can be decided by the view but only the default filters, but I made it as an example because filter instructions are usually coming from user input, and therefore, the controller is in charge in telling that model that "this is your state" based on the request.
  • @raeldc - Thanks for the code, it's very much appreciated. I'll try to work out something with it today.
  • @raeldc I was talking about where to call the Model from which unless you need to modify the data in the Model, it can be initiated within the View class. Sure certain parameters may be passed from the Controller to the View to decide how it displays, but the Model call remains in the View itself.

    Edit: I just re-read my post and can see I wasn't too clear about what I meant.
  • @xenakis just improve the code. For one, you may auto-append "model" in the model path so you just call it like this Model::instance('test')->hello();