API CHANGE: Introduce DataModel object, as a representation of the project's entire data model, and tie it to $this->model an all DataObjects, Controllers, and RequestHandlers for easy non-static access.

API CHANGE: Add DataList::newObject(), which creates a new object on that DataList.
API CHANGE: RequestHandler::handleRequest() now needs to handle a $model argument, if you override it.
This commit is contained in:
Sam Minnee 2011-05-01 17:33:02 +12:00
parent 2f79961988
commit 7fbb919ce8
13 changed files with 153 additions and 32 deletions

View File

@ -11,10 +11,11 @@ class VersionedRestfulServer extends Controller {
'index'
);
function handleRequest($request) {
function handleRequest($request, $model) {
$this->setModel($model);
Versioned::reading_stage('Live');
$restfulserver = new RestfulServer();
$response = $restfulserver->handleRequest($request);
$response = $restfulserver->handleRequest($request, $model);
return $response;
}
}

View File

@ -80,6 +80,7 @@ if(!$url) {
$_SERVER['REQUEST_URI'] = BASE_URL . '/' . $url;
// Direct away - this is the "main" function, that hands control to the apporopriate controller
Director::direct($url);
DataModel::set_inst(new DataModel());
Director::direct($url, DataModel::inst());
?>

View File

@ -115,13 +115,14 @@ class Controller extends RequestHandler {
* @return SS_HTTPResponse The response that this controller produces,
* including HTTP headers such as redirection info
*/
function handleRequest(SS_HTTPRequest $request) {
function handleRequest(SS_HTTPRequest $request, DataModel $model) {
if(!$request) user_error("Controller::handleRequest() not passed a request!", E_USER_ERROR);
$this->pushCurrent();
$this->urlParams = $request->allParams();
$this->request = $request;
$this->response = new SS_HTTPResponse();
$this->setModel($model);
$this->extend('onBeforeInit');
@ -138,7 +139,7 @@ class Controller extends RequestHandler {
return $this->response;
}
$body = parent::handleRequest($request);
$body = parent::handleRequest($request, $model);
if($body instanceof SS_HTTPResponse) {
if(isset($_REQUEST['debug_request'])) Debug::message("Request handler returned SS_HTTPResponse object to $this->class controller; returning it without modification.");
$this->response = $body;

View File

@ -74,7 +74,7 @@ class Director {
* @uses handleRequest() rule-lookup logic is handled by this.
* @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call.
*/
static function direct($url) {
static function direct($url, DataModel $model) {
// Validate $_FILES array before merging it with $_POST
foreach($_FILES as $k => $v) {
if(is_array($v['tmp_name'])) {
@ -107,7 +107,7 @@ class Director {
// Load the session into the controller
$session = new Session(isset($_SESSION) ? $_SESSION : null);
$result = Director::handleRequest($req, $session);
$result = Director::handleRequest($req, $session, $model);
$session->inst_save();
// Return code for a redirection request
@ -206,7 +206,8 @@ class Director {
$req = new SS_HTTPRequest($httpMethod, $url, $getVars, $postVars, $body);
if($headers) foreach($headers as $k => $v) $req->addHeader($k, $v);
$result = Director::handleRequest($req, $session);
// TODO: Pass in the DataModel
$result = Director::handleRequest($req, $session, DataModel::inst());
// Restore the superglobals
$_REQUEST = $existingRequestVars;
@ -231,7 +232,7 @@ class Director {
*
* @return SS_HTTPResponse|string
*/
protected static function handleRequest(SS_HTTPRequest $request, Session $session) {
protected static function handleRequest(SS_HTTPRequest $request, Session $session, DataModel $model) {
krsort(Director::$rules);
if(isset($_REQUEST['debug'])) Debug::show(Director::$rules);
@ -264,7 +265,7 @@ class Director {
$controllerObj->setSession($session);
try {
$result = $controllerObj->handleRequest($request);
$result = $controllerObj->handleRequest($request, $model);
} catch(SS_HTTPResponse_Exception $responseException) {
$result = $responseException->getResponse();
}

View File

@ -36,6 +36,11 @@ class RequestHandler extends ViewableData {
*/
protected $request = null;
/**
* The DataModel for this request
*/
protected $model = null;
/**
* This variable records whether RequestHandler::__construct()
* was called or not. Useful for checking if subclasses have
@ -86,9 +91,19 @@ class RequestHandler extends ViewableData {
// Check necessary to avoid class conflicts before manifest is rebuilt
if(class_exists('NullHTTPRequest')) $this->request = new NullHTTPRequest();
// This will prevent bugs if setModel() isn't called.
$this->model = DataModel::inst();
parent::__construct();
}
/**
* Set the DataModel for this request.
*/
public function setModel($model) {
$this->model = $model;
}
/**
* Handles URL requests.
*
@ -110,7 +125,7 @@ class RequestHandler extends ViewableData {
* @uses SS_HTTPRequest->match()
* @return SS_HTTPResponse|RequestHandler|string|array
*/
function handleRequest(SS_HTTPRequest $request) {
function handleRequest(SS_HTTPRequest $request, DataModel $model) {
// $handlerClass is used to step up the class hierarchy to implement url_handlers inheritance
$handlerClass = ($this->class) ? $this->class : get_class($this);
@ -119,6 +134,7 @@ class RequestHandler extends ViewableData {
}
$this->request = $request;
$this->setModel($model);
// We stop after RequestHandler; in other words, at ViewableData
while($handlerClass && $handlerClass != 'ViewableData') {
@ -164,7 +180,7 @@ class RequestHandler extends ViewableData {
// to prevent infinite loops. Also prevent further handling of controller actions which return themselves
// to avoid infinite loops.
if($this !== $result && !$request->isEmptyPattern($rule) && is_object($result) && $result instanceof RequestHandler) {
$returnValue = $result->handleRequest($request);
$returnValue = $result->handleRequest($request, $model);
// Array results can be used to handle
if(is_array($returnValue)) $returnValue = $this->customise($returnValue);

View File

@ -134,7 +134,7 @@ class DevelopmentAdmin extends Controller {
function build($request) {
if(Director::is_cli()) {
$da = Object::create('DatabaseAdmin');
return $da->handleRequest($request);
return $da->handleRequest($request, $this->model);
} else {
$renderer = Object::create('DebugView');
$renderer->writeHeader();
@ -143,7 +143,7 @@ class DevelopmentAdmin extends Controller {
echo "<div class=\"status pending\"><h2 class='buildProgress'>Database is building.... Check below for any errors</h2><h2 class='buildCompleted'>Database has been built successfully</h2></div>";
$da = Object::create('DatabaseAdmin');
return $da->handleRequest($request);
return $da->handleRequest($request, $this->model);
echo "</div>";
$renderer->writeFooter();

View File

@ -112,6 +112,8 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
*/
protected $fixtures;
protected $model;
function setUp() {
// Mark test as being run
$this->originalIsRunningTest = self::$is_running_test;
@ -146,6 +148,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$className = get_class($this);
$fixtureFile = eval("return {$className}::\$fixture_file;");
$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
// Todo: this could be a special test model
$this->model = DataModel::inst();
// Set up fixture
if($fixtureFile || $this->usesDatabase || !self::using_temp_db()) {
@ -180,7 +185,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
}
$fixture = new YamlFixture($fixtureFilePath);
$fixture->saveIntoDatabase();
$fixture->saveIntoDatabase($this->model);
$this->fixtures[] = $fixture;
// backwards compatibility: Load first fixture into $this->fixture

View File

@ -154,7 +154,7 @@ class YamlFixture extends Object {
* the record is written twice: first after populating all non-relational fields,
* then again after populating all relations (has_one, has_many, many_many).
*/
public function saveIntoDatabase() {
public function saveIntoDatabase(DataModel $model) {
// We have to disable validation while we import the fixtures, as the order in
// which they are imported doesnt guarantee valid relations until after the
// import is complete.
@ -167,7 +167,7 @@ class YamlFixture extends Object {
$this->fixtureDictionary = array();
foreach($fixtureContent as $dataClass => $items) {
if(ClassInfo::exists($dataClass)) {
$this->writeDataObject($dataClass, $items);
$this->writeDataObject($model, $dataClass, $items);
} else {
$this->writeSQL($dataClass, $items);
}
@ -182,9 +182,9 @@ class YamlFixture extends Object {
* @param string $dataClass
* @param array $items
*/
protected function writeDataObject($dataClass, $items) {
protected function writeDataObject($model, $dataClass, $items) {
foreach($items as $identifier => $fields) {
$obj = new $dataClass();
$obj = $model->$dataClass->newObject();
// If an ID is explicitly passed, then we'll sort out the initial write straight away
// This is just in case field setters triggered by the population code in the next block

View File

@ -123,8 +123,10 @@ if (isset($_GET['debug_profile'])) Profiler::unmark('DB::connect');
if (isset($_GET['debug_profile'])) Profiler::unmark('main.php init');
// Direct away - this is the "main" function, that hands control to the appropriate controller
Director::direct($url);
DataModel::set_inst(new DataModel());
Director::direct($url, DataModel::inst());
if (isset($_GET['debug_profile'])) {
Profiler::unmark('all_execution');

View File

@ -15,6 +15,11 @@ class DataList extends DataObjectSet {
*/
protected $dataQuery;
/**
* The DataModel from which this DataList comes.
*/
protected $model;
/**
* Synonym of the constructor. Can be chained with literate methods.
* DataList::create("SiteTree")->sort("Title") is legal, but
@ -35,6 +40,10 @@ class DataList extends DataObjectSet {
parent::__construct();
}
public function setModel(DataModel $model) {
$this->model = $model;
}
public function dataClass() {
return $this->dataClass;
}
@ -141,8 +150,10 @@ class DataList extends DataObjectSet {
if(empty($row['RecordClassName'])) $row['RecordClassName'] = $row['ClassName'];
// Instantiate the class mentioned in RecordClassName only if it exists, otherwise default to $this->dataClass
if(class_exists($row['RecordClassName'])) return new $row['RecordClassName']($row);
else return new $defaultClass($row);
if(class_exists($row['RecordClassName'])) $item = new $row['RecordClassName']($row, false, $this->model);
else $item = new $defaultClass($row, false, $this->model);
return $item;
}
/**
@ -362,6 +373,15 @@ class DataList extends DataObjectSet {
// Nothing needs to happen by default
// TO DO: If a filter is given to this data list then
}
/**
* Return a new item to add to this DataList.
* @todo This doesn't factor in filters.
*/
function newObject($initialFields = null) {
$class = $this->dataClass;
return new $class($initialFields, false, $this->model);
}
function remove($item) {
// TO DO: Allow for amendment of this behaviour - for exmaple, we can remove an item from

49
model/DataModel.php Normal file
View File

@ -0,0 +1,49 @@
<?php
/**
* Representation of a DataModel - a collection of DataLists for each different data type.
*
* Usage:
*
* $model = new DataModel;
* $mainMenu = $model->SiteTree->where('"ParentID" = 0 AND "ShowInMenus" = 1');
*/
class DataModel {
protected static $inst;
/**
* Get the global DataModel.
*/
static function inst() {
if(!self::$inst) self::$inst = new self;
return self::$inst;
}
/**
* Set the global DataModel, used when data is requested from static methods.
*/
static function set_inst(DataModel $inst) {
self::$inst = $inst;
}
////////////////////////////////////////////////////////////////////////
protected $customDataLists = array();
function __get($class) {
if(isset($this->customDataLists[$class])) {
return clone $this->customDataLists[$class];
} else {
$list = DataList::create($class);
$list->setModel($this);
return $list;
}
}
function __set($class, $item) {
$item = clone $item;
$item->setModel($this);
$this->customDataLists[$class] = $item;
}
}

View File

@ -96,6 +96,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public $destroyed = false;
/**
* The DataModel from this this object comes
*/
protected $model;
/**
* Data stored in this objects database record. An array indexed by fieldname.
*
@ -289,7 +294,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods. Singletons
* don't have their defaults set.
*/
function __construct($record = null, $isSingleton = false) {
function __construct($record = null, $isSingleton = false, $model = null) {
// Set the fields data.
if(!$record) {
$record = array(
@ -374,6 +379,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// prevent populateDefaults() and setField() from marking overwritten defaults as changed
$this->changed = array();
$this->model = $model ? $model : DataModel::inst();
}
/**
* Set the DataModel
*/
function setModel(DataModel $model) {
$this->model = $model;
}
/**
@ -399,7 +413,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
function duplicate($doWrite = true) {
$className = $this->class;
$clone = new $className( $this->record );
$clone = new $className( $this->record, false, $this->model );
$clone->ID = 0;
if($doWrite) {
@ -497,7 +511,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
'ClassName' => $originalClass,
'RecordClassName' => $originalClass,
)
));
), false, $this->model);
if($newClassName != $originalClass) {
$newInstance->setClassName($newClassName);
@ -1199,7 +1213,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// - move the details of the delete code in the DataQuery system
// - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
// obviously, that means getting requireTable() to configure cascading deletes ;-)
$srcQuery = DataList::create($this->class)->where("ID = $this->ID")->dataQuery()->query();
$srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query();
foreach($srcQuery->queriedTables() as $table) {
$query = new SQLQuery("*", array('"'.$table.'"'));
$query->where("\"ID\" = $this->ID");
@ -1274,11 +1288,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$joinID = $this->getField($joinField);
if($joinID) {
$component = DataObject::get_by_id($class, $joinID);
$component = $this->model->$class->byID($joinID);
}
if(!isset($component) || !$component) {
$component = new $class();
$component = $this->model->$class->newObject();
}
} elseif($class = $this->belongs_to($componentName)) {
$joinField = $this->getRemoteJoinField($componentName, 'belongs_to');
@ -1289,7 +1303,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
if(!isset($component) || !$component) {
$component = new $class();
$component = $this->model->$class->newObject();
$component->$joinField = $this->ID;
}
} else {
@ -1327,6 +1341,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$joinField = $this->getRemoteJoinField($componentName, 'has_many');
$result = new HasManyList($componentClass, $joinField);
if($this->model) $result->setModel($this->model);
if($this->ID) $result->setForeignID($this->ID);
$result = $result->where($filter)->limit($limit)->sort($sort)->join($join);
@ -1414,6 +1429,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$result = new ManyManyList($componentClass, $table, $componentField, $parentField,
$this->many_many_extraFields($componentName));
if($this->model) $result->setModel($this->model);
// If this is called on a singleton, then we return an 'orphaned relation' that can have the
// foreignID set elsewhere.
@ -2488,6 +2504,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Todo: Make the $containerClass method redundant
if($containerClass != "DataList") user_error("The DataObject::get() \$containerClass argument has been deprecated", E_USER_NOTICE);
$result = DataList::create($callerClass)->where($filter)->sort($sort)->join($join)->limit($limit);
$result->setModel(DataModel::inst());
return $result;
}
@ -2495,9 +2512,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @deprecated
*/
public function Aggregate($class = null) {
if($class) return new DataList($class);
else if(isset($this)) return new DataList(get_class($this));
if($class) {
$list = new DataList($class);
$list->setModel(DataModel::inst());
} else if(isset($this)) {
$list = new DataList(get_class($this));
$list->setModel($this->model);
}
else throw new InvalidArgumentException("DataObject::aggregate() must be called as an instance method or passed a classname");
return $list;
}
/**
@ -2596,6 +2619,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
if(!$cache || !isset(DataObject::$cache_get_one[$callerClass][$cacheKey])) {
$dl = DataList::create($callerClass)->where($filter)->sort($orderby);
$dl->setModel(DataModel::inst());
$item = $dl->First();
if($cache) {
@ -2794,7 +2818,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(!$hasData) {
$className = $this->class;
foreach($defaultRecords as $record) {
$obj = new $className($record);
$obj = $this->model->$className->newObject($record);
$obj->write();
}
DB::alteration_message("Added default records to $className table","created");

View File

@ -349,6 +349,7 @@ class Security extends Controller {
$tmpPage->ID = -1 * rand(1,10000000);
$controller = new Page_Controller($tmpPage);
$controller->setModel($this->model);
$controller->init();
//Controller::$currentController = $controller;
} else {