From 7fbb919ce824ba3b80e0c294c90c9b42d048bdca Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sun, 1 May 2011 17:33:02 +1200 Subject: [PATCH] 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. --- api/VersionedRestfulServer.php | 5 ++-- cli-script.php | 3 ++- control/Controller.php | 5 ++-- control/Director.php | 11 ++++---- control/RequestHandler.php | 20 ++++++++++++-- dev/DevelopmentAdmin.php | 4 +-- dev/SapphireTest.php | 7 ++++- dev/YamlFixture.php | 8 +++--- main.php | 4 ++- model/DataList.php | 24 +++++++++++++++-- model/DataModel.php | 49 ++++++++++++++++++++++++++++++++++ model/DataObject.php | 44 +++++++++++++++++++++++------- security/Security.php | 1 + 13 files changed, 153 insertions(+), 32 deletions(-) create mode 100644 model/DataModel.php diff --git a/api/VersionedRestfulServer.php b/api/VersionedRestfulServer.php index b40413f7c..a2fbf6584 100644 --- a/api/VersionedRestfulServer.php +++ b/api/VersionedRestfulServer.php @@ -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; } } diff --git a/cli-script.php b/cli-script.php index 74dd97b46..a65baaa90 100755 --- a/cli-script.php +++ b/cli-script.php @@ -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()); ?> diff --git a/control/Controller.php b/control/Controller.php index 4f959bd87..eff8c624f 100755 --- a/control/Controller.php +++ b/control/Controller.php @@ -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; diff --git a/control/Director.php b/control/Director.php index 472152feb..6c8c8f593 100755 --- a/control/Director.php +++ b/control/Director.php @@ -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(); } diff --git a/control/RequestHandler.php b/control/RequestHandler.php index c8af381c2..2309a04d4 100755 --- a/control/RequestHandler.php +++ b/control/RequestHandler.php @@ -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); diff --git a/dev/DevelopmentAdmin.php b/dev/DevelopmentAdmin.php index f40b8a5c5..e3edecb76 100644 --- a/dev/DevelopmentAdmin.php +++ b/dev/DevelopmentAdmin.php @@ -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 "

Database is building.... Check below for any errors

Database has been built successfully

"; $da = Object::create('DatabaseAdmin'); - return $da->handleRequest($request); + return $da->handleRequest($request, $this->model); echo ""; $renderer->writeFooter(); diff --git a/dev/SapphireTest.php b/dev/SapphireTest.php index 3cabf2590..c35a12161 100755 --- a/dev/SapphireTest.php +++ b/dev/SapphireTest.php @@ -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 diff --git a/dev/YamlFixture.php b/dev/YamlFixture.php index 0a969e56f..abd207dbf 100644 --- a/dev/YamlFixture.php +++ b/dev/YamlFixture.php @@ -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 diff --git a/main.php b/main.php index ac6f9809f..a5bd3801b 100644 --- a/main.php +++ b/main.php @@ -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'); diff --git a/model/DataList.php b/model/DataList.php index c94250add..3f06c29b6 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -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 diff --git a/model/DataModel.php b/model/DataModel.php new file mode 100644 index 000000000..55bf74c15 --- /dev/null +++ b/model/DataModel.php @@ -0,0 +1,49 @@ +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; + } + +} \ No newline at end of file diff --git a/model/DataObject.php b/model/DataObject.php index 8da2175fd..e1b9aff55 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -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"); diff --git a/security/Security.php b/security/Security.php index b0019d8ef..afc8662be 100644 --- a/security/Security.php +++ b/security/Security.php @@ -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 {