A missing concept: model “components”

Published on and tagged with cakephp  component  ideas  model

The fatter your models become, the more obvious it is (at least for me) that something is missing. Let’s say we have a bunch of models. Two of those models use internally the same functionality. To avoid code duplication, you want to share this functionality between the two models. But how could you accomplish that?

One idea is to move the functionality to the AppModel. That works, but it is a dirty solution. The AppModel should contain generic functionality relevant for all models, and not functionality specific to only two models.

Another idea is to move the functionality into a class in the vendors folder and to load it with App::import(). That works fine, but if you compare this solution with elements and helpers for reusing view functionality, and with components for reusing controller functionality, it is a bit strange there is no “native” solution for models (sure, there are behaviors, but they are not designed for the kind of code reuse I described above).

Hence I think it would be useful to have model “components”, which could be realized in the same way as controller components are realized. For example:

class Example extends AppModel {
    public $components = array('X');

    public function doSomething() {
        ...
        $this->X->doSomethingElse(); // using model component
        ...
    }
}

In the longer term it would even make sense to have a unified “components” concept to replace the current concepts of components and helpers. But well, we will see what the future brings ;-)

22 comments baked

  • Christian Winther

    What about Behaviors ? Its a 1.2 feature – but they would be the component of the model ? :)

  • dr. Hannibal Lecter

    Hi,

    is there a specific reason not to use behaviors for the same purpose? Simply choose not to use any callbacks and create a “ToolsBehavior” and use it. Is that wrong?

  • Jonathan Snook

    Alternatively, and mind you I’ve never tried this, but couldn’t you create a model that inherits from AppModel and then 2 subclasses that inherit from that?

  • Adam Royle

    Like Snook says, just use inheritance. I’ve also used this with Behaviors as well.

    FileBehavior extends ModelBehavior
    ImageBehavior extends FileBehavior
    VideoBehavior extends FileBehavior

    etc

    Of course you need App::import(‘Behavior’, ‘File’); before the class definition, but it works and works well!

  • cakebaker

    @Christian, Hannibal Lecter: Thanks for your comments!

    The “problem” of behaviors is that they are not designed for this purpose and so you cannot access the behavior’s methods from within a model. But that would be necessary to share functionality between models.

    Let’s say we have the following model:

    class A extends AppModel {
        public function doSomething() {
            ...
            $x = $this->calculateX();
            ...
        }
    
        private function calculateX() {
            ...
        }
    }
    

    Now we have a model B which uses the same calculateX() method. If we move this method to a behavior (and make it public), then we can call this method from the controller with $this->A->calculateX() resp. $this->B->calculateX(), but that’s not what we want. We want to call the calculateX() method from within a model method…

    I hope it is now clear why you need a different concept for this purpose.

  • Bret Kuhns

    I also have to agree with Snook here. This post is a fairly good example of one gripe I have with frameworks: they sometimes make people forget the fundamentals. Traditional PHP OOP still works in cakePHP, so why not use “classic” inheritance. I blogged about this quite a while ago and it worked out to be a pretty good solution http://miphp.net/blog/view/how_to_use_inheritance_for_cake_models Now that models are loaded lazily in cake, I’m not sure if you’d have to call loadModel() before you can extend it, but at the very least you should be able to require/include the file “like in the good ol’ days” and then use inheritance to your heart’s content!

  • Mariano Iglesias

    You could achieve the exact same thing (eventhough I think you are just “forcing” a problem by using the same method name for a private method in the model and the behavior) just by using the dispatchMethod functionality on your Behavior collection:

    $this->A->Behavior->dispatchMethod($this->A, ‘calculateX’);

  • Felix Geisendörfer

    $this->calculateX() will work within Model A & B if they both have the same behavior attached that implements this model. If you define this method in A or B itself as well you’ll obviously overwrite the magic handler dispatching the method to the behavior. In that case use Mariano’s suggestion.

  • nate

    “If we move this method to a behavior (and make it public), then we can call this method from the controller with $this->A->calculateX() resp. $this->B->calculateX(), but that’s not what we want.”

    As has been mentioned, that’s completely untrue. In fact, I don’t even know how to make sense of what you just said. If the method is callable from the controller, why *wouldn’t* it also be callable from within the model itself?? Your reasoning makes no sense.

  • John

    Snook’s example of inherited models is already in use in the core. Check out the way the Aco and Aro models work.

  • cakebaker

    @all: Thanks for your comments!

    @Christian, Hannibal Lecter: I’m sorry, but what I wrote in the first comment is wrong, see the comments from Mariano and Felix.

    @Jonathan, Adam, Bret: Yes, inheritance might work in that particular case. But as soon as the situation becomes more complex (A and B share x, A and C share y) it is easier to use composition instead of inheritance.

    @Mariano, Felix: Thanks for your corrections. I wasn’t aware of dispatchMethod() nor that you can access a behavior method with $this->behaviorMethod() from your model (I tried $this->BehaviorName->behaviorMethod(), and that didn’t work).

    Anyway, I’m not sure it is a good idea to use behaviors for the purpose I described. If I add a method to a behavior, it means it becomes a part of the public api of the respective model. And that’s something I don’t want, as the outside world (in this case the controllers) shouldn’t have access to implementation details of my models. Model components would allow to share model functionality between models without affecting the public api of the respective models.

    @nate: See my answer to Felix and Mariano, I used the wrong approach to call a behavior method, hence my wrong reasoning…

    @John: Yes, that’s a good example for inheriting from another model.

  • Mariano Iglesias

    @cakebaker: if you don’t want some of your behavior methods to be exposed to the models then just use the underscore as starting name for those methods (accessibility of magic methods in behaviors follows the same logic as the methods on a controller.)

  • Garret

    Well I for one agree with you.

    I’ve been in many situations where I wished there were components available to load into your model just as easily as you can for controllers. You don’t necessarily even have to call them components, you could call them, say, extensions, processors, plugins? I dunno, something that indicates that they’re not part of the actual model, but will work inside the model for processing purposes and is protected from outside forces i.e. the controller and view.

    I don’t think you’re trying to understand how to get it to work, as I know you probably understand completely how to import files and/or use Snook’s class inheritance solution. You’re looking for the best most elegant way to do it, and I think you’ve hit it on the head.

    Putting everything in AppModel is definitely dirty. I also think Snook’s solution is dirty, he says so himself, and can get very complicated very quickly if you have a large model structure. It would be easier if you could just load a component into your model, just like you can controllers.

    Example:

    Component->action($this->data);

    }
    ?>

    (I know this is a bit of a reiteration of what you wrote, it just adds a little bit for clarity)

    So, anyways, I think you’re right, and I don’t want to mess with the structure of the cake core too much since I like being able to keep current for bug fixes, so I kind of have to live with it for now. However, I think it would be fantastic if this were implemented in the core.

  • Garret

    hmm, your input filter removed some of my example code

  • Mariano Iglesias

    @Garrett: I don’t get it, did you read the comments? What are behaviors then??

  • cakebaker

    @Mariano: I’m not sure you understand what I want to accomplish. I want to share model functionality between models without making the shared functionality available for controllers. If I add a (public) method to a behavior, then I can access it from models *and* controllers. If I make this method protected or private, I can access it neither from models nor controllers. You see the problem?

    @Garret: Thanks for your long comment and the clarifications!

    I have chosen the name “model components” to show it is a very similar concept to the concept of “controller components”. But maybe it would be better to use a different name…

    Yes, you are right, I’m looking for the best most elegant way to do it, and not for a workaround to accomplish (almost) the same.

    The same here, I don’t like to mess with the structure of the cake core, and so I keep using the solution with the “vendors” folder. And yes, it would be cool to have it implemented in the core :)

  • Felix Geisendörfer

    Daniel: I don’t get it. I see the problem with all behaviors affecting the Model API, but that can easily be worked around by Overloading your behavior, and making its instance available to your Model during the setup process and then dispatching calls on it to private methods inside your behavior. However, adding shared functionality between models but hiding it from your controllers makes little to know sense. Do you have an actual usage case for that?

  • the_woodsman

    > However, adding shared functionality
    > between models
    > but hiding it from your controllers
    > makes little to know sense.

    Erm, isn’t this akin to a protected method/field in a parent class?
    If it’s a core part of OO languages, I think it’s probably a good idea: it makes good sense to only expose a well chosen public API.

    PHP doesn’t support multiple inheritence, so as mentioned, a composition pattern is essential to the whole concept.

    Could you not achieve similar using an interface (PHP5 only, so not for the cake core obv), i.e your models implement the interfaces, and your controllers can check if they have?

  • cakebaker

    @Felix: A simple example: a method to format data (e.g. a date) in a certain way. But in principle you can take any method you make private in your model as a potential use case.

    @the_woodsman: Yes, it is the same principle, only in a different context.

    Hm, I don’t understand your idea with the interface :| With an interface you define a public API, so I don’t see how you could hide something by using an interface.

  • Xr

    the_woodsman: The interface forces you to implement some methods, it doesn’t give a shared implementation but a shared API. Not what we’re after, if I understood correctly.
    Still, it is indeed of good practice in OOP to separate concerns.

    I agree: if we already have Helpers and Components, why not have something similar for Models. Behaviors add a *behavior* to the model, i.e. responsibilities to that model. What if we want utility classes ?

  • cakebaker

    @Xr: Thanks for your comment, you worded it much better than I did ;-)

  • Garret

    > I agree: if we already have Helpers and Components, why not
    > have something similar for Models. Behaviors add a *behavior* to
    > the model, i.e. responsibilities to that model. What if we want
    > utility classes ?

    @XR: EXACTLY

Bake a comment




(for code please use <code>...</code> [no escaping necessary])

© daniel hofstetter. Licensed under a Creative Commons License