That is the concept of the "mapper" pattern. More specifically "data mapper" which is an evolution of the mapper pattern. The mapper is "that place". This is what differentiates the pattern from Active Record. Its better from a testability standpoint because when you goto "this room" you need not worry about what strategy it using (database vs csv or other), you simply just need to know that you have a 'finder' interface that accepts whatever argumentsVladSun wrote: Now ...
You want to visit people having names starting with 'V' ... Apparently, you can't just visit a place (it doesn't exist) where there is a bunch of people with names starting with 'V'.
So, what I'm trying to do is to build another building with rooms for every group of people whose names start with a particular letter. I have to move the people to that building and then I can visit the people in a room and meet them face to face
MVC & Pagination, Filtering, Ordering, etc.
Moderator: General Moderators
Re: MVC & Pagination, Filtering, Ordering, etc.
Re: MVC & Pagination, Filtering, Ordering, etc.
Can you please show me the simplified M-V_C files (and optional domain object files) for my "story"?
There are 10 types of people in this world, those who understand binary and those who don't
- Christopher
- Site Administrator
- Posts: 13596
- Joined: Wed Aug 25, 2004 7:54 pm
- Location: New York, NY, US
Re: MVC & Pagination, Filtering, Ordering, etc.
I think you have your metaphors a little mixed here. It sounds like the Domain object here is a "building full of people" which is really just a People object that know about a specific set of people (in that building).VladSun wrote:1) Imagine a building full of people. If you want to visit them you just go to that building and there they are.
2) Imagine a building full of people. There are some rooms in it. If you want to visit people in a room you just go to that building, then go to the room and there they are.
3) Imagine a building full of people. There are some levels and some rooms on every level. If you want to visit people in a room you just go to that building, go to the level, then go to the room and there they are.
Because things exist in the real world does not mean they will be modeled in the Domain. For example, in most instances "building", "level", "room" would be attributes of a Person, not modeled themselves. And "visiting" is not really natural. The reasons that the receptionist would want to find people are different than why a manager or HR would want to find people. Most of those uses don't entail a "visit."VladSun wrote:I think this story can be mapped into MVC as:
1) "building", "level", "room" and "people" are business domain objects. They all exist in the real world. It's all very natural.
2) "visiting" is a "natural" finder method.
3) finally, you will meet them face to face - thats for the "View".
Again, why "visit" is not that appropriate a metaphor. You might want to print a phone list of all the names starting with 'V', or schedule meetings with them, or fire them.VladSun wrote:You want to visit people having names starting with 'V' ... Apparently, you can't just visit a place (it doesn't exist) where there is a bunch of people with names starting with 'V'.
This is where you go off the rails. There are times when you need to combine Models, extend Model classes to add additional functionality, etc.. Better to give your Domain object all the functionality needed. So your People class would have a findByFirstLetterOfLastName($letter) method.VladSun wrote:So, what I'm trying to do is to build another building with rooms for every group of people whose names start with a particular letter. I have to move the people to that building and then I can visit the people in a room and meet them face to face.
I think that while the first building is a "natural" one, the second building is a "surrogate" one. Also, you can't have (in real life) a building that have both types of rooms, because you can't have the people be there at the same time. While I can build many types of "surrogate" buildings I can have a single "natural" building build.
(#10850)
Re: MVC & Pagination, Filtering, Ordering, etc.
ControllerVladSun wrote:Can you please show me the simplified M-V_C files (and optional domain object files) for my "story"?
Code: Select all
function browsegreenpeopleAction()
{
$finder = $this->getFinder();
/** @var Shuffler_Collection of Person */
$people = $finder->findGreen();
$this->view->people = $people;
}
Code: Select all
/** @return Shuffler_Collection of Person */
function findGreen( $page)
{
$select = $this->select();
$select->order('age');
$select->limit(10 *$page);
return $this->query($select);
}
In an application I might have
tables
employer
employer_awards
employer_demerits
and packages
Employer
Award
Demerit
I can name my database tables how I want, to most effectively communicate to other's that the awards are owned by the employer, while having an object schema that doesn't couple the concepts of employers & rewards
Furthermore database gurus like you will definitely fall in love w/ the data mapper pattern because if you a dogmatic about making explicit finder methods, you can always go in and put in hand tuned SQL later on, without disturbing the software that depends on that finder. TO para-phrase Martin Fowler "it doesnt treat the database like your crazy aunt locked up in the attic", it "acknowledges that the storage mechanism(s) exist and is something we need to deal with".
Here is another overloading of a mapper I am working on in an app
Code: Select all
/** @return Change */
protected function doLoad( $id, $rs )
{
$change = $this->createModel( $id, $rs );
$mapper = $this->getMapper( ucfirst($rs['change_entity_type']) );
$entity = $mapper->find( $rs['change_entity_id'] );
$change->setEntity( $entity );
$change->setBefore( $rs['change_before'] );
$change->setAfter( $rs['change_after'] );
$change->setVotes( $rs['change_votes'] );
if( $rs['change_applied'] )
{
$change->apply();
}
$change = $this->vote_mapping->load( $change, $rs ); // I also have the concept of mappings now, which encapsulates re-usable custom loading/saving logic
return $change;
}
Re: MVC & Pagination, Filtering, Ordering, etc.
I know I am a little bit cheeky but may I post my current Doctrine-powered MVC and you tell me what's wrong with my current approach (by theory) and what would you suggest to change, please 
In general: I use Doctrine. I want to have a generic CRUD Controller/Model classes that will cover 90% of the CRUD functionality I need.
My requests/replies are in JSON format - POST only.
So ...
Base CRUD controller class:
* getRemapping() method will in fact call get() method when URLs like /controller/get* is requested.
An inheritance:
Base CRUD Model class
* API: getByMunicipality(), add(), save(), remove()
An inheritance:
My "decorator" base class
A Pagination (it has all of the data-grid functionality, so it's not just a pagination. Wrong name I guess ):
I know it's a lot of code, but please let me been excused 
I just want to have a clear view of what I'm doing wrong once and for all
EDIT: Wrong version, updated.
In general: I use Doctrine. I want to have a generic CRUD Controller/Model classes that will cover 90% of the CRUD functionality I need.
My requests/replies are in JSON format - POST only.
So ...
Base CRUD controller class:
Code: Select all
<?php/**
* @property CRUD_Model _model
*/
abstract class CRUD_Controller extends Auth_Controller
{
protected $dataPostName = 'response';
protected $_model = null;
function __construct($model = null)
{
parent::__construct();
$this->setModel($model);
}
/**
* Add an object to the Model
*
* @access public
* @return bool
*/
public function add()
{
if ($data = $this->validateInput())
foreach ($this->getModel()->add($data) as $key => $value)
RowResponse::register($key, $value);
$this->respond();
}
/**
* Add an object to the Model
*
* @access public
* @return bool
*/
public function save()
{
if ($data = $this->validateInput())
foreach ($this->getModel()->save($data) as $key => $value)
RowResponse::register($key, $value);
$this->respond();
}
/**
* Remove an object from the Model
*
* @access public
* @return bool
*/
public function remove()
{
if ($data = $this->validateInput())
foreach ($this->getModel()->remove($data) as $key => $value)
RowResponse::register($key, $value);
$this->respond();
}
/**
* Update an object in the Model
*
* @access public
* @return bool
*/
public function update()
{
if ($data = $this->validateInput())
foreach ($this->getModel()->update($data) as $key => $value)
RowResponse::register($key, $value);
$this->respond();
}
/**
* Get Model data
*
* @access public
* @return bool
*/
protected function get($method = 'get', $conf = null)
{
$this->getModel()->{'set'.ucfirst($method).'Dql'}($conf);
foreach ($this->getModel()->get() as $key => $value)
RowResponse::register($key, $value);
$this->respond();
}
/**
* Set the Model class
*
* @access protected
*/
protected function setModel($model)
{
if (!empty($model))
{
$this->_model = $model;
$this->_model->_assign_libraries();
}
}
/**
* Get the Model
*
* @access protected
* @return DB_List_Model
*/
protected function getModel()
{
if (!$this->_model)
$this->defaultInit();
return $this->_model;
}
/**
* Set the default Model class
*
* @access private
*/
private function defaultInit()
{
$class = get_class($this).'_CRUD_Model';
require_once(APPPATH.'/models/'.strtolower($class).EXT);
$this->setModel(new $class);
}
/**
* Validate and transform JSON input data
*
* @access private
* @return stdClass|false
*/
protected function validateInput()
{
if (!$data = json_decode($this->input->post($this->dataPostName), true))
{
Error::register('???????? ?????? ?? ???????.');
return false;
}
return $data;
}
/**
* Load View for ExtJS response
*
* @access protected
*/
protected function respond()
{
$this->view('response');
}
protected function getRemapping($method, $arguments)
{
$remapping = parent::getRemapping($method, $arguments);
if (strpos($remapping->method, 'get') === 0)
{
$remapping->arguments = array($remapping->method, $_POST);
$remapping->method = 'get';
}
return $remapping;
}
}An inheritance:
Code: Select all
class City extends CRUD_Controller
{
function __construct()
{
parent::__construct();
$this->setModel(new Pagination(new City_CRUD_Model()));
}
}Code: Select all
/**
* @property string $tableName
* @property Doctrine_Collection $collection
* @property Doctrine_Record $record
* @property Doctrine_Table $table
* @property Doctrine_Query $currentQuery
* @property String $tableAlias
*
*/
class CRUD_Model extends Model
{
protected $recordClass = null;
protected $table = null;
protected $collection = null;
protected $record = null;
protected $currentQuery = null;
protected $tableAlias = '_tempTable';
public function __construct($recordClass = null)
{
parent::__construct();
$this->setRecordClass($recordClass);
$this->setGetDql();
}
public function getCurrentQuery()
{
return $this->currentQuery;
}
public function getTableAlias()
{
return $this->tableAlias;
}
/**
* Load the collection of objects.
*
* @access public
* @return bool
*/
public function setGetDql()
{
$this->currentQuery = $this->getTable()->createQuery($this->tableAlias);
}
/**
* Load the collection of objects.
*
* @access public
* @return bool
*/
public function load()
{
$this->collection = $this->getCurrentQuery()->execute();
}
/**
* Return the object collection
*
* @access public
* @return Array
*/
public function get()
{
if (empty($this->collection))
$this->load();
return array
(
'response' => $this->collection->toArray(true)
);
}
/**
* Add an object to the collection.
*
* @access public
* @param mixed $data
* @return bool
*/
public function add($data)
{
try
{
$this->record = $this->createFromObject($data, false);
$this->record->save();
}
catch (Exception $E)
{
Error::register($E->getMessage());
return array
(
'response' => new stdClass()
);
}
return array
(
'response' => $this->record->toArray(true)
);
}
/**
* Update the object in the collection.
*
* @access public
* @return bool
*/
public function update($data)
{
try
{
$this->record = $this->createFromObject($data);
$this->record->save();
}
catch (Exception $E)
{
Error::register($E->getMessage());
return array
(
'response' => new stdClass()
);
}
return array
(
'response' => $this->record->toArray(true)
);
}
/**
* Save the object in the collection.
*
* @access public
* @return bool
*/
public function save($data)
{
try
{
$this->record = $this->createFromObject($data);
$this->record->save();
}
catch (Exception $E)
{
Error::register($E->getMessage());
return array
(
'response' => new stdClass()
);
}
return array
(
'response' => $this->record->toArray(true)
);
}
/**
* Remove an object from the collection
*
* @access public
* @return bool
*/
public function remove($data)
{
try
{
$this->record = $this->createFromObject($data);
$this->record->delete();
}
catch (Exception $E)
{
Error::register($E->getMessage());
}
return array
(
'response' => $this->record->toArray(true)
);
}
public function setRecordClass($recordClass)
{
if (!empty($recordClass))
{
$this->recordClass = $recordClass;
$this->table = Doctrine_Manager::connection()->getTable($this->recordClass);
}
}
/**
* Return the ORM table
*
* @access public
* @return Doctrine_Table
*/
public function getTable()
{
if (!$this->table)
$this->defaultInit();
return $this->table;
}
private function defaultInit()
{
$this->setRecordClass(substr('R'.get_class($this), 0, -strlen('_CRUD_Model')));
}
protected function createFromObject($data, $assignID = true)
{
$record = $this->getTable()->create();
if ($assignID)
if ($ids = array_intersect_key($data, array_flip($this->getTable()->getIdentifierColumnNames())))
$record->assignIdentifier($ids);
$record->synchronizeWithArray($data, true);
return $record;
}
}An inheritance:
Code: Select all
class City_CRUD_Model extends CRUD_Model
{
public function setGetByMunicipalityDql($conf = null)
{
$this->currentQuery = $this->getTable()
->createQuery($this->tableAlias)
->andWhere('municipality_id = ?', isset($conf['municipality_id']) ? $conf['municipality_id'] : 0);
}
}Code: Select all
/**
* @property CRUD_Model $model
* @property mixed $conf
*
*/
abstract class Facade_CRUD_Model extends CRUD_Model
{
protected $model = null;
protected $conf = null;
public function __construct($model, $conf = null)
{
$this->model = $model;
$this->conf = $conf;
$this->_assign_libraries();
}
protected function responseObject()
{
return new stdClass();
}
public function getCurrentQuery()
{
return $this->currentQuery;
}
public final function getTableAlias()
{
return $this->model->getTableAlias();
}
public function load()
{
$this->collection = $this->currentQuery->execute();
}
public function get()
{
return $this->model->get();
}
public function add($data)
{
return $this->model->add($data);
}
public function update($data)
{
return $this->model->update($data);
}
public function save($data)
{
return $this->model->save($data);
}
public function remove($data)
{
return $this->model->remove($data);
}
public function setRecordClass($recordClass)
{
$this->model->setRecordClass($recordClass);
}
public function getTable()
{
return $this->model->getTable();
}
private function defaultInit()
{
$this->model->defaultInit();
}
public function __call($method, $arguments)
{
return call_user_func_array(array($this->model, $method), $arguments);
}
}Code: Select all
/**
* Pagination facade class
*
* This class is to be used in Model for pagination, ordering and filtering purposes.
*
* @package MyMoto
* @subpackage Libraries
* @category Libraries
* @author VladSun
*
* @property string $paramStartPage
* @property string $paramLimitPage
* @property string $paramSortField
* @property string $paramSortDir
* @property string $paramStartPage
* @property string $paramFilter
* @property string $paramFilter
* @property string $totalField
*
* @property CRUD_Model $model
*/
class Pagination extends Facade_CRUD_Model
{
public $paramStartPage = 'start';
public $paramLimitPage = 'limit';
public $paramSortField = 'sort';
public $paramSortDir = 'dir';
public $paramFilter = 'filter';
public $totalField = 'total';
public $responseField = 'response';
private $startPageValue = 0;
private $limitPageValue = 0;
private $sortFieldValue = '';
private $sortDirValue = '';
private $filterValue = array();
private $totalCount = 0;
public function __construct($model, $conf = null)
{
parent::__construct($model, $conf);
}
public function getCurrentQuery()
{
$this->currentQuery = $this->model->getCurrentQuery();
$this->apply();
return $this->currentQuery;
}
public function load($params = null)
{
if (!$this->currentQuery)
$this->getCurrentQuery();
$this->collection = $this->currentQuery->execute();
}
public function get()
{
if (empty($this->collection))
$this->load();
$o = $this->responseObject();
$o->{$this->responseField} = $this->currentQuery->execute()->toArray();
$o->{$this->totalField} = $this->totalCount;
return $o;
}
protected function apply()
{
$this->setParameterValues($_POST);
if ($this->filter() === false)
Error::register('?????? ??? ???????????? ?? ???????.');
elseif ($this->orderBy() === false)
Error::register('?????? ??? ??????????? ?? ???????.');
elseif ($this->limit() === false)
Error::register('?????? ??? ?????????????? ?? ???????.');
else
{
Warning::register($this->currentQuery->getSql());
$this->totalCount = $this->currentQuery->count();
}
return null;
}
private function orderBy()
{
if (empty($this->sortFieldValue) || empty($this->sortDirValue))
return true;
if (strtoupper($this->sortDirValue) != 'ASC' && strtoupper($this->sortDirValue) != 'DESC')
return false;
if (!$this->model->getTable()->hasField($this->sortFieldValue))
return false;
$this->currentQuery = $this->currentQuery->orderBy($this->model->getTableAlias().'.'.$this->sortFieldValue.' '.$this->sortDirValue);
return true;
}
private function limit()
{
if (!empty($this->limitPageValue))
$this->currentQuery = $this->currentQuery->limit($this->limitPageValue)->offset($this->startPageValue);
return true;
}
private function filter()
{
foreach ($this->filterValue as $filterData)
{
if (($filter = Filter_Provider::get($filterData, $this->model->getTable())) === false)
return false;
elseif ($filter->apply($this->currentQuery) === false)
return false;
}
return true;
}
public function setParameterNames($data)
{
$this->paramStartPage = $data['paramStartPage'] ? $data['paramStartPage'] : 'start';
$this->paramLimitPage = $data['paramLimitPage'] ? $data['paramLimitPage'] : 'limit';
$this->paramSortField = $data['paramSortField'] ? $data['paramSortField'] : 'sort';
$this->paramSortDir = $data['paramSortDir'] ? $data['paramSortDir'] : 'dir';
$this->paramFilter = $data['paramFilter'] ? $data['paramFilter'] : 'filter';
$this->totalField = $data['totalField'] ? $data['totalField'] : 'totalCount';
}
public function setParameterValues($data)
{
$this->startPageValue = isset($data[$this->paramStartPage]) ? intval($data[$this->paramStartPage]) : 0;
$this->limitPageValue = isset($data[$this->paramLimitPage]) ? intval($data[$this->paramLimitPage]) : 0;
$this->sortFieldValue = isset($data[$this->paramSortField]) ? $data[$this->paramSortField] : null;
$this->sortDirValue = isset($data[$this->paramSortDir]) ? $data[$this->paramSortDir] : null;
$this->filterValue = isset($data[$this->paramFilter]) ? $data[$this->paramFilter] : array();
}
}I just want to have a clear view of what I'm doing wrong once and for all
EDIT: Wrong version, updated.
There are 10 types of people in this world, those who understand binary and those who don't
Re: MVC & Pagination, Filtering, Ordering, etc.
The only issue that stands out to me (and its kinda big one, depending who you ask) is you aren't doing real "models" (In my opinion).
In my opinion CRUD is a concept that should only be present in a controller. Models should match their real world concepts as close as possible. This is a good heuristic for good design. Just like your building example, each "person" wouldn't need all this bloat if there was this "virtual room" where you could relocate it. Basically I just think you are combining separate concepts, you are treating the model, the persistence layer, and the "crud logic" as all one concept, which IMO they are each distinct.
For instance think of the real world equivalent of your code, if your code were translated literally to your building example, each and every person would know every last detail about each and every other person. Each of your models has these methods, that are in my opinion a "cardinality mismatch" (not to mention all the other objective benefits to moving those methods to a separate "finder" interface).
One way of doing this with Active Record is to make your active record objects 'behave' as data transfer objects only. So you would have a Person class and a Person_Record class, the Person_Record class would extend Doctrine_Record, the person class would be plain ol' PHP business logic. You would make the model only depend on some Person_Record_Interface. The model need not know whether it's record is just a "stub", a database aware object, or something else. This would also free you up to use inheritance properly with doctrine.
For some applications it is no big deal (ones where you have lots of fields and little behavior). if you tend to have more behaviors than 'fields', it pays to make these concepts explicit.
Kind of funny how the code is all in English except the error messages.
In my opinion CRUD is a concept that should only be present in a controller. Models should match their real world concepts as close as possible. This is a good heuristic for good design. Just like your building example, each "person" wouldn't need all this bloat if there was this "virtual room" where you could relocate it. Basically I just think you are combining separate concepts, you are treating the model, the persistence layer, and the "crud logic" as all one concept, which IMO they are each distinct.
For instance think of the real world equivalent of your code, if your code were translated literally to your building example, each and every person would know every last detail about each and every other person. Each of your models has these methods, that are in my opinion a "cardinality mismatch" (not to mention all the other objective benefits to moving those methods to a separate "finder" interface).
One way of doing this with Active Record is to make your active record objects 'behave' as data transfer objects only. So you would have a Person class and a Person_Record class, the Person_Record class would extend Doctrine_Record, the person class would be plain ol' PHP business logic. You would make the model only depend on some Person_Record_Interface. The model need not know whether it's record is just a "stub", a database aware object, or something else. This would also free you up to use inheritance properly with doctrine.
For some applications it is no big deal (ones where you have lots of fields and little behavior). if you tend to have more behaviors than 'fields', it pays to make these concepts explicit.
Kind of funny how the code is all in English except the error messages.
Re: MVC & Pagination, Filtering, Ordering, etc.
I want to have such CRUD MVC classes to serve only my client side ExtJS data grids. So, they have no "behaviors" indeed, just data. You said that "it is no big deal (ones where you have lots of fields and little behavior)", so I think it's OK, isn't it?
Anyway, I'm going to rethink it. Thanks for replying
I really appreciate it.
End users won't argue about it 
Anyway, I'm going to rethink it. Thanks for replying
josh wrote:Kind of funny how the code is all in English except the error messages.
There are 10 types of people in this world, those who understand binary and those who don't
Re: MVC & Pagination, Filtering, Ordering, etc.
Yes its "ok", if I had to work on said system I would probably rename CRUD_MOdel to Model_Collection or just Model. The rest of it is decent from an architecture standpoint. If most of what you are doing is true "CRUD" then active record works out nicely. WHere I have specifically felt benefits of data mapper is where our "fields" are not simple text fields, but rather many fields that interact and behave logically as one single field. On the persistence side of things it opens up a "can of worms" when you have that extra complexity because you might want to store the data "inline"
Take for instance a job posting form, you might have
Salary: Choose type ( dropdown) < If certain options are selected
Enter Amount < Then a text box shows up
When this sort of stuff starts happening, it pays to decouple. So then you can have the salary object without having to make a pointless salary table (other then the salary_types)
Take for instance a job posting form, you might have
Salary: Choose type ( dropdown) < If certain options are selected
Enter Amount < Then a text box shows up
When this sort of stuff starts happening, it pays to decouple. So then you can have the salary object without having to make a pointless salary table (other then the salary_types)
Re: MVC & Pagination, Filtering, Ordering, etc.
I will use the "relations" features of the Doctrine_Record - in my next "CRUD MVC" version I will be able to pass relations objects data from my ExtJS grids/stores and load relation data to the ExtJS grid/stores (single object, a collection of objects). So, if such relations exist it will be read/stored.
I am not sure if it's somehow related to your "Jobs" example, but I will be able to process existing/none-existing relations.
I am not sure if it's somehow related to your "Jobs" example, but I will be able to process existing/none-existing relations.
There are 10 types of people in this world, those who understand binary and those who don't