(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@60227 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2008-08-09 05:57:44 +00:00
parent c1178c31ce
commit 8fd1a33d84
14 changed files with 619 additions and 220 deletions

View File

@ -6,15 +6,40 @@
*/
abstract class DataFormatter extends Object {
/**
* Set priority from 0-100.
* If multiple formatters for the same extension exist,
* we select the one with highest priority.
*
* @var int
*/
public static $priority = 50;
/**
* Follow relations for the {@link DataObject} instances
* ($has_one, $has_many, $many_many).
* Set to "0" to disable relation output.
*
* @todo Support more than one nesting level
*
* @var int
*/
public $relationDepth = 1;
/**
* Get a DataFormatter object suitable for handling the given file extension
*/
static function for_extension($extension) {
$classes = ClassInfo::subclassesFor("DataFormatter");
array_shift($classes);
$sortedClasses = array();
foreach($classes as $class) {
$formatter = singleton($class);
$sortedClasses[$class] = singleton($class)->stat('priority');
}
arsort($sortedClasses);
foreach($sortedClasses as $className => $priority) {
$formatter = singleton($className);
if(in_array($extension, $formatter->supportedExtensions())) {
return $formatter;
}

View File

@ -22,7 +22,7 @@ class JSONDataFormatter extends DataFormatter {
$id = $obj->ID;
$json = "{\n className : \"$className\",\n";
$dbFields = array_merge($obj->databaseFields(), array('ID'=>'Int'));
$dbFields = array_merge($obj->inheritedDatabaseFields(), array('ID'=>'Int'));
foreach($dbFields as $fieldName => $fieldType) {
if(is_object($obj->$fieldName)) {
$jsonParts[] = "$fieldName : " . $obj->$fieldName->toJSON();
@ -31,36 +31,38 @@ class JSONDataFormatter extends DataFormatter {
}
}
foreach($obj->has_one() as $relName => $relClass) {
$fieldName = $relName . 'ID';
if($obj->$fieldName) {
$href = Director::absoluteURL(self::$api_base . "$relClass/" . $obj->$fieldName);
} else {
$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName");
if($this->relationDepth > 0) {
foreach($obj->has_one() as $relName => $relClass) {
$fieldName = $relName . 'ID';
if($obj->$fieldName) {
$href = Director::absoluteURL(self::$api_base . "$relClass/" . $obj->$fieldName);
} else {
$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName");
}
$jsonParts[] = "$relName : { className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
}
$jsonParts[] = "$relName : { className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
}
foreach($obj->has_many() as $relName => $relClass) {
$jsonInnerParts = array();
$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");
$jsonInnerParts[] = "{ className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
foreach($obj->has_many() as $relName => $relClass) {
$jsonInnerParts = array();
$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");
$jsonInnerParts[] = "{ className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
}
$jsonParts[] = "$relName : [\n " . implode(",\n ", $jsonInnerParts) . " \n ]";
}
$jsonParts[] = "$relName : [\n " . implode(",\n ", $jsonInnerParts) . " \n ]";
}
foreach($obj->many_many() as $relName => $relClass) {
$jsonInnerParts = array();
$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");
$jsonInnerParts[] = " { className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
foreach($obj->many_many() as $relName => $relClass) {
$jsonInnerParts = array();
$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");
$jsonInnerParts[] = " { className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }";
}
$jsonParts[] = "$relName : [\n " . implode(",\n ", $jsonInnerParts) . "\n ]";
}
$jsonParts[] = "$relName : [\n " . implode(",\n ", $jsonInnerParts) . "\n ]";
}
return "{\n " . implode(",\n ", $jsonParts) . "\n}"; }

View File

@ -122,8 +122,15 @@ class RestfulServer extends Controller {
* @return String The serialized representation of the requested object(s) - usually XML or JSON.
*/
protected function getHandler($className, $id, $relation, $formatter) {
$limit = (int)$this->request->getVar('limit');
$sort = array(
'sort' => $this->request->getVar('sort'),
'dir' => $this->request->getVar('dir')
);
$limit = array(
'start' => $this->request->getVar('start'),
'limit' => $this->request->getVar('limit')
);
if($id) {
$obj = DataObject::get_by_id($className, $id);
if(!$obj) {
@ -135,12 +142,27 @@ class RestfulServer extends Controller {
}
if($relation) {
if($obj->hasMethod($relation)) $obj = $obj->$relation('', '', '', $limit);
else return $this->notFound();
if($relationClass = $obj->many_many($relation)) {
$query = $obj->getManyManyComponentsQuery($relation);
} elseif($relationClass = $obj->has_many($relation)) {
$query = $obj->getComponentsQuery($relation);
} elseif($relationClass = $obj->has_one($relation)) {
$query = null;
} elseif($obj->hasMethod("{$relation}Query")) {
// @todo HACK Switch to ComponentSet->getQuery() once we implement it (and lazy loading)
$query = $obj->{"{$relation}Query"}(null, $sort, null, $limit);
$relationClass = $obj->{"{$relation}Class"}();
} else {
return $this->notFound();
}
// get all results
$obj = $this->search($relationClass, $this->request->getVars(), $sort, $limit, $query);
if(!$obj) $obj = new DataObjectSet();
}
} else {
$obj = DataObject::get($className, "");
$obj = $this->search($className, $this->request->getVars(), $sort, $limit);
// show empty serialized result when no records are present
if(!$obj) $obj = new DataObjectSet();
if(!singleton($className)->stat('api_access')) {
@ -151,6 +173,25 @@ class RestfulServer extends Controller {
if($obj instanceof DataObjectSet) return $formatter->convertDataObjectSet($obj);
else return $formatter->convertDataObject($obj);
}
/**
* Uses the default {@link SearchContext} specified through
* {@link DataObject::getDefaultSearchContext()} to augument
* an existing query object (mostly a component query from {@link DataObject})
* with search clauses.
*
* @todo Allow specifying of different searchcontext getters on model-by-model basis
*
* @param string $className
* @param array $params
* @return DataObjectSet
*/
protected function search($className, $params = null, $sort = null, $limit = null, $existingQuery = null) {
$searchContext = singleton($className)->getDefaultSearchContext();
$query = $searchContext->getQuery($params, $sort, $limit, $existingQuery);
return singleton($className)->buildDataObjectSet($query->execute());
}
/**
* Handler for object delete
@ -195,7 +236,6 @@ class RestfulServer extends Controller {
}
/**
* Restful server handler for a DataObjectSet
*/

View File

@ -29,7 +29,7 @@ class XMLDataFormatter extends DataFormatter {
$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID");
$json = "<$className href=\"$objHref.xml\">\n";
$dbFields = array_merge($obj->databaseFields(), array('ID'=>'Int'));
$dbFields = array_merge($obj->inheritedDatabaseFields(), array('ID'=>'Int'));
foreach($dbFields as $fieldName => $fieldType) {
if(is_object($obj->$fieldName)) {
$json .= $obj->$fieldName->toXML();
@ -38,36 +38,37 @@ class XMLDataFormatter extends DataFormatter {
}
}
foreach($obj->has_one() as $relName => $relClass) {
$fieldName = $relName . 'ID';
if($obj->$fieldName) {
$href = Director::absoluteURL(self::$api_base . "$relClass/" . $obj->$fieldName);
} else {
$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName");
if($this->relationDepth > 0) {
foreach($obj->has_one() as $relName => $relClass) {
$fieldName = $relName . 'ID';
if($obj->$fieldName) {
$href = Director::absoluteURL(self::$api_base . "$relClass/" . $obj->$fieldName);
} else {
$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName");
}
$json .= "<$relName linktype=\"has_one\" href=\"$href.xml\" 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\" 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.xml\" id=\"{$item->ID}\" />\n";
foreach($obj->has_many() as $relName => $relClass) {
$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.xml\" id=\"{$item->ID}\" />\n";
}
$json .= "</$relName>\n";
}
$json .= "</$relName>\n";
}
foreach($obj->many_many() as $relName => $relClass) {
$json .= "<$relName linktype=\"many_many\" href=\"$objHref/$relName.xml\">\n";
$items = $obj->$relName();
foreach($items as $item) {
$href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID");
$json .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\" />\n";
foreach($obj->many_many() as $relName => $relClass) {
$json .= "<$relName linktype=\"many_many\" href=\"$objHref/$relName.xml\">\n";
$items = $obj->$relName();
foreach($items as $item) {
$href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID");
$json .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\" />\n";
}
$json .= "</$relName>\n";
}
$json .= "</$relName>\n";
}
$json .= "</$className>";

View File

@ -828,7 +828,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
user_error("DataObject::getComponent(): Unknown 1-to-1 component '$componentName' on class '$this->class'", E_USER_ERROR);
}
}
/**
* A cache used by component getting classes
* @var array
@ -840,9 +840,9 @@ class DataObject extends ViewableData implements DataObjectInterface {
*
* @param string $componentName Name of the component
* @param string $filter A filter to be inserted into the WHERE clause
* @param string $sort A sort expression to be inserted into the ORDER BY clause. If omitted, the static field $default_sort on the component class will be used.
* @param string|array $sort A sort expression to be inserted into the ORDER BY clause. If omitted, the static field $default_sort on the component class will be used.
* @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned.
* @param string $limit A limit expression to be inserted into the LIMIT clause
* @param string|array $limit A limit expression to be inserted into the LIMIT clause
*
* @return ComponentSet The components of the one-to-many relationship.
*/
@ -861,14 +861,9 @@ class DataObject extends ViewableData implements DataObjectInterface {
$joinField = $this->getComponentJoinField($componentName);
if($this->isInDB()) { //Check to see whether we should query the db
$componentObj = singleton($componentClass);
$id = $this->getField("ID");
// get filter
$combinedFilter = "$joinField = '$id'";
if($filter) $combinedFilter .= " AND {$filter}";
$result = $componentObj->instance_get($combinedFilter, $sort, $join, $limit, "ComponentSet");
$query = $this->getComponentsQuery($componentName, $filter, $sort, $join, $limit);
$result = $this->buildDataObjectSet($query->execute(), 'ComponentSet', $query, $componentClass);
if($result) $result->parseQueryLimit($query);
}
if(!$result) {
@ -882,6 +877,37 @@ class DataObject extends ViewableData implements DataObjectInterface {
return $result;
}
/**
* Get the query object for a $has_many Component.
*
* Use {@link DataObjectSet->setComponentInfo()} to attach metadata to the
* resultset you're building with this query.
* Use {@link DataObject->buildDataObjectSet()} to build a set out of the {@link SQLQuery}
* object, and pass "ComponentSet" as a $containerClass.
*
* @param string $componentName
* @param string $filter
* @param string|array $sort
* @param string $join
* @param string|array $limit
* @return SQLQuery
*/
public function getComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
if(!$componentClass = $this->has_many($componentName)) {
user_error("DataObject::getComponentsQuery(): Unknown 1-to-many component '$componentName' on class '$this->class'", E_USER_ERROR);
}
$joinField = $this->getComponentJoinField($componentName);
$id = $this->getField("ID");
// get filter
$combinedFilter = "$joinField = '$id'";
if($filter) $combinedFilter .= " AND {$filter}";
return singleton($componentClass)->extendedSQL($combinedFilter, $sort, $limit, $join);
}
/**
* Tries to find the db-key for storing a relation (defaults to "ParentID" if no relation is found).
@ -893,7 +919,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
*/
public function getComponentJoinField($componentName) {
if(!$componentClass = $this->has_many($componentName)) {
user_error("DataObject::getComsponents(): Unknown 1-to-many component '$componentName' on class '$this->class'", E_USER_ERROR);
user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName' on class '$this->class'", E_USER_ERROR);
}
$componentObj = singleton($componentClass);
@ -947,24 +973,14 @@ class DataObject extends ViewableData implements DataObjectInterface {
list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
// Join expression is done on SiteTree.ID even if we link to Page; it helps work around
// database inconsistencies
$componentBaseClass = ClassInfo::baseDataClass($componentClass);
if($this->ID && is_numeric($this->ID)) {
if($componentClass) {
$componentObj = singleton($componentClass);
// Join expression is done on SiteTree.ID even if we link to Page; it helps work around
// database inconsistencies
$componentBaseClass = ClassInfo::baseDataClass($componentClass);
$query = $componentObj->extendedSQL(
"`$table`.$parentField = $this->ID", // filter
$sort,
$limit,
"INNER JOIN `$table` ON `$table`.$componentField = `$componentBaseClass`.ID" // join
);
array_unshift($query->select, "`$table`.*");
if($filter) $query->where[] = $filter;
if($join) $query->from[] = $join;
$query = $this->getManyManyComponentsQuery($componentName, $filter, $sort, $join, $limit);
$records = $query->execute();
$result = $this->buildDataObjectSet($records, "ComponentSet", $query, $componentBaseClass);
@ -986,6 +1002,43 @@ class DataObject extends ViewableData implements DataObjectInterface {
return $result;
}
/**
* Get the query object for a $many_many Component.
* Use {@link DataObjectSet->setComponentInfo()} to attach metadata to the
* resultset you're building with this query.
* Use {@link DataObject->buildDataObjectSet()} to build a set out of the {@link SQLQuery}
* object, and pass "ComponentSet" as a $containerClass.
*
* @param string $componentName
* @param string $filter
* @param string|array $sort
* @param string $join
* @param string|array $limit
* @return SQLQuery
*/
public function getManyManyComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
$componentObj = singleton($componentClass);
// Join expression is done on SiteTree.ID even if we link to Page; it helps work around
// database inconsistencies
$componentBaseClass = ClassInfo::baseDataClass($componentClass);
$query = $componentObj->extendedSQL(
"`$table`.$parentField = $this->ID", // filter
$sort,
$limit,
"INNER JOIN `$table` ON `$table`.$componentField = `$componentBaseClass`.ID" // join
);
array_unshift($query->select, "`$table`.*");
if($filter) $query->where[] = $filter;
if($join) $query->from[] = $join;
return $query;
}
/**
* Creates an empty component for the given one-one or one-many relationship
@ -1004,6 +1057,61 @@ class DataObject extends ViewableData implements DataObjectInterface {
}
}
/**
* Add the scaffold-generated relation fields to the given field set
*/
protected function addScaffoldRelationFields($fieldSet) {
if ($this->has_many() || $this->_many_many()) {
$oldFields = $fieldSet;
$fieldSet = new FieldSet(
new TabSet("Root", new Tab("Main"))
);
foreach($oldFields as $field) {
$fieldSet->addFieldToTab("Root.Main", $field);
}
}
if($this->has_many()) {
// Add each relation as a separate tab
foreach($this->has_many() as $relationship => $component) {
$relationshipFields = singleton($component)->summary_fields();
$foreignKey = $this->getComponentJoinField($relationship);
$fieldSet->addFieldToTab("Root.$relationship", new ComplexTableField($this, $relationship, $component, $relationshipFields, "getCMSFields", "$foreignKey = $this->ID"));
}
}
if ($this->many_many()) {
foreach($this->many_many() as $relationship => $component) {
$relationshipFields = singleton($component)->summary_fields();
$filterJoin = $this->getManyManyJoin($relationship, $component);
$tableField = new ComplexTableField($this, $relationship, $component, $relationshipFields, "getCMSFields", '', '', $filterJoin);
$tableField->popupClass = "ScaffoldingComplexTableField_Popup";
$fieldSet->addFieldToTab("Root.$relationship", $tableField);
}
}
return $fieldSet;
}
/**
* Pull out a join clause for a many-many relationship.
*
* @param string $componentName The many_many or belongs_many_many relation to join to.
* @param string $baseTable The classtable that will already be included in the SQL query to which this join will be added.
* @return string SQL join clause
*/
function getManyManyJoin($componentName, $baseTable) {
if(!$componentClass = $this->many_many($componentName)) {
user_error("DataObject::getComponents(): Unknown many-to-many component '$componentName' on class '$this->class'", E_USER_ERROR);
}
$classes = array_reverse(ClassInfo::ancestry($this->class));
list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
if($baseTable == $parentClass) {
return "LEFT JOIN `$table` ON (`$parentField` = `$parentClass`.`ID` AND `$componentField` = '{$this->ID}')";
} else {
return "LEFT JOIN `$table` ON (`$componentField` = `$componentClass`.`ID` AND `$parentField` = '{$this->ID}')";
}
}
/**
* Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and their classes.
*
@ -1225,29 +1333,6 @@ class DataObject extends ViewableData implements DataObjectInterface {
return $fields;
}
/**
* Add the scaffold-generated relation fields to the given field set
*/
protected function addScaffoldRelationFields($fieldSet) {
if($this->has_many()) {
// Refactor the fields that we have been given into a tab, "Main", in a tabset
$oldFields = $fieldSet;
$fieldSet = new FieldSet(
new TabSet("Root", new Tab("Main"))
);
foreach($oldFields as $field) $fieldSet->addFieldToTab("Root.Main", $field);
// Add each relation as a separate tab
foreach($this->has_many() as $relationship => $component) {
$relationshipFields = singleton($component)->summary_fields();
$foreignKey = $this->getComponentJoinField($relationship);
$fieldSet->addFieldToTab("Root.$relationship", new ComplexTableField($this, $relationship, $component, $relationshipFields, "getCMSFields", "$foreignKey = $this->ID"));
}
}
return $fieldSet;
}
/**
* Centerpiece of every data administration interface in Silverstripe,
* which returns a {@link FieldSet} suitable for a {@link Form} object.
@ -1643,8 +1728,8 @@ class DataObject extends ViewableData implements DataObjectInterface {
* Build a {@link SQLQuery} object to perform the given query.
*
* @param string $filter A filter to be inserted into the WHERE clause.
* @param string $sort A sort expression to be inserted into the ORDER BY clause. If omitted, self::$default_sort will be used.
* @param string $limit A limit expression to be inserted into the LIMIT clause.
* @param string|array $sort A sort expression to be inserted into the ORDER BY clause. If omitted, self::$default_sort will be used.
* @param string|array $limit A limit expression to be inserted into the LIMIT clause.
* @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned.
* @param boolean $restictClasses Restrict results to only objects of either this class of a subclass of this class
* @param string $having A filter to be inserted into the HAVING clause.
@ -1666,43 +1751,13 @@ class DataObject extends ViewableData implements DataObjectInterface {
$baseClass = array_shift($tableClasses);
$select = array("`$baseClass`.*");
// If sort contains a function call, let's move the sort clause into a separate selected field.
// Some versions of MySQL choke if you have a group function referenced directly in the ORDER BY
if($sort && strpos($sort,'(') !== false) {
// Sort can be "Col1 DESC|ASC, Col2 DESC|ASC", we need to handle that
$sortParts = explode(",", $sort);
// If you have select if(X,A,B),C then the array will return 'if(X','A','B)','C'.
// Turn this into 'if(X,A,B)','C' by counting brackets
while(list($i,$sortPart) = each($sortParts)) {
while(substr_count($sortPart,'(') > substr_count($sortPart,')')) {
list($i,$nextSortPart) = each($sortParts);
if($i === null) break;
$sortPart .= ',' . $nextSortPart;
}
$lumpedSortParts[] = $sortPart;
}
foreach($lumpedSortParts as $i => $sortPart) {
$sortPart = trim($sortPart);
if(substr(strtolower($sortPart),-5) == ' desc') {
$select[] = substr($sortPart,0,-5) . " AS _SortColumn{$i}";
$newSorts[] = "_SortColumn{$i} DESC";
} else if(substr(strtolower($sortPart),-4) == ' asc') {
$select[] = substr($sortPart,0,-4) . " AS _SortColumn{$i}";
$newSorts[] = "_SortColumn{$i} ASC";
} else {
$select[] = "$sortPart AS _SortColumn{$i}";
$newSorts[] = "_SortColumn{$i} ASC";
}
}
$sort = implode(", ", $newSorts);
}
// Build our intial query
$query = new SQLQuery($select, "`$baseClass`", $filter, $sort);
$query = new SQLQuery($select);
$query->from("`$baseClass`");
$query->where($filter);
$query->orderby($sort);
$query->limit($limit);
// Add SQL for multi-value fields on the base table
$databaseFields = $this->databaseFields();
if($databaseFields) foreach($databaseFields as $k => $v) {
@ -1712,7 +1767,6 @@ class DataObject extends ViewableData implements DataObjectInterface {
}
}
}
// Join all the tables
if($tableClasses) {
foreach($tableClasses as $tableClass) {
@ -1752,10 +1806,6 @@ class DataObject extends ViewableData implements DataObjectInterface {
$query->where[] = "`$baseClass`.ClassName IN ('" . implode("','", $classNames) . "')";
}
if($limit) {
$query->limit = $limit;
}
if($having) {
$query->having[] = $having;
}
@ -1772,8 +1822,8 @@ class DataObject extends ViewableData implements DataObjectInterface {
* Like {@link buildSQL}, but applies the extension modifications.
*
* @param string $filter A filter to be inserted into the WHERE clause.
* @param string $sort A sort expression to be inserted into the ORDER BY clause. If omitted, self::$default_sort will be used.
* @param string $limit A limit expression to be inserted into the LIMIT clause.
* @param string|array $sort A sort expression to be inserted into the ORDER BY clause. If omitted, self::$default_sort will be used.
* @param string|array $limit A limit expression to be inserted into the LIMIT clause.
* @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned.
* @param string $having A filter to be inserted into the HAVING clause.
*
@ -1808,9 +1858,9 @@ class DataObject extends ViewableData implements DataObjectInterface {
*
* @param string $callerClass The class of objects to be returned
* @param string $filter A filter to be inserted into the WHERE clause.
* @param string $sort A sort expression to be inserted into the ORDER BY clause. If omitted, self::$default_sort will be used.
* @param string|array $sort A sort expression to be inserted into the ORDER BY clause. If omitted, self::$default_sort will be used.
* @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned.
* @param string $limit A limit expression to be inserted into the LIMIT clause.
* @param string|array $limit A limit expression to be inserted into the LIMIT clause.
* @param string $containerClass The container class to return the results in.
*
* @return mixed The objects matching the filter, in the class specified by $containerClass
@ -2153,7 +2203,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
$fields = array();
$currentObj = $this;
while(get_class($currentObj) != 'DataObject') {
$fields = array_merge($fields, $currentObj->customDatabaseFields());
$fields = array_merge($fields, (array)$currentObj->customDatabaseFields());
$currentObj = singleton($currentObj->parentClass());
}
return $fields;

View File

@ -15,19 +15,19 @@ class SQLQuery extends Object {
* An array of fields to select.
* @var array
*/
public $select;
public $select = array();
/**
* An array of join clauses. The first one is just the table name.
* @var array
*/
public $from;
public $from = array();
/**
* An array of filters.
* @var array
*/
public $where;
public $where = array();
/**
* An ORDER BY clause.
@ -39,13 +39,13 @@ class SQLQuery extends Object {
* An array of fields to group by.
* @var array
*/
public $groupby;
public $groupby = array();
/**
* An array of having clauses.
* @var array
*/
public $having;
public $having = array();
/**
* A limit clause.
@ -57,13 +57,13 @@ class SQLQuery extends Object {
* If this is true DISTINCT will be added to the SQL.
* @var boolean
*/
public $distinct;
public $distinct = false;
/**
* If this is true, this statement will delete rather than select.
* @var boolean
*/
public $delete;
public $delete = false;
/**
* The logical connective used to join WHERE clauses. Defaults to AND.
@ -83,13 +83,14 @@ class SQLQuery extends Object {
* @param string $limit A LIMIT clause.
*/
function __construct($select = "*", $from = array(), $where = "", $orderby = "", $groupby = "", $having = "", $limit = "") {
if($select) $this->select = is_array($select) ? $select : array($select);
if($from) $this->from = is_array($from) ? $from : array(str_replace('`','',$from) => $from);
if($where) $this->where = is_array($where) ? $where : array($where);
$this->orderby = $orderby;
if($groupby) $this->groupby = is_array($groupby) ? $groupby : array($groupby);
if($having) $this->having = is_array($having) ? $having : array($having);
$this->limit = $limit;
$this->select($select);
// @todo
$this->from = is_array($from) ? $from : array(str_replace('`','',$from) => $from);
$this->where($where);
$this->orderby($orderby);
$this->groupby($groupby);
$this->having($having);
$this->limit($limit);
parent::__construct();
}
@ -114,6 +115,7 @@ class SQLQuery extends Object {
} else {
$this->select = is_array($fields) ? $fields : array($fields);
}
return $this;
}
@ -125,18 +127,147 @@ class SQLQuery extends Object {
* </code>
*
* @param string $table
* @return SQLQuery
* @return SQLQuery This instance
*/
public function from($table) {
$this->from[] = $table;
return $this;
}
/**
* Add a LEFT JOIN criteria to the FROM clause.
* Add a LEFT JOIN criteria to the FROM clause.
*
* @return SQLQuery This instance
*/
public function leftJoin($table, $onPredicate) {
$this->from[] = "LEFT JOIN $table ON $onPredicate";
return $this;
}
/**
* Pass LIMIT clause either as SQL snippet or in array format.
*
* @param string|array $limit
* @return SQLQuery This instance
*/
public function limit($limit) {
// Pass limit as array or SQL string value
if(is_array($limit)) {
if(!array_key_exists('limit',$limit)) user_error('SQLQuery::limit(): Wrong format for $limit', E_USER_ERROR);
if(isset($limit['start']) && is_numeric($limit['start']) && isset($limit['limit']) && is_numeric($limit['limit'])) {
// @todo MySQL specific LIMIT syntax
$combinedLimit = (int)$limit['start'] . ',' . (int)$limit['limit'];
} elseif(isset($limit['limit']) && is_numeric($limit['limit'])) {
$combinedLimit = (int)$limit['limit'];
} else {
$combinedLimit = false;
}
} else {
$combinedLimit = $limit;
}
if(!empty($combinedLimit)) $this->limit = $combinedLimit;
return $this;
}
/**
* Pass ORDER BY clause either as SQL snippet or in array format.
*
* @todo Implement passing of multiple orderby pairs in nested array syntax,
* e.g. array(array('sort'=>'A','dir'=>'asc'),array('sort'=>'B'))
*
* @param string|array $orderby
* @return SQLQuery This instance
*/
public function orderby($orderby) {
// if passed as an array, assume two array values with column and direction (asc|desc)
if(is_array($orderby)) {
if(!array_key_exists('sort', $orderby)) user_error('SQLQuery::orderby(): Wrong format for $orderby array', E_USER_ERROR);
if(isset($orderby['sort']) && !empty($orderby['sort']) && isset($orderby['dir']) && !empty($orderby['dir'])) {
$combinedOrderby = "`" . Convert::raw2sql($orderby['sort']) . "` " . Convert::raw2sql(strtoupper($orderby['dir']));
} elseif(isset($orderby['sort']) && !empty($orderby['sort'])) {
$combinedOrderby = "`" . Convert::raw2sql($orderby['sort']) . "`";
} else {
$combinedOrderby = false;
}
} else {
$combinedOrderby = $orderby;
}
// If sort contains a function call, let's move the sort clause into a separate selected field.
// Some versions of MySQL choke if you have a group function referenced directly in the ORDER BY
if($combinedOrderby && strpos($combinedOrderby,'(') !== false) {
// Sort can be "Col1 DESC|ASC, Col2 DESC|ASC", we need to handle that
$sortParts = explode(",", $combinedOrderby);
// If you have select if(X,A,B),C then the array will return 'if(X','A','B)','C'.
// Turn this into 'if(X,A,B)','C' by counting brackets
while(list($i,$sortPart) = each($sortParts)) {
while(substr_count($sortPart,'(') > substr_count($sortPart,')')) {
list($i,$nextSortPart) = each($sortParts);
if($i === null) break;
$sortPart .= ',' . $nextSortPart;
}
$lumpedSortParts[] = $sortPart;
}
foreach($lumpedSortParts as $i => $sortPart) {
$sortPart = trim($sortPart);
if(substr(strtolower($sortPart),-5) == ' desc') {
$select[] = substr($sortPart,0,-5) . " AS _SortColumn{$i}";
$newSorts[] = "_SortColumn{$i} DESC";
} else if(substr(strtolower($sortPart),-4) == ' asc') {
$select[] = substr($sortPart,0,-4) . " AS _SortColumn{$i}";
$newSorts[] = "_SortColumn{$i} ASC";
} else {
$select[] = "$sortPart AS _SortColumn{$i}";
$newSorts[] = "_SortColumn{$i} ASC";
}
}
$combinedOrderby = implode(", ", $newSorts);
}
if(!empty($combinedOrderby)) $this->orderby = $combinedOrderby;
return $this;
}
/**
* Add a GROUP BY clause.
*
* @param string|array $groupby
* @return SQLQuery
*/
public function groupby($groupby) {
if(is_array($groupby)) {
$this->groupby = array_merge($this->groupby, $groupby);
} elseif(!empty($groupby)) {
$this->groupby[] = $groupby;
}
return $this;
}
/**
* Add a HAVING clause.
*
* @param string|array $having
* @return SQLQuery
*/
public function having($having) {
if(is_array($having)) {
$this->having = array_merge($this->having, $having);
} elseif(!empty($having)) {
$this->having[] = $having;
}
return $this;
}
/**
@ -166,7 +297,13 @@ class SQLQuery extends Object {
} else {
$filter = $args[0];
}
$this->where[] = $filter;
if(is_array($filter)) {
$this->where = array_merge($this->where,$filter);
} elseif(!empty($filter)) {
$this->where[] = $filter;
}
return $this;
}
@ -246,7 +383,7 @@ class SQLQuery extends Object {
$text = "SELECT $distinct" . implode(", ", $this->select);
}
$text .= " FROM " . implode(" ", $this->from);
if($this->where) $text .= " WHERE (" . $this->getFilter(). ")";
if($this->groupby) $text .= " GROUP BY " . implode(", ", $this->groupby);
if($this->having) $text .= " HAVING ( " . implode(" ) AND ( ", $this->having) . " )";

View File

@ -888,4 +888,70 @@ class ComplexTableField_Popup extends Form {
}
}
/**
* Used by ModelAdmin scaffolding, to manage many-many relationships.
*/
class ScaffoldingComplexTableField_Popup extends Form {
protected $sourceClass;
protected $dataObject;
function __construct($controller, $name, $fields, $validator, $readonly, $dataObject) {
$this->dataObject = $dataObject;
/**
* WARNING: DO NOT CHANGE THE ORDER OF THESE JS FILES
* Some have special requirements.
*/
//Requirements::css('cms/css/layout.css');
Requirements::css('jsparty/tabstrip/tabstrip.css');
Requirements::css('sapphire/css/Form.css');
Requirements::css('sapphire/css/ComplexTableField_popup.css');
Requirements::css('cms/css/typography.css');
Requirements::css('cms/css/cms_right.css');
Requirements::css('jsparty/jquery/plugins/autocomplete/jquery.ui.autocomplete.css');
Requirements::javascript("jsparty/prototype.js");
Requirements::javascript("jsparty/behaviour.js");
Requirements::javascript("jsparty/prototype_improvements.js");
Requirements::javascript("jsparty/loader.js");
Requirements::javascript("jsparty/tabstrip/tabstrip.js");
Requirements::javascript("jsparty/scriptaculous/scriptaculous.js");
Requirements::javascript("jsparty/scriptaculous/controls.js");
Requirements::javascript("jsparty/layout_helpers.js");
Requirements::javascript("cms/javascript/LeftAndMain.js");
Requirements::javascript("cms/javascript/LeftAndMain_right.js");
Requirements::javascript("sapphire/javascript/TableField.js");
Requirements::javascript("sapphire/javascript/ComplexTableField.js");
Requirements::javascript("sapphire/javascript/ComplexTableField_popup.js");
// jQuery requirements (how many of these are actually needed?)
Requirements::javascript('jsparty/jquery/jquery.js');
Requirements::javascript('jsparty/jquery/plugins/livequery/jquery.livequery.js');
Requirements::javascript('jsparty/jquery/ui/ui.core.js');
Requirements::javascript('jsparty/jquery/ui/ui.tabs.js');
Requirements::javascript('jsparty/jquery/plugins/form/jquery.form.js');
Requirements::javascript('jsparty/jquery/plugins/dimensions/jquery.dimensions.js');
Requirements::javascript('jsparty/jquery/plugins/autocomplete/jquery.ui.autocomplete.js');
Requirements::javascript('sapphire/javascript/ScaffoldComplexTableField.js');
Requirements::javascript('cms/javascript/ModelAdmin.js');
if($this->dataObject->hasMethod('getRequirementsForPopup')) {
$this->dataObject->getRequirementsForPopup();
}
$actions = new FieldSet();
if(!$readonly) {
$actions->push(
$saveAction = new FormAction("saveComplexTableField", "Save")
);
$saveAction->addExtraClass('save');
}
parent::__construct($controller, $name, $fields, $actions, $validator);
}
function FieldHolder() {
return $this->renderWith('ComplexTableField_Form');
}
}
?>

View File

@ -139,7 +139,7 @@ class FieldSet extends DataObjectSet {
// Create any missing tabs
if(!$currentPointer) {
if(is_a($parentPointer,'TabSet')) {
$currentPointer = new Tab($tabName);
$currentPointer = new Tab($part);
$parentPointer->push($currentPointer);
} else {
user_error("FieldSet::addFieldToTab() Tried to add a tab to a " . $parentPointer->class . " object - '$part' didn't exist.", E_USER_ERROR);

View File

@ -7,10 +7,13 @@
class Tab extends CompositeField {
protected $tabSet;
public function __construct($title) {
public function __construct($name) {
$args = func_get_args();
$this->title = array_shift($args);
$this->id = ereg_replace('[^0-9A-Za-z]+', '', $this->title);
$name = array_shift($args);
$this->id = preg_replace('/[^0-9A-Za-z]+/', '', $name);
$this->title = preg_replace('/([a-z0-9])([A-Z])/', '\\1 \\2', $name);
$this->name = $name;
parent::__construct($args);
}

View File

@ -0,0 +1,5 @@
window.onload = function() {
jQuery("fieldset input:first").attr('autocomplete', 'off').autocomplete({list: ["mark rickerby", "maxwell sparks"]});
};

View File

@ -77,19 +77,20 @@ class SearchContext extends Object {
}
/**
* @refactor move to SQLQuery
* @todo fix hack
*/
protected function applyBaseTableFields() {
$classes = ClassInfo::dataClassesFor($this->modelClass);
//Debug::dump($classes);
//die();
$fields = array($classes[0].'.*', $this->modelClass.'.*');
$fields = array($this->modelClass.'.*');
if($this->modelClass != $classes[0]) $fields[] = $classes[0].'.*';
//$fields = array_keys($model->db());
$fields[] = $classes[0].'.ClassName AS RecordClassName';
return $fields;
}
/**
* @refactor move to SQLQuery
* @todo fix hack
*/
protected function applyBaseTable() {
@ -100,6 +101,7 @@ class SearchContext extends Object {
/**
* @todo only works for one level deep of inheritance
* @todo fix hack
* @deprecated - remove me!
*/
protected function applyBaseTableJoin($query) {
$classes = ClassInfo::dataClassesFor($this->modelClass);
@ -111,28 +113,30 @@ class SearchContext extends Object {
* list of query parameters.
*
* @param array $searchParams
* @param string|array $sort Database column to sort on. Falls back to {@link DataObject::$default_sort} if not provided.
* @param string|array $limit
* @param SQLQuery $existingQuery
* @return SQLQuery
*/
public function getQuery($searchParams, $start = false, $limit = false) {
public function getQuery($searchParams, $sort = false, $limit = false, $existingQuery = null) {
$model = singleton($this->modelClass);
$fields = $this->applyBaseTableFields($model);
$query = new SQLQuery($fields);
$baseTable = $this->applyBaseTable();
$query->from($baseTable);
if($limit) $query->limit = (!empty($start)) ? "{$start},{$limit}" : $limit;
// SRM: This stuff is copied from DataObject,
if($this->modelClass != $baseTable) {
$classNames = ClassInfo::subclassesFor($this->modelClass);
$query->where[] = "`$baseTable`.ClassName IN ('" . implode("','", $classNames) . "')";
if($existingQuery) {
$query = $existingQuery;
$query->select = array_merge($query->select,$fields);
} else {
$query = $model->buildSQL();
$query->select($fields);
}
$this->applyBaseTableJoin($query);
$SQL_limit = Convert::raw2sql($limit);
$query->limit($SQL_limit);
$SQL_sort = (!empty($sort)) ? Convert::raw2sql($sort) : singleton($this->modelClass)->stat('default_sort');
$query->orderby($SQL_sort);
foreach($searchParams as $key => $value) {
if ($value != '0') {
@ -145,6 +149,7 @@ class SearchContext extends Object {
}
}
}
return $query;
}
@ -154,14 +159,16 @@ class SearchContext extends Object {
* @todo rearrange start and limit params to reflect DataObject
*
* @param array $searchParams
* @param int $start
* @param int $limit
* @param string|array $sort
* @param string|array $limit
* @return DataObjectSet
*/
public function getResults($searchParams, $start = false, $limit = false) {
public function getResults($searchParams, $sort = false, $limit = false) {
$searchParams = array_filter($searchParams, array($this,'clearEmptySearchFields'));
$query = $this->getQuery($searchParams, $start, $limit);
$query = $this->getQuery($searchParams, $sort, $limit);
//Debug::dump($query->sql());
// use if a raw SQL query is needed
$results = new DataObjectSet();
foreach($query->execute() as $row) {

View File

@ -6,14 +6,25 @@ class DataObjectDecoratorTest extends SapphireTest {
function testOneToManyAssociationWithDecorator() {
$contact = new DataObjectDecoratorTest_Member();
$contact->Website = "http://www.example.com";
$object = new DataObjectDecoratorTest_RelatedObject();
$object->FieldOne = "Lorem ipsum dolor";
$object->FieldTwo = "Random notes";
/* The following code doesn't currently work:
$contact->RelatedObjects()->add($object);
$contact->write();
*/
/* Instead we have to do the following */
$contact->write();
$object->ContactID = $contact->ID;
$object->write();
unset($contact);
$contact = DataObject::get_one("DataObjectDecoratorTest_Member", "Website='http://www.example.com'");
$this->assertType('DataObjectDecoratorTest_RelatedObject', $contact->RelatedObjects()->First());
$this->assertEquals("Lorem ipsum dolor", $contact->RelatedObjects()->First()->FieldOne);
$this->assertEquals("Random notes", $contact->RelatedObjects()->First()->FieldTwo);
@ -50,11 +61,11 @@ class DataObjectDecoratorTest_RelatedObject extends DataObject implements TestOn
static $db = array(
"FieldOne" => "Text",
"FieldOne" => "Text"
"FieldTwo" => "Text"
);
static $has_one = array(
"Contact" => "Member"
"Contact" => "DataObjectDecoratorTest_Member"
);
}

View File

@ -55,18 +55,70 @@ class SQLQueryTest extends SapphireTest {
function testSelectWithPredicateFilters() {
$query = new SQLQuery();
$query->select(array("Name"))->from("MyTable");
$query->select(array("Name"))->from("SQLQueryTest_DO");
$match = new ExactMatchFilter("Name", "Value");
$match->setModel('SQLQueryTest_DO');
$match->apply($query);
$match = new PartialMatchFilter("Meta", "Value");
$match->setModel('SQLQueryTest_DO');
$match->apply($query);
$this->assertEquals("SELECT Name FROM MyTable WHERE (Name = 'Value') AND (Meta LIKE '%Value%')", $query->sql());
$this->assertEquals("SELECT Name FROM SQLQueryTest_DO WHERE (SQLQueryTest_DO.Name = 'Value') AND (SQLQueryTest_DO.Meta LIKE '%Value%')", $query->sql());
}
function testSelectWithLimitClause() {
// not implemented
// numeric limit
$query = new SQLQuery();
$query->from[] = "MyTable";
$query->limit("99");
$this->assertEquals("SELECT * FROM MyTable LIMIT 99", $query->sql());
// array limit
$query = new SQLQuery();
$query->from[] = "MyTable";
$query->limit(array('limit'=>99));
$this->assertEquals("SELECT * FROM MyTable LIMIT 99", $query->sql());
// array limit with start (MySQL specific)
$query = new SQLQuery();
$query->from[] = "MyTable";
$query->limit(array('limit'=>99, 'start'=>97));
$this->assertEquals("SELECT * FROM MyTable LIMIT 97,99", $query->sql());
}
function testSelectWithOrderbyClause() {
// numeric limit
$query = new SQLQuery();
$query->from[] = "MyTable";
$query->orderby('MyName ASC');
// can't escape as we don't know if ASC or DESC is appended
$this->assertEquals("SELECT * FROM MyTable ORDER BY MyName ASC", $query->sql());
// array limit
$query = new SQLQuery();
$query->from[] = "MyTable";
$query->orderby(array('sort'=>'MyName'));
$this->assertEquals("SELECT * FROM MyTable ORDER BY `MyName`", $query->sql());
// array limit with start (MySQL specific)
$query = new SQLQuery();
$query->from[] = "MyTable";
$query->orderby(array('sort'=>'MyName','dir'=>'desc'));
$this->assertEquals("SELECT * FROM MyTable ORDER BY `MyName` DESC", $query->sql());
}
function testSelectWithComplexOrderbyClause() {
// @todo Test "ORDER BY RANDOM() ASC,MyName DESC" etc.
}
}
class SQLQueryTest_DO extends DataObject implements TestOnly {
static $db = array(
"Name" => "Varchar",
"Meta" => "Varchar",
);
}
?>

View File

@ -30,15 +30,15 @@ class SearchContextTest extends SapphireTest {
$this->assertContains('Industry', $company->summary_fields());
}
function testExactMatchUsedByDefaultWhenNotExplicitlySet() {
function testPartialMatchUsedByDefaultWhenNotExplicitlySet() {
$person = singleton('SearchContextTest_Person');
$context = $person->getDefaultSearchContext();
$this->assertEquals(
array(
"Name" => new ExactMatchFilter("Name"),
"HairColor" => new ExactMatchFilter("HairColor"),
"EyeColor" => new ExactMatchFilter("EyeColor")
"Name" => new PartialMatchFilter("Name"),
"HairColor" => new PartialMatchFilter("HairColor"),
"EyeColor" => new PartialMatchFilter("EyeColor")
),
$context->getFilters()
);
@ -50,7 +50,7 @@ class SearchContextTest extends SapphireTest {
$this->assertEquals(
array(
"Title" => new ExactMatchFilter("Title")
"Title" => new PartialMatchFilter("Title")
),
$context->getFilters()
);
@ -63,7 +63,7 @@ class SearchContextTest extends SapphireTest {
$this->assertEquals(
array(
"Name" => new PartialMatchFilter("Name"),
"Industry" => new ExactMatchFilter("Industry"),
"Industry" => new PartialMatchFilter("Industry"),
"AnnualProfit" => new PartialMatchFilter("AnnualProfit")
),
$context->getFilters()
@ -80,7 +80,7 @@ class SearchContextTest extends SapphireTest {
$this->assertEquals(1, $results->Count());
Debug::dump(DB::query("select * from SearchContextTest_Deadline")->next());
//Debug::dump(DB::query("select * from SearchContextTest_Deadline")->next());
$project = $results->First();
@ -88,7 +88,7 @@ class SearchContextTest extends SapphireTest {
$this->assertEquals("Blog Website", $project->Name);
$this->assertEquals(2, $project->Actions()->Count());
$this->assertEquals("Get RSS feeds working", $project->Actions()->First()->Description);
Debug::dump($project->Deadline()->CompletionDate);
//Debug::dump($project->Deadline()->CompletionDate);
//$this->assertEquals()
}