views:

286

answers:

5

I'm in the process of implementing an ultra-light MVC framework in PHP. It seems to be a common opinion that the loading of data from a database, file etc. should be independent of the Model, and I agree. What I'm unsure of is the best way to link this "data layer" into MVC.


Datastore interacts with Model

//controller
public function update()
{

 $model = $this->loadModel('foo');
 $data = $this->loadDataStore('foo', $model);

 $data->loadBar(9); //loads data and populates Model
 $model->setBar('bar');
 $data->save(); //reads data from Model and saves

}

Controller mediates between Model and Datastore

Seems a bit verbose and requires the model to know that a datastore exists.

//controller
public function update()
{

 $model = $this->loadModel('foo');
 $data = $this->loadDataStore('foo');

 $model->setDataStore($data);

 $model->getDataStore->loadBar(9); //loads data and populates Model
 $model->setBar('bar');
 $model->getDataStore->save(); //reads data from Model and saves

}

Datastore extends Model

What happens if we want to save a Model extending a database datastore to a flatfile datastore?

//controller
public function update()
{

 $model = $this->loadHybrid('foo'); //get_class == Datastore_Database

 $model->loadBar(9); //loads data and populates
 $model->setBar('bar');
 $model->save(); //saves

}

Model extends datastore

This allows for Model portability, but it seems wrong to extend like this. Further, the datastore cannot make use of any of the Model's methods.

//controller extends model
public function update()
{

 $model = $this->loadHybrid('foo');  //get_class == Model

 $model->loadBar(9); //loads data and populates
 $model->setBar('bar');
 $model->save(); //saves

}

EDIT: Model communicates with DAO

//model
public function __construct($dao)
{
    $this->dao = $dao;
}

//model
public function setBar($bar)
{
    //a bunch of business logic goes here
    $this->dao->setBar($bar);
}

//controller
public function update()
{
    $model = $this->loadModel('foo');
    $model->setBar('baz');
    $model->save();
}


Any input on the "best" option - or alternative - is most appreciated.

+4  A: 

I don't think it belongs in controller - that's really part of the view, which can come and go. The business logic and persistence shouldn't depend on view or controller.

A separate service layer gives you the chance to expose that logic as distributed components to non-browser based clients (e.g., mobile views, batch processing, etc.)

I wouldn't have the model extend the data store in an object-oriented sense, because inheritance is an IS-A relationship. As you've noted, the model is not a relational database; that's merely one choice among many for persisting information.

I think it's more composition. The persistence layer could be a separate DAO that persists model objects without requiring that they be aware of the fact that they are longer-lived. OR you have mixin behavior where a model advertises the fact that it has CRUD operations, but it merely passes off requests to the implementation they're given.

The usual Spring idiom would have a service layer that's separate from the controller. It knows about use cases and units of work. It uses model and persistence objects to accomplish the goals of the use case.

MVC implies three actors. I would say there are more layers than that in a typical application - persistence and service need to be added.

UPDATE:

If the model object does not import anything from the persistence package, then it has no knowledge of it whatsoever. Here's a simple example, using a simple model class Person and its DAO interface, each in its own package:

package persistence.model;

public class Person
{
    private String name;

    public Person(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }
}

Note that the DAO interface imports the model package, but not the other way around:

package persistence.persistence;

import persistence.model.Person;

import java.util.List;

public interface PersonDao
{
    Person find(Long id);
    List<Person> find();

    void saveOrUpdate(Person p);
}
duffymo
The Model advertising it has CRUD operations is self-explanatory, but I'm a little unsure of how one might implement a DAO with the Model having absolutely no knowledge of it.
etheros
A: 

My vote is for going down the route of Model extends datastore. That seems most natural because in effect your model is an extension of the underlying datastore. However, you may want to use a Service design pattern to decouple the creation of the model object with the consumption of the resulting object.

class Model {
   protected $_tablename == __CLASSNAME__; // or some derivative of the classname
   protected $_datastore;
   protected $_typeOfDataStore;

   function __construct( $type == 'mysql' ) { 
      $this->_typeOfDataStore = $type;
      $this->bindToDataStore();
   }

   function bindToDataStore( $ds ) { 
      $this->_datastore = DataStoreFactory::buildDataStore( $this->_typeOfDataStore );
   }

}

class DataStoreFactory {

   function buildDataStore( $type ) {
     switch ( $type ) {
       case 'flatfile' : return new FlatFileDataStore();
       case 'mysql': return new MySQLDataStore();
     }
   }
}

class DataStoreBase {
   function connect() { }
   function disconnect() { }
   function getData() { }
   // ...
} 

class FlatFileDataStore extends DataStoreBase { }
class MySQLDataStore extends DataStoreBase { 
   function runQuery() { }
}

That's the construction part. When you need to create/implement a new model, you can simply extend the model:

class Users extends Model {
   protected $_tablename = 'users';
   protected $_typeOfDataStore = 'flatfile';

   function getAllUsers() {
      return $this->runQuery( "SELECT * FROM " . $this->_tablename );
   }

}

And in your controller:

$usersModel = new Users();
$usersModel->getAllUsers();

The objection that you pointed out about the datastore being unable to use any of the Model's methods is a good constraint on the system. The model is a consumer of the datastore and thus the datastore should not be able to directly call the model object's functions. The model should maintain the control and make use of the datastore service whenever it needs to.

Pras
A: 

I think you're trying to build a framework that is too tightly tied to names and concepts. I've found the names and concepts in the Model-View-Controller idiom are not rigidly defined. I've also found that code working in that paradigm needs to be able to bend the rules sometimes.

@duffymo has a very important point about the Model component: a Model isn't a Datastore, it just has a Datastore. If that is not clear, another way of thinking about that it is to look at what an instance of each is supposed to be. An instance of a "datastore" object represents a resource which mediates access to an arbitrary amount of data. Usually it is a database connection. An instance of a "model" object is usually a discrete identity with multiple pieces of data. Usually it represents a database row in a table, but it could be a line in a text file, or one file in a file-store.

Applying the same quesion to the Controller and the View and you can see that the Model part is quite a different animal. Whilst only one Controller object and one View object will normally be in existence at one time, it is quite likely that several different Model objects of different types are in existence at one time.

Unfortunately, I also know that a lot of MVC "frameworks" define a "Model" as an API layer behind which you do SQL statements. In those frameworks, a "Model" is a static class or a single instance and does nothing more than provide near-useless namespace partitioning. And a lot of programmers think this is supposed to make sense and struggle with it. This is why I recommend you don't use the names and concepts of the MVC idiom.

My preferred way to write structured PHP only nods to the MVC paradigm. It has dispatcher and controller logic in the top, and HTML markup in the bottom. This works well because Controllers and Views are often tightly coupled and putting them in the same file is very PHP-ish. Yes, it does take discipline to not do controller-ish actions in view-ish code, but I'd rather have to do that myself than have the framework force me to. These pages don't stand alone, however, they include a lot of common logic and can import things like a lightweight dispatcher system, or even call whole swathes of common controller logic.

But the main thing the common logic provides is a data-abstraction layer -- basically the Model layer as described at the top of my answer. The data-abstraction layer I wrote is basically three things: a database handler, an "object" object and a "collection" object.

The database handler is the DataStore for all objects that live in the datbasee. It's main purpose is so that activities to the database that aren't making a query and getting a response are all in one place. Most database calls are in the object core. The database handler doesn't know anything about the object core: it just takes SQL and returns resultsets.

The other two objects are designed to be sub-classed and will not work if instantiated themselves. Using inheritance and a special extended declaration (done by a static class method), they know how to turn database data into an object. An instance of an inherited "object" object represents one row of the declared class. It is given an ID upon instantiation, and will query the database when it needs to to retreive its one row of data. It also keeps track of changes and can do a one-row UPDATE when told to. The API it presents is completely devoid of SQL: the object has fields you ->get() and ->set(), and you can ->save() when you're done.

The "collection" object knows how to get a filtered list of rows of a particular object and knows how to turn that into individual objects as appropriate, each of which function just like they were individually instantiated. (The collection object also supports being an object itself, but I rarely use this feature as it currently has some implementation limitations. And it's a concept I've found most programmers have trouble grasping.)

Most objects need only the core inheritance code and the declarations for their specific table.

The end effect is that the controller code gets to do obvious things like this:

$ticket = Ticket::Create($ticket_id);
$ticket->set('queue', $new_queue);
$ticket->set('queue_changed', date('Y-m-d H:i:s'));
$ticket->save();

... instead of something opaque like:

TicketModel::ChangeTicketQueue($ticket_id, $new_queue);

This approach also lets you code things in the Ticket object that might update other fields or other objects when the queue field changes, and it will always occur when you change that field, instead of having to remember to do it in every function that can change a ticket's queue. It also means you can add other methods to a Ticket: it would probably make sense for $ticket->get_queue() to do return TicketQueue::Create($this->get('queue')); for you. Object oriented programming at it's finest! :-)

So, to finally answer your question, I would recommend none of the approaches in your question. Your model objects should present a unified API to the controller code and it is up to them as to how they store their data. If it is in a database, then they need to organise their own calls to do that. But a model isn't a datastore: however, it may well have one.

staticsan
Are you suggesting something like "Model communicates with DAO"? (I added a new example to my original question to reflext this.)
etheros
Yes, that would be a good description. However, be careful of what you mean by 'business logic': most code I've seen that would qualify as 'business logic' belongs in the controller, not the model.
staticsan
A: 

I ended up decorating my model with a datastore class:

//datastore class
public function __call($name, $arguments)
{

 if (!method_exists($this->model, $name))
  throw new Exception('does not exist!');

 return call_user_func_array(array($this->model, $name), $arguments);

}

//controller - decorating
$model = new Model;
$datastore = new Datastore($model);
$datastore->actions();
etheros
A: 

RedBeanPHP uses a system called Fuse to solve this problem. (details on wiki: http://redbeanphp.com/community/wiki/index.php/Fuse )

//Controller
$foo = R::load("foo", 9);
$foo->bar = "bar";
R::store( $foo );

Here you model is actually nothing more than a bean. However the system fuses the bean with a full fledged model on demand.

class Model_Foo extends RedBean_SimpleModel {
  public function update() {
    if ($this->bar!="bar") do something... 
  }
}

The framework binds Model_Foo to bean of type "foo" on the fly and calls the update() method just before saving. This is where you can store the business rules. Besides update(), you can also use open() and delete().

Gabor de Mooij