(merged from branches/roa. use "svn log -c <changeset> -g <module-svn-path>" for detailed commit message)

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@60204 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2008-08-09 02:16:46 +00:00
parent e25f44604f
commit 9ac464cc57
6 changed files with 243 additions and 79 deletions

View File

@ -81,7 +81,7 @@ Director::addRules(10, array(
'images/$Action/$Class/$ID/$Field' => 'Image_Uploader',
'' => 'RootURLController',
'sitemap.xml' => 'GoogleSitemap',
'api/v1/$ClassName/$ID' => 'RestfulServer',
'api/v1/$ClassName/$ID/$Relation' => 'RestfulServer',
'dev/$Action/$NestedAction' => 'DevelopmentAdmin'
));

View File

@ -42,16 +42,33 @@ class RestfulServer extends Controller {
if(!isset($this->urlParams['ClassName'])) return $this->notFound();
$className = $this->urlParams['ClassName'];
$id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null;
$relation = (isset($this->urlParams['Relation'])) ? $this->urlParams['Relation'] : null;
// This is a little clumsy and should be improved with the new TokenisedURL that's coming
if(strpos($relation,'.') !== false) list($relation, $extension) = explode('.', $relation, 2);
else if(strpos($id,'.') !== false) list($id, $extension) = explode('.', $id, 2);
else if(strpos($className,'.') !== false) list($className, $extension) = explode('.', $className, 2);
else $extension = null;
// Determine mime-type from extension
$contentMap = array(
'xml' => 'text/xml',
'json' => 'text/json',
'js' => 'text/json',
'xhtml' => 'text/html',
'html' => 'text/html',
);
$contentType = isset($contentMap[$extension]) ? $contentMap[$extension] : 'text/xml';
switch($requestMethod) {
case 'GET':
return $this->getHandler($className, $id);
return $this->getHandler($className, $id, $relation, $contentType);
case 'PUT':
return $this->putHandler($className, $id);
return $this->putHandler($className, $id, $relation, $contentType);
case 'DELETE':
return $this->deleteHandler($className, $id);
return $this->deleteHandler($className, $id, $relation, $contentType);
case 'POST':
}
@ -83,46 +100,67 @@ class RestfulServer extends Controller {
* - static $api_access must be set. This enables the API on a class by class basis
* - $obj->canView() must return true. This lets you implement record-level security
*/
protected function getHandler($className, $id) {
$obj = DataObject::get_by_id($className, $id);
if(!$obj) {
return $this->notFound();
}
// TO DO - inspect that Accept header as well. $_GET['accept'] can still be checked, as it's handy for debugging
$contentType = isset($_GET['accept']) ? $_GET['accept'] : 'text/xml';
if($obj->stat('api_access') && $obj->canView()) {
switch($contentType) {
case "text/xml":
$this->getResponse()->addHeader("Content-type", "text/xml");
return $this->dataObjectAsXML($obj);
case "text/json":
$this->getResponse()->addHeader("Content-type", "text/json");
return $this->dataObjectAsJSON($obj);
case "text/html":
case "application/xhtml+xml":
$this->getResponse()->addHeader("Content-type", "text/json");
return $this->dataObjectAsXHTML($obj);
protected function getHandler($className, $id, $relation, $contentType) {
if($id) {
$obj = DataObject::get_by_id($className, $id);
if(!$obj) {
return $this->notFound();
}
if(!$obj->stat('api_access') || !$obj->canView()) {
return $this->permissionFailure();
}
if($relation) {
if($obj->hasMethod($relation)) $obj = $obj->$relation();
else return $this->notFound();
}
} else {
return $this->permissionFailure();
$obj = DataObject::get($className, "");
if(!singleton($className)->stat('api_access')) {
return $this->permissionFailure();
}
}
// TO DO - inspect that Accept header as well. $_GET['accept'] can still be checked, as it's handy for debugging
switch($contentType) {
case "text/xml":
$this->getResponse()->addHeader("Content-type", "text/xml");
if($obj instanceof DataObjectSet) return $this->dataObjectSetAsXML($obj);
else return $this->dataObjectAsXML($obj);
case "text/json":
//$this->getResponse()->addHeader("Content-type", "text/json");
if($obj instanceof DataObjectSet) return $this->dataObjectSetAsJSON($obj);
else return $this->dataObjectAsJSON($obj);
case "text/html":
case "application/xhtml+xml":
if($obj instanceof DataObjectSet) return $this->dataObjectSetAsXHTML($obj);
else return $this->dataObjectAsXHTML($obj);
}
}
/**
* Generate an XML representation of the given DataObject.
*/
protected function dataObjectAsXML(DataObject $obj) {
protected function dataObjectAsXML(DataObject $obj, $includeHeader = true) {
$className = $obj->class;
$id = $obj->ID;
$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID");
$json = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<$className>\n";
$json = "";
if($includeHeader) $json .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
$json .= "<$className href=\"$objHref.xml\">\n";
foreach($obj->db() as $fieldName => $fieldType) {
$json .= "<$fieldName>" . Convert::raw2xml($obj->$fieldName) . "</$fieldName>\n";
if(is_object($obj->$fieldName)) {
$json .= $obj->$fieldName->toXML();
} else {
$json .= "<$fieldName>" . Convert::raw2xml($obj->$fieldName) . "</$fieldName>\n";
}
}
foreach($obj->has_one() as $relName => $relClass) {
$fieldName = $relName . 'ID';
@ -131,27 +169,26 @@ class RestfulServer extends Controller {
} else {
$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName");
}
$json .= "<$relName linktype=\"has_one\" href=\"$href\" id=\"{$obj->$fieldName}\" />\n";
$json .= "<$relName linktype=\"has_one\" href=\"$href.xml\" id=\"{$obj->$fieldName}\" />\n";
}
foreach($obj->has_many() as $relName => $relClass) {
$json .= "<$relName linktype=\"has_many\">\n";
$json .= "<$relName linktype=\"has_many\" href=\"$objHref/$relName.xml\">\n";
$items = $obj->$relName();
foreach($items as $item) {
//$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID");
$href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID");
$json .= "<$relClass href=\"$href\" id=\"{$item->ID}\" />\n";
$json .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\" />\n";
}
$json .= "</$relName>\n";
}
foreach($obj->many_many() as $relName => $relClass) {
$json .= "<$relName linktype=\"many_many\">\n";
$json .= "<$relName linktype=\"many_many\" href=\"$objHref/$relName.xml\">\n";
$items = $obj->$relName();
foreach($items as $item) {
//$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID");
$href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID");
$json .= "<$relClass href=\"$href\" id=\"{$item->ID}\" />\n";
$json .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\" />\n";
}
$json .= "</$relName>\n";
}
@ -161,6 +198,20 @@ class RestfulServer extends Controller {
return $json;
}
/**
* Generate an XML representation of the given DataObject.
*/
protected function dataObjectSetAsXML(DataObjectSet $set) {
$className = $set->class;
$json = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<$className>\n";
foreach($set as $item) {
if($item->canView()) $json .= $this->dataObjectAsXML($item, false);
}
$json .= "</$className>";
return $json;
}
/**
* Generate an XML representation of the given DataObject.
@ -171,7 +222,11 @@ class RestfulServer extends Controller {
$json = "{\n className : \"$className\",\n";
foreach($obj->db() as $fieldName => $fieldType) {
$jsonParts[] = "$fieldName : \"" . Convert::raw2js($obj->$fieldName) . "\"";
if(is_object($obj->$fieldName)) {
$jsonParts[] = "$fieldName : " . $obj->$fieldName->toJSON();
} else {
$jsonParts[] = "$fieldName : \"" . Convert::raw2js($obj->$fieldName) . "\"";
}
}
foreach($obj->has_one() as $relName => $relClass) {
@ -181,7 +236,7 @@ class RestfulServer extends Controller {
} else {
$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName");
}
$jsonParts[] = "$relName : { className : \"$relClass\", href : \"$href\", id : \"{$obj->$fieldName}\" }";
$jsonParts[] = "$relName : { className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
}
foreach($obj->has_many() as $relName => $relClass) {
@ -190,7 +245,7 @@ class RestfulServer extends Controller {
foreach($items as $item) {
//$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID");
$href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID");
$jsonInnerParts[] = "{ className : \"$relClass\", href : \"$href\", id : \"{$obj->$fieldName}\" }";
$jsonInnerParts[] = "{ className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
}
$jsonParts[] = "$relName : [\n " . implode(",\n ", $jsonInnerParts) . " \n ]";
}
@ -201,13 +256,26 @@ class RestfulServer extends Controller {
foreach($items as $item) {
//$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID");
$href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID");
$jsonInnerParts[] = " { className : \"$relClass\", href : \"$href\", id : \"{$obj->$fieldName}\" }";
$jsonInnerParts[] = " { className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
}
$jsonParts[] = "$relName : [\n " . implode(",\n ", $jsonInnerParts) . "\n ]";
}
return "{\n " . implode(",\n ", $jsonParts) . "\n}";
}
/**
* Generate an XML representation of the given DataObject.
*/
protected function dataObjectSetAsJSON(DataObjectSet $set) {
$jsonParts = array();
foreach($set as $item) {
if($item->canView()) $jsonParts[] = $this->dataObjectAsJSON($item);
}
return "[\n" . implode(",\n", $jsonParts) . "\n]";
}
/**
* Handler for object delete
*/

View File

@ -145,5 +145,13 @@ class ClassInfo {
global $_ALL_CLASSES;
return (isset($_ALL_CLASSES['implementors'][$interfaceName])) ? $_ALL_CLASSES['implementors'][$interfaceName] : false;
}
/**
* Returns true if the given class implements the given interface
*/
static function classImplements($className, $interfaceName) {
global $_ALL_CLASSES;
return isset($_ALL_CLASSES['implementors'][$interfaceName]) ? in_array($className, $_ALL_CLASSES['implementors'][$interfaceName]) : false;
}
}
?>

View File

@ -557,6 +557,14 @@ class DataObject extends ViewableData implements DataObjectInterface {
if(($this->ID && is_numeric($this->ID)) && !$forceInsert) {
$dbCommand = 'update';
// Update the changed array with references to changed obj-fields
foreach($this->record as $k => $v) {
if(is_object($v) && $v->isChanged()) {
$this->changed[$k] = true;
}
}
} else{
$dbCommand = 'insert';
@ -1022,7 +1030,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
*
* @return array The database fields
*/
public function db() {
public function db($component = null) {
$classes = ClassInfo::ancestry($this);
$good = false;
$items = array();
@ -1035,7 +1043,15 @@ class DataObject extends ViewableData implements DataObjectInterface {
}
continue;
}
eval("\$items = array_merge((array){$class}::\$db, (array)\$items);");
if($component) {
$candidate = eval("return isset({$class}::\$db[\$component]) ? {$class}::\$db[\$component] : null;");
if($candidate) {
return $candidate;
}
} else {
eval("\$items = array_merge((array){$class}::\$db, (array)\$items);");
}
}
return $items;
@ -1176,8 +1192,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
public function scaffoldFormFields() {
$fields = new FieldSet();
foreach($this->inheritedDatabaseFields() as $fieldName => $fieldType) {
foreach($this->db() as $fieldName => $fieldType) {
// @todo Pass localized title
// commented out, to be less of a pain in the ass
//$fields->addFieldToTab('Root.Main', $this->dbObject($fieldName)->scaffoldFormField());
@ -1243,6 +1258,22 @@ class DataObject extends ViewableData implements DataObjectInterface {
* @return mixed The field value
*/
protected function getField($field) {
// If we already have an object in $this->record, then we should just return that
if(isset($this->record[$field]) && is_object($this->record[$field])) return $this->record[$field];
// Otherwise, we need to determine if this is a complex field
$fieldClass = $this->db($field);
if($fieldClass && ClassInfo::classImplements($fieldClass, 'CompositeDBField')) {
$helperPair = $this->castingHelperPair($field);
$constructor = $helperPair['castingHelper'];
$fieldName = $field;
$fieldObj = eval($constructor);
if(isset($this->record[$field])) $fieldObj->setValue($this->record[$field], $this->record);
$this->record[$field] = $fieldObj;
return $this->record[$field];
}
return isset($this->record[$field]) ? $this->record[$field] : null;
}
@ -1293,28 +1324,36 @@ class DataObject extends ViewableData implements DataObjectInterface {
* @param mixed $val New field value
*/
function setField($fieldName, $val) {
$defaults = $this->stat('defaults');
// if a field is not existing or has strictly changed
if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
// TODO Add check for php-level defaults which are not set in the db
// TODO Add check for hidden input-fields (readonly) which are not set in the db
if(
// Only existing fields
$this->fieldExists($fieldName)
// Catches "0"==NULL
&& (isset($this->record[$fieldName]) && (intval($val) != intval($this->record[$fieldName])))
// Main non type-based check
&& (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
) {
// Non-strict check fails, so value really changed, e.g. "abc" != "cde"
$this->changed[$fieldName] = 2;
} else {
// Record change-level 1 if only the type changed, e.g. 0 !== NULL
$this->changed[$fieldName] = 1;
}
// value is always saved back when strict check succeeds
// Situation 1: Passing a DBField
if($val instanceof DBField) {
$val->Name = $fieldName;
$this->record[$fieldName] = $val;
// Situation 2: Passing a literal
} else {
$defaults = $this->stat('defaults');
// if a field is not existing or has strictly changed
if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
// TODO Add check for php-level defaults which are not set in the db
// TODO Add check for hidden input-fields (readonly) which are not set in the db
if(
// Only existing fields
$this->fieldExists($fieldName)
// Catches "0"==NULL
&& (isset($this->record[$fieldName]) && (intval($val) != intval($this->record[$fieldName])))
// Main non type-based check
&& (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
) {
// Non-strict check fails, so value really changed, e.g. "abc" != "cde"
$this->changed[$fieldName] = 2;
} else {
// Record change-level 1 if only the type changed, e.g. 0 !== NULL
$this->changed[$fieldName] = 1;
}
// value is always saved back when strict check succeeds
$this->record[$fieldName] = $val;
}
}
}
@ -1334,7 +1373,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
$castingHelper = $this->castingHelper($fieldName);
if($castingHelper) {
$fieldObj = eval($castingHelper);
$fieldObj->setVal($val);
$fieldObj->setValue($val);
$fieldObj->saveInto($this);
} else {
$this->$fieldName = $val;
@ -1349,7 +1388,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
* @return boolean True if the given field exists
*/
public function hasField($field) {
return array_key_exists($field, $this->record);
return array_key_exists($field, $this->record) || $this->fieldExists($field);
}
/**
@ -1486,14 +1525,17 @@ class DataObject extends ViewableData implements DataObjectInterface {
* @return DBField The field as a DBField object
*/
public function dbObject($fieldName) {
return $this->obj($fieldName);
/*
$helperPair = $this->castingHelperPair($fieldName);
$constructor = $helperPair['castingHelper'];
if($obj = eval($constructor)) {
$obj->setVal($this->$fieldName, $this->record);
$obj->setValue($this->$fieldName, $this->record);
}
return $obj;
*/
}
/**
@ -1560,18 +1602,34 @@ class DataObject extends ViewableData implements DataObjectInterface {
// Build our intial query
$query = new SQLQuery($select, "`$baseClass`", $filter, $sort);
// Add SQL for multi-value fields on the base table
$databaseFields = $this->databaseFields();
if($databaseFields) foreach($databaseFields as $k => $v) {
if(!in_array($k, array('ClassName', 'LastEdited', 'Created'))) {
if(ClassInfo::classImplements($v, 'CompositeDBField')) {
$this->obj($k)->addToQuery($query);
}
}
}
// Join all the tables
if($tableClasses) {
foreach($tableClasses as $tableClass) {
$query->from[$tableClass] = "LEFT JOIN `$tableClass` ON `$tableClass`.ID = `$baseClass`.ID";
$query->select[] = "`$tableClass`.*";
// ask each $db field on the specific table for alterations to the query
$uninheritedDbFields = singleton($tableClass)->uninherited('db',true);
if($uninheritedDbFields) foreach($uninheritedDbFields as $fieldName => $fieldType) {
singleton($tableClass)->obj($fieldName)->addToQuery($query);
// Add SQL for multi-value fields
$SNG = singleton($tableClass);
foreach($SNG->databaseFields() as $k => $v) {
if(!in_array($k, array('ClassName', 'LastEdited', 'Created'))) {
if(ClassInfo::classImplements($v, 'CompositeDBField')) {
$SNG->obj($k)->addToQuery($query);
}
}
}
}
}
$query->select[] = "`$baseClass`.ID";
$query->select[] = "if(`$baseClass`.ClassName,`$baseClass`.ClassName,'$baseClass') AS RecordClassName";

View File

@ -1,8 +1,14 @@
<?php
/**
* Single field in the database.
* Every field from the database is represented as a sub-class of DBField. In addition to supporting
* the creation of the field in the database,
* Every field from the database is represented as a sub-class of DBField.
*
* <h2>Multi-value DBField objects</h2>
* Sometimes you will want to make DBField classes that don't have a 1-1 match to database fields. To do this, there are a
* number of fields for you to overload.
* - Overload {@link writeToManipulation} to add the appropriate references to the INSERT or UPDATE command
* - Overload {@link addToQuery} to add the appropriate items to a SELECT query's field list
* - Add appropriate accessor methods
*
* @package sapphire
* @subpackage model
@ -38,8 +44,30 @@ abstract class DBField extends ViewableData {
return $dbField;
}
/**
* @deprecated
*/
function setVal($value, $record = null) {
return $this->setValue($value);
return $this->setValue($value, $record);
}
/**
* Set the name of this field.
* The name should never be altered, but it if was never given a name in the first place you can set a name.
* If you try an alter the name a warning will be thrown.
*/
function setName($name) {
if($this->name) {
user_error("DBField::setName() shouldn't be called once a DBField already has a name. It's partially immutable - it shouldn't be altered after it's given a value.", E_USER_WARNING);
}
$this->name = $name;
}
/**
* Returns the name of this field
*/
function getName() {
return $this->name;
}
/**
@ -92,7 +120,9 @@ abstract class DBField extends ViewableData {
*
* @param Query $query
*/
function addToQuery(&$query) {}
function addToQuery(&$query) {
}
function setTable($tableName) {
$this->tableName = $tableName;

View File

@ -91,7 +91,6 @@ class SearchContext extends Object {
public function getQuery($searchParams) {
$q = new SQLQuery("*", $this->modelClass);
$this->processFilters($q);
return $q;
}
@ -162,7 +161,8 @@ class SearchContext extends Object {
$fields = array_filter($fields, array($this,'clearEmptySearchFields'));
$length = count($fields);
foreach($fields as $key=>$val) {
if ($val != '') {
// Array values come from more complex fields - for now let's just disable searching on them
if (!is_array($val) && $val != '') {
$filter .= "`$key`='$val'";
} else {
$length--;