Merged new-orm into datagrid

This commit is contained in:
Will Rossiter 2011-09-26 16:47:54 +13:00
commit 1732a17114
134 changed files with 5133 additions and 4743 deletions

View File

@ -26,7 +26,7 @@ abstract class CMSBatchAction extends Object {
* Run this action for the given set of pages.
* Return a set of status-updated JavaScript to return to the CMS.
*/
abstract function run(DataObjectSet $objs);
abstract function run(SS_List $objs);
/**
* Helper method for processing batch actions.
@ -46,7 +46,7 @@ abstract class CMSBatchAction extends Object {
* }
* }
*/
public function batchaction(DataObjectSet $objs, $helperMethod, $successMessage, $arguments = array()) {
public function batchaction(SS_List $objs, $helperMethod, $successMessage, $arguments = array()) {
$status = array('modified' => array(), 'error' => array());
foreach($objs as $obj) {

View File

@ -116,7 +116,7 @@ class CMSBatchActionHandler extends RequestHandler {
}
}
} else {
$pages = new DataObjectSet();
$pages = new ArrayList();
}
return $actionHandler->run($pages);
@ -173,7 +173,7 @@ class CMSBatchActionHandler extends RequestHandler {
*/
function batchActionList() {
$actions = $this->batchActions();
$actionList = new DataObjectSet();
$actionList = new ArrayList();
foreach($actions as $urlSegment => $action) {
$actionClass = $action['class'];

View File

@ -35,7 +35,7 @@ class GroupImportForm extends Form {
$importSpec = $importer->getImportSpec();
$helpHtml = sprintf($helpHtml, implode(', ', array_keys($importSpec['fields'])));
$fields = new FieldSet(
$fields = new FieldList(
new LiteralField('Help', $helpHtml),
$fileField = new FileField(
'CsvFile',
@ -48,7 +48,7 @@ class GroupImportForm extends Form {
$fileField->getValidator()->setAllowedExtensions(array('csv'));
}
if(!$actions) $actions = new FieldSet(
if(!$actions) $actions = new FieldList(
new FormAction('doImport', _t('SecurityAdmin_MemberImportForm.BtnImport', 'Import'))
);

View File

@ -437,10 +437,10 @@ class LeftAndMain extends Controller {
*/
public function MainMenu() {
// Don't accidentally return a menu if you're not logged in - it's used to determine access.
if(!Member::currentUser()) return new DataObjectSet();
if(!Member::currentUser()) return new ArrayList();
// Encode into DO set
$menu = new DataObjectSet();
$menu = new ArrayList();
$menuItems = CMSMenu::get_viewable_menu_items();
if($menuItems) {
foreach($menuItems as $code => $menuItem) {
@ -807,7 +807,7 @@ class LeftAndMain extends Controller {
* Calls {@link SiteTree->getCMSFields()}
*
* @param Int $id
* @param FieldSet $fields
* @param FieldList $fields
* @return Form
*/
public function getEditForm($id = null, $fields = null) {
@ -915,7 +915,7 @@ class LeftAndMain extends Controller {
$form = new Form(
$this,
"EditForm",
new FieldSet(
new FieldList(
// new HeaderField(
// 'WelcomeHeader',
// $this->getApplicationName()
@ -929,7 +929,7 @@ class LeftAndMain extends Controller {
// )
// )
),
new FieldSet()
new FieldList()
);
$form->unsetValidator();
$form->addExtraClass('cms-edit-form');
@ -949,10 +949,10 @@ class LeftAndMain extends Controller {
$form = new Form(
$this,
'AddForm',
new FieldSet(
new FieldList(
new HiddenField('ParentID')
),
new FieldSet(
new FieldList(
$addAction = new FormAction('doAdd', _t('AssetAdmin_left.ss.GO','Go'))
)
);
@ -1017,7 +1017,7 @@ class LeftAndMain extends Controller {
$form = new Form(
$this,
'BatchActionsForm',
new FieldSet(
new FieldList(
new HiddenField('csvIDs'),
new DropdownField(
'Action',
@ -1025,7 +1025,7 @@ class LeftAndMain extends Controller {
$actionsMap
)
),
new FieldSet(
new FieldList(
// TODO i18n
new FormAction('submit', "Go")
)

View File

@ -34,7 +34,7 @@ class MemberImportForm extends Form {
$importSpec = $importer->getImportSpec();
$helpHtml = sprintf($helpHtml, implode(', ', array_keys($importSpec['fields'])));
$fields = new FieldSet(
$fields = new FieldList(
new LiteralField('Help', $helpHtml),
$fileField = new FileField(
'CsvFile',
@ -47,7 +47,7 @@ class MemberImportForm extends Form {
$fileField->getValidator()->setAllowedExtensions(array('csv'));
}
if(!$actions) $actions = new FieldSet(
if(!$actions) $actions = new FieldList(
new FormAction('doImport', _t('SecurityAdmin_MemberImportForm.BtnImport', 'Import'))
);

View File

@ -33,8 +33,6 @@ class MemberTableField extends ComplexTableField {
public $itemClass = 'MemberTableField_Item';
static $data_class = 'Member';
/**
* Set the page size for this table.
* @var int
@ -60,8 +58,24 @@ class MemberTableField extends ComplexTableField {
* @param boolean $hidePassword Hide the password field or not in the summary?
*/
function __construct($controller, $name, $group = null, $members = null, $hidePassword = true) {
$sourceClass = self::$data_class;
$SNG_member = singleton($sourceClass);
if(!$members) {
if($group) {
if(is_numeric($group)) $group = DataObject::get_by_id('Group', $group);
$this->group = $group;
$members = $group->Members();
} elseif(isset($_REQUEST['ctf'][$this->Name()]["ID"]) && is_numeric($_REQUEST['ctf'][$this->Name()]["ID"])) {
throw new Exception("Is this still being used? It's a hack and we should remove it.");
$group = DataObject::get_by_id('Group', $_REQUEST['ctf'][$this->Name()]["ID"]);
$this->group = $group;
$members = $group->Members();
} else {
$members = DataObject::get("Member");
}
}
$SNG_member = singleton('Member');
$fieldList = $SNG_member->summaryFields();
$memberDbFields = $SNG_member->db();
$csvFieldList = array();
@ -70,49 +84,24 @@ class MemberTableField extends ComplexTableField {
$csvFieldList[$field] = $field;
}
if($group) {
if(is_object($group)) {
$this->group = $group;
} elseif(is_numeric($group)) {
$this->group = DataObject::get_by_id('Group', $group);
}
} else if(isset($_REQUEST['ctf'][$this->Name()]["ID"]) && is_numeric($_REQUEST['ctf'][$this->Name()]["ID"])) {
$this->group = DataObject::get_by_id('Group', $_REQUEST['ctf'][$this->Name()]["ID"]);
}
if(!$hidePassword) {
$fieldList["SetPassword"] = "Password";
}
$this->hidePassword = $hidePassword;
// @todo shouldn't this use $this->group? It's unclear exactly
// what group it should be customising the custom Member set with.
if($members && $group) {
$this->setCustomSourceItems($this->memberListWithGroupID($members, $group));
}
parent::__construct($controller, $name, $sourceClass, $fieldList);
// Add a search filter
$SQL_search = isset($_REQUEST['MemberSearch']) ? Convert::raw2sql($_REQUEST['MemberSearch']) : null;
if(!empty($_REQUEST['MemberSearch'])) {
$searchFilters = array();
foreach($SNG_member->searchableFields() as $fieldName => $fieldSpec) {
if(strpos($fieldName, '.') === false) $searchFilters[] = "\"$fieldName\" LIKE '%{$SQL_search}%'";
}
$this->sourceFilter[] = '(' . implode(' OR ', $searchFilters) . ')';
$members = $members->where('(' . implode(' OR ', $searchFilters) . ')');
}
if($this->group) {
$groupIDs = array($this->group->ID);
if($this->group->AllChildren()) $groupIDs = array_merge($groupIDs, $this->group->AllChildren()->column('ID'));
$this->sourceFilter[] = sprintf(
'"Group_Members"."GroupID" IN (%s)',
implode(',', $groupIDs)
);
}
parent::__construct($controller, $name, $members, $fieldList);
$this->sourceJoin = " INNER JOIN \"Group_Members\" ON \"MemberID\"=\"Member\".\"ID\"";
$this->setFieldListCsv($csvFieldList);
$this->setPageSize($this->stat('page_size'));
}
@ -127,14 +116,6 @@ class MemberTableField extends ComplexTableField {
return $ret;
}
function sourceID() {
return ($this->group) ? $this->group->ID : 0;
}
function AddLink() {
return Controller::join_links($this->Link(), 'add');
}
function SearchForm() {
$groupID = (isset($this->group)) ? $this->group->ID : 0;
$query = isset($_GET['MemberSearch']) ? $_GET['MemberSearch'] : null;
@ -165,6 +146,7 @@ class MemberTableField extends ComplexTableField {
if(!$token->checkRequest($this->controller->getRequest())) return $this->httpError(400);
$data = $_REQUEST;
$groupID = (isset($data['ctf']['ID'])) ? $data['ctf']['ID'] : null;
if(!is_numeric($groupID)) {
@ -174,7 +156,7 @@ class MemberTableField extends ComplexTableField {
// Get existing record either by ID or unique identifier.
$identifierField = Member::get_unique_identifier_field();
$className = self::$data_class;
$className = 'Member';
$record = null;
if(isset($data[$identifierField])) {
$record = DataObject::get_one(
@ -201,7 +183,7 @@ class MemberTableField extends ComplexTableField {
$valid = $record->validate();
if($valid->valid()) {
$record->write();
$record->Groups()->add($groupID);
$this->getDataList()->add($record);
$this->sourceItems();
@ -229,68 +211,12 @@ class MemberTableField extends ComplexTableField {
return FormResponse::respond();
}
/**
* Custom delete implementation:
* Remove member from group rather than from the database
*/
function delete() {
// Protect against CSRF on destructive action
$token = $this->getForm()->getSecurityToken();
// TODO Not sure how this is called, using $_REQUEST to be on the safe side
if(!$token->check($_REQUEST['SecurityID'])) return $this->httpError(400);
$groupID = Convert::raw2sql($_REQUEST['ctf']['ID']);
$memberID = Convert::raw2sql($_REQUEST['ctf']['childID']);
if(is_numeric($groupID) && is_numeric($memberID)) {
$member = DataObject::get_by_id('Member', $memberID);
$member->Groups()->remove($groupID);
} else {
user_error("MemberTableField::delete: Bad parameters: Group=$groupID, Member=$memberID", E_USER_ERROR);
}
return FormResponse::respond();
}
/**
* #################################
* Utility Functions
* #################################
*/
function getParentClass() {
return 'Group';
}
function getParentIdName($childClass, $parentClass) {
return 'GroupID';
}
/**
* #################################
* Custom Functions
* #################################
*/
/**
* Customise an existing DataObjectSet of Member
* objects with a GroupID.
*
* @param DataObjectSet $members Set of Member objects to customise
* @param Group $group Group object to customise with
* @return DataObjectSet Customised set of Member objects
*/
function memberListWithGroupID($members, $group) {
$newMembers = new DataObjectSet();
foreach($members as $member) {
$newMembers->push($member->customise(array('GroupID' => $group->ID)));
}
return $newMembers;
}
function setGroup($group) {
$this->group = $group;
}
/**
* @return Group
*/
@ -298,19 +224,11 @@ class MemberTableField extends ComplexTableField {
return $this->group;
}
function setController($controller) {
$this->controller = $controller;
}
function GetControllerName() {
return $this->controller->class;
}
/**
* Add existing member to group by name (with JS-autocompletion)
*/
function AddRecordForm() {
$fields = new FieldSet();
$fields = new FieldList();
foreach($this->FieldList() as $fieldName => $fieldTitle) {
// If we're adding the set password field, we want to hide the text from any peeping eyes
if($fieldName == 'SetPassword') {
@ -322,7 +240,7 @@ class MemberTableField extends ComplexTableField {
if($this->group) {
$fields->push(new HiddenField('ctf[ID]', null, $this->group->ID));
}
$actions = new FieldSet(
$actions = new FieldList(
new FormAction('addtogroup', _t('MemberTableField.ADD','Add'))
);
@ -387,66 +305,6 @@ class MemberTableField extends ComplexTableField {
$this->controller->redirectBack();
}
/**
* Cached version for getting the appropraite members for this particular group.
*
* This includes getting inherited groups, such as groups under groups.
*/
function sourceItems() {
// Caching.
if($this->sourceItems) {
return $this->sourceItems;
}
// Setup limits
$limitClause = '';
if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) {
$limitClause = ($_REQUEST['ctf'][$this->Name()]['start']) . ", {$this->pageSize}";
} else {
$limitClause = "0, {$this->pageSize}";
}
// We use the group to get the members, as they already have the bulk of the look up functions
$start = isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0;
$this->sourceItems = false;
if($this->group) {
$this->sourceItems = $this->group->Members(
$this->pageSize, // limit
$start, // offset
$this->sourceFilter,
$this->sourceSort
);
} else {
$this->sourceItems = DataObject::get(self::$data_class,
$this->sourceFilter,
$this->sourceSort,
null,
array('limit' => $this->pageSize, 'start' => $start)
);
}
// Because we are not used $this->upagedSourceItems any more, and the DataObjectSet is usually the source
// that a large member set runs out of memory. we disable it here.
//$this->unpagedSourceItems = $this->group->Members('', '', $this->sourceFilter, $this->sourceSort);
$this->totalCount = ($this->sourceItems) ? $this->sourceItems->TotalItems() : 0;
return $this->sourceItems;
}
function TotalCount() {
$this->sourceItems(); // Called for its side-effect of setting total count
return $this->totalCount;
}
/**
* Handles item requests
* MemberTableField needs its own item request class so that it can overload the delete method
*/
function handleItem($request) {
return new MemberTableField_ItemRequest($this, $request->param('ID'));
}
}
/**
@ -457,7 +315,7 @@ class MemberTableField extends ComplexTableField {
class MemberTableField_Popup extends ComplexTableField_Popup {
function __construct($controller, $name, $fields, $validator, $readonly, $dataObject) {
$group = ($controller instanceof MemberTableField) ? $controller->getGroup() : $controller->getParent()->getGroup();
$group = ($controller instanceof MemberTableField) ? $controller->getGroup() : $controller->getParentController()->getGroup();
// Set default groups - also implemented in AddForm()
if($group) {
$groupsField = $fields->dataFieldByName('Groups');
@ -510,44 +368,4 @@ class MemberTableField_Item extends ComplexTableField_Item {
}
}
/**
* @package cms
* @subpackage security
*/
class MemberTableField_ItemRequest extends ComplexTableField_ItemRequest {
/**
* Deleting an item from a member table field should just remove that member from the group
*/
function delete($request) {
// Protect against CSRF on destructive action
$token = $this->ctf->getForm()->getSecurityToken();
if(!$token->checkRequest($request)) return $this->httpError('400');
if($this->ctf->Can('delete') !== true) {
return false;
}
// if a group limitation is set on the table, remove relation.
// otherwise remove the record from the database
if($this->ctf->getGroup()) {
$groupID = $this->ctf->sourceID();
$group = DataObject::get_by_id('Group', $groupID);
// Remove from group and all child groups
foreach($group->getAllChildren() as $subGroup) {
$this->dataObj()->Groups()->remove($subGroup);
}
$this->dataObj()->Groups()->remove($groupID);
} else {
$this->dataObj()->delete();
}
}
function getParent() {
return $this->ctf;
}
}
?>

View File

@ -248,7 +248,7 @@ abstract class ModelAdmin extends LeftAndMain {
*/
protected function getModelForms() {
$models = $this->getManagedModels();
$forms = new DataObjectSet();
$forms = new ArrayList();
foreach($models as $class => $options) {
if(is_numeric($class)) $class = $options;
@ -402,7 +402,7 @@ class ModelAdmin_CollectionController extends Controller {
$form = new Form($this, "SearchForm",
$fields,
new FieldSet(
new FieldList(
new FormAction('search', _t('MemberTableField.SEARCH', 'Search')),
$clearAction = new ResetFormAction('clearsearch', _t('ModelAdmin.CLEAR_SEARCH','Clear Search'))
),
@ -434,8 +434,8 @@ class ModelAdmin_CollectionController extends Controller {
$buttonLabel = sprintf(_t('ModelAdmin.CREATEBUTTON', "Create '%s'", PR_MEDIUM, "Create a new instance from a model class"), singleton($modelName)->i18n_singular_name());
$form = new Form($this, "CreateForm",
new FieldSet(),
new FieldSet($createButton = new FormAction('add', $buttonLabel)),
new FieldList(),
new FieldList($createButton = new FormAction('add', $buttonLabel)),
$validator = new RequiredFields()
);
$createButton->addExtraClass('ss-ui-action-constructive');
@ -467,7 +467,7 @@ class ModelAdmin_CollectionController extends Controller {
if(!singleton($modelName)->canCreate(Member::currentUser())) return false;
$fields = new FieldSet(
$fields = new FieldList(
new HiddenField('ClassName', _t('ModelAdmin.CLASSTYPE'), $modelName),
new FileField('_CsvFile', false)
);
@ -476,11 +476,11 @@ class ModelAdmin_CollectionController extends Controller {
$importerClass = $importers[$modelName];
$importer = new $importerClass($modelName);
$spec = $importer->getImportSpec();
$specFields = new DataObjectSet();
$specFields = new ArrayList();
foreach($spec['fields'] as $name => $desc) {
$specFields->push(new ArrayData(array('Name' => $name, 'Description' => $desc)));
}
$specRelations = new DataObjectSet();
$specRelations = new ArrayList();
foreach($spec['relations'] as $name => $desc) {
$specRelations->push(new ArrayData(array('Name' => $name, 'Description' => $desc)));
}
@ -493,7 +493,7 @@ class ModelAdmin_CollectionController extends Controller {
$fields->push(new LiteralField("SpecFor{$modelName}", $specHTML));
$fields->push(new CheckboxField('EmptyBeforeImport', 'Clear Database before import', false));
$actions = new FieldSet(
$actions = new FieldList(
new FormAction('import', _t('ModelAdmin.IMPORT', 'Import from CSV'))
);
@ -766,11 +766,11 @@ class ModelAdmin_CollectionController extends Controller {
$form = new Form(
$this,
'ResultsForm',
new FieldSet(
new FieldList(
new HeaderField('SearchResults', _t('ModelAdmin.SEARCHRESULTS','Search Results'), 2),
$tf
),
new FieldSet()
new FieldList()
);
// Include the search criteria on the results form URL, but not dodgy variables like those below
@ -839,7 +839,7 @@ class ModelAdmin_CollectionController extends Controller {
if(!$validator) $validator = new RequiredFields();
$validator->setJavascriptValidationHandler('none');
$actions = new FieldSet (
$actions = new FieldList (
new FormAction("doCreate", _t('ModelAdmin.ADDBUTTON', "Add"))
);
@ -1036,7 +1036,7 @@ class ModelAdmin_RecordController extends Controller {
*/
public function ViewForm() {
$fields = $this->currentRecord->getCMSFields();
$form = new Form($this, "EditForm", $fields, new FieldSet());
$form = new Form($this, "EditForm", $fields, new FieldList());
$form->loadDataFrom($this->currentRecord);
$form->makeReadonly();
return $form;

View File

@ -109,9 +109,8 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
);
// unset 'inlineadd' permission, we don't want inline addition
$memberList->setPermissions(array('edit', 'delete', 'add'));
$memberList->setRelationAutoSetting(false);
$fields = new FieldSet(
$fields = new FieldList(
new TabSet(
'Root',
new Tab('Members', singleton('Member')->i18n_plural_name(),
@ -156,7 +155,7 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
$rolesTab->push($rolesCTF);
}
$actions = new FieldSet(
$actions = new FieldList(
new FormAction('addmember',_t('SecurityAdmin.ADDMEMBER','Add Member'))
);
@ -359,7 +358,7 @@ class SecurityAdmin_DeleteBatchAction extends CMSBatchAction {
return _t('AssetAdmin_DeleteBatchAction.TITLE', 'Delete groups');
}
function run(DataObjectSet $records) {
function run(SS_List $records) {
$status = array(
'modified'=>array(),
'deleted'=>array()

View File

@ -305,7 +305,6 @@ MemberFilterButton.prototype = {
updateURL += '&' + this.inputFields[index].name + '=' + encodeURIComponent( this.inputFields[index].value );
}
}
updateURL += ($('SecurityID') ? '&SecurityID=' + $('SecurityID').value : '');
jQuery($(fieldID)).get(updateURL, null, function() {Behaviour.apply($(fieldID), true);});
} catch(er) {

View File

@ -116,8 +116,8 @@ class MemberTableFieldTest_Controller extends Controller implements TestOnly {
return new Form(
$this,
'FormNoGroup',
new FieldSet(new MemberTableField($this, "Members", $group1)),
new FieldSet(new FormAction('submit'))
new FieldList(new MemberTableField($this, "Members", $group1)),
new FieldList(new FormAction('submit'))
);
}
@ -131,8 +131,8 @@ class MemberTableFieldTest_Controller extends Controller implements TestOnly {
return new Form(
$this,
'FormNoGroup',
new FieldSet(new MemberTableField($this, "Members")),
new FieldSet(new FormAction('submit'))
new FieldList(new MemberTableField($this, "Members")),
new FieldList(new FormAction('submit'))
);
}

View File

@ -288,7 +288,7 @@ abstract class DataFormatter extends Object {
/**
* Convert a data object set to this format. Return a string.
*/
abstract function convertDataObjectSet(DataObjectSet $set);
abstract function convertDataObjectSet(SS_List $set);
/**
* @param string $strData HTTP Payload as string

View File

@ -121,7 +121,7 @@ class JSONDataFormatter extends DataFormatter {
* @param DataObjectSet $set
* @return String XML
*/
public function convertDataObjectSet(DataObjectSet $set, $fields = null) {
public function convertDataObjectSet(SS_List $set, $fields = null) {
$items = array();
foreach ($set as $do) $items[] = $this->convertDataObjectToJSONObject($do, $fields);

View File

@ -99,7 +99,7 @@ class RSSFeed extends ViewableData {
* @param string $etag The ETag is an unique identifier that is changed
* every time the representation does
*/
function __construct(DataObjectSet $entries, $link, $title,
function __construct(SS_List $entries, $link, $title,
$description = null, $titleField = "Title",
$descriptionField = "Content", $authorField = null,
$lastModified = null, $etag = null) {
@ -137,7 +137,7 @@ class RSSFeed extends ViewableData {
* @return DataObjectSet Returns the {@link RSSFeed_Entry} objects.
*/
function Entries() {
$output = new DataObjectSet();
$output = new ArrayList();
if(isset($this->entries)) {
foreach($this->entries as $entry) {
$output->push(new RSSFeed_Entry($entry, $this->titleField, $this->descriptionField, $this->authorField));

View File

@ -221,26 +221,19 @@ class RestfulServer extends Controller {
// depending on the request
if($id) {
// Format: /api/v1/<MyClass>/<ID>
$query = $this->getObjectQuery($className, $id, $params);
$obj = singleton($className)->buildDataObjectSet($query->execute());
$obj = $this->getObjectQuery($className, $id, $params)->First();
if(!$obj) return $this->notFound();
$obj = $obj->First();
if(!$obj->canView()) return $this->permissionFailure();
// Format: /api/v1/<MyClass>/<ID>/<Relation>
if($relationName) {
$query = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName);
if($query === false) return $this->notFound();
$obj = singleton($className)->buildDataObjectSet($query->execute());
$obj = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName);
if(!$obj) return $this->notFound();
}
} else {
// Format: /api/v1/<MyClass>
$query = $this->getObjectsQuery($className, $params, $sort, $limit);
$obj = singleton($className)->buildDataObjectSet($query->execute());
// show empty serialized result when no records are present
if(!$obj) $obj = new DataObjectSet();
$obj = $this->getObjectsQuery($className, $params, $sort, $limit);
}
$this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
@ -248,12 +241,12 @@ class RestfulServer extends Controller {
$rawFields = $this->request->getVar('fields');
$fields = $rawFields ? explode(',', $rawFields) : null;
if($obj instanceof DataObjectSet) {
$responseFormatter->setTotalSize($query->unlimitedRowCount());
if($obj instanceof SS_List) {
$responseFormatter->setTotalSize($obj->dataQuery()->query()->unlimitedRowCount());
return $responseFormatter->convertDataObjectSet($obj, $fields);
} else if(!$obj) {
$responseFormatter->setTotalSize(0);
return $responseFormatter->convertDataObjectSet(new DataObjectSet(), $fields);
return $responseFormatter->convertDataObjectSet(new ArrayList(), $fields);
} else {
return $responseFormatter->convertDataObject($obj, $fields);
}
@ -277,9 +270,8 @@ class RestfulServer extends Controller {
} else {
$searchContext = singleton($className)->getDefaultSearchContext();
}
$query = $searchContext->getQuery($params, $sort, $limit, $existingQuery);
return $query;
return $searchContext->getQuery($params, $sort, $limit, $existingQuery);
}
/**
@ -499,13 +491,10 @@ class RestfulServer extends Controller {
* @param string $className
* @param int $id
* @param array $params
* @return SQLQuery
* @return DataList
*/
protected function getObjectQuery($className, $id, $params) {
$baseClass = ClassInfo::baseDataClass($className);
return singleton($className)->extendedSQL(
"\"$baseClass\".\"ID\" = {$id}"
);
return DataList::create($className)->byIDs(array($id));
}
/**
@ -529,24 +518,11 @@ class RestfulServer extends Controller {
* @return SQLQuery|boolean
*/
protected function getObjectRelationQuery($obj, $params, $sort, $limit, $relationName) {
if($obj->hasMethod("{$relationName}Query")) {
// @todo HACK Switch to ComponentSet->getQuery() once we implement it (and lazy loading)
$query = $obj->{"{$relationName}Query"}(null, $sort, null, $limit);
$relationClass = $obj->{"{$relationName}Class"}();
} elseif($relationClass = $obj->many_many($relationName)) {
// many_many() returns different notation
$relationClass = $relationClass[1];
$query = $obj->getManyManyComponentsQuery($relationName);
} elseif($relationClass = $obj->has_many($relationName)) {
$query = $obj->getComponentsQuery($relationName);
} elseif($relationClass = $obj->has_one($relationName)) {
$query = null;
} else {
return false;
// The relation method will return a DataList, that getSearchQuery subsequently manipulates
if($obj->hasMethod($relationName)) {
$query = $obj->$relationName();
return $this->getSearchQuery($query->dataClass(), $params, $sort, $limit, $query);
}
// get all results
return $this->getSearchQuery($relationClass, $params, $sort, $limit, $query);
}
protected function permissionFailure() {

View File

@ -250,7 +250,7 @@ class RestfulService extends ViewableData {
public function getAttributes($xml, $collection=NULL, $element=NULL){
$xml = new SimpleXMLElement($xml);
$output = new DataObjectSet();
$output = new ArrayList();
if($collection)
$childElements = $xml->{$collection};
@ -305,7 +305,7 @@ class RestfulService extends ViewableData {
public function getValues($xml, $collection=NULL, $element=NULL){
$xml = new SimpleXMLElement($xml);
$output = new DataObjectSet();
$output = new ArrayList();
$childElements = $xml;
if($collection)
@ -386,7 +386,7 @@ class RestfulService extends ViewableData {
*/
function searchAttributes($xml, $node=NULL){
$xml = new SimpleXMLElement($xml);
$output = new DataObjectSet();
$output = new ArrayList();
$childElements = $xml->xpath($node);

View File

@ -65,12 +65,12 @@ class SapphireSoapServer extends Controller {
}
$methods[] = new ArrayData(array(
"Name" => $methodName,
"Arguments" => new DataObjectSet($processedArguments),
"Arguments" => new ArrayList($processedArguments),
"ReturnType" => self::$xsd_types[$returnType],
));
}
return new DataObjectSet($methods);
return new ArrayList($methods);
}
/**

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

@ -132,7 +132,7 @@ class XMLDataFormatter extends DataFormatter {
* @param DataObjectSet $set
* @return String XML
*/
public function convertDataObjectSet(DataObjectSet $set, $fields = null) {
public function convertDataObjectSet(SS_List $set, $fields = null) {
Controller::curr()->getResponse()->addHeader("Content-Type", "text/xml");
$className = $set->class;

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

@ -63,7 +63,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'])) {
@ -103,7 +103,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
@ -202,7 +202,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;
@ -227,7 +228,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);
@ -260,7 +261,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);

407
core/PaginatedList.php Normal file
View File

@ -0,0 +1,407 @@
<?php
/**
* A decorator that wraps around a data list in order to provide pagination.
*
* @package sapphire
* @subpackage view
*/
class PaginatedList extends SS_ListDecorator {
protected $request;
protected $getVar = 'start';
protected $pageLength = 10;
protected $pageStart;
protected $totalItems;
/**
* Constructs a new paginated list instance around a list.
*
* @param SS_List $list The list to paginate. The getRange method will
* be used to get the subset of objects to show.
* @param array|ArrayAccess Either a map of request parameters or
* request object that the pagination offset is read from.
*/
public function __construct(SS_List $list, $request = array()) {
if (!is_array($request) && !$request instanceof ArrayAccess) {
throw new Exception('The request must be readable as an array.');
}
$this->request = $request;
parent::__construct($list);
}
/**
* Returns the GET var that is used to set the page start. This defaults
* to "start".
*
* If there is more than one paginated list on a page, it is neccesary to
* set a different get var for each using {@link setPaginationGetVar()}.
*
* @return string
*/
public function getPaginationGetVar() {
return $this->getVar;
}
/**
* Sets the GET var used to set the page start.
*
* @param string $var
*/
public function setPaginationGetVar($var) {
$this->getVar = $var;
}
/**
* Returns the number of items displayed per page. This defaults to 10.
*
* @return int.
*/
public function getPageLength() {
return $this->pageLength;
}
/**
* Set the number of items displayed per page.
*
* @param int $length
*/
public function setPageLength($length) {
$this->pageLength = $length;
}
/**
* Sets the current page.
*
* @param int $page
*/
public function setCurrentPage($page) {
$this->pageStart = ($page - 1) * $this->pageLength;
}
/**
* Returns the offset of the item the current page starts at.
*
* @return int
*/
public function getPageStart() {
if ($this->pageStart === null) {
if ($this->request && isset($this->request[$this->getVar])) {
$this->pageStart = (int) $this->request[$this->getVar];
} else {
$this->pageStart = 0;
}
}
return $this->pageStart;
}
/**
* Sets the offset of the item that current page starts at. This should be
* a multiple of the page length.
*
* @param int $start
*/
public function setPageStart($start) {
$this->pageStart = $start;
}
/**
* Returns the total number of items in the unpaginated list.
*
* @return int
*/
public function getTotalItems() {
if ($this->totalItems === null) {
$this->totalItems = count($this->list);
}
return $this->totalItems;
}
/**
* Sets the total number of items in the list. This is useful when doing
* custom pagination.
*
* @param int $items
*/
public function setTotalItems($items) {
$this->totalItems = $items;
}
/**
* Sets the page length, page start and total items from a query object's
* limit, offset and unlimited count. The query MUST have a limit clause.
*
* @param SQLQuery $query
*/
public function setPaginationFromQuery(SQLQuery $query) {
if ($query->limit) {
$this->setPageLength($query->limit['limit']);
$this->setPageStart($query->limit['start']);
$this->setTotalItems($query->unlimitedRowCount());
}
}
/**
* @return IteratorIterator
*/
public function getIterator() {
return new IteratorIterator(
$this->list->getRange($this->getPageStart(), $this->pageLength)
);
}
/**
* Returns a set of links to all the pages in the list. This is useful for
* basic pagination.
*
* By default it returns links to every page, but if you pass the $max
* parameter the number of pages will be limited to that number, centered
* around the current page.
*
* @param int $max
* @return DataObjectSet
*/
public function Pages($max = null) {
$result = new ArrayList();
if ($max) {
$start = ($this->CurrentPage() - floor($max / 2)) - 1;
$end = $this->CurrentPage() + floor($max / 2);
if ($start < 0) {
$start = 0;
$end = $max;
}
if ($end > $this->TotalPages()) {
$end = $this->TotalPages();
$start = max(0, $end - $max);
}
} else {
$start = 0;
$end = $this->TotalPages();
}
for ($i = $start; $i < $end; $i++) {
$result->push(new ArrayData(array(
'PageNum' => $i + 1,
'Link' => HTTP::setGetVar($this->getVar, $i * $this->pageLength),
'CurrentBool' => $this->CurrentPage() == ($i + 1)
)));
}
return $result;
}
/**
* Returns a summarised pagination which limits the number of pages shown
* around the current page for visually balanced.
*
* Example: 25 pages total, currently on page 6, context of 4 pages
* [prev] [1] ... [4] [5] [[6]] [7] [8] ... [25] [next]
*
* Example template usage:
* <code>
* <% if MyPages.MoreThanOnePage %>
* <% if MyPages.NotFirstPage %>
* <a class="prev" href="$MyPages.PrevLink">Prev</a>
* <% end_if %>
* <% control MyPages.PaginationSummary(4) %>
* <% if CurrentBool %>
* $PageNum
* <% else %>
* <% if Link %>
* <a href="$Link">$PageNum</a>
* <% else %>
* ...
* <% end_if %>
* <% end_if %>
* <% end_control %>
* <% if MyPages.NotLastPage %>
* <a class="next" href="$MyPages.NextLink">Next</a>
* <% end_if %>
* <% end_if %>
* </code>
*
* @param int $context The number of pages to display around the current
* page. The number should be event, as half the number of each pages
* are displayed on either side of the current one.
* @return DataObjectSet
*/
public function PaginationSummary($context = 4) {
$result = new ArrayList();
$current = $this->CurrentPage();
$total = $this->TotalPages();
// Make the number even for offset calculations.
if ($context % 2) {
$context--;
}
// If the first or last page is current, then show all context on one
// side of it - otherwise show half on both sides.
if ($current == 1 || $current == $total) {
$offset = $context;
} else {
$offset = floor($context / 2);
}
$left = max($current - $offset, 1);
$range = range($current - $offset, $current + $offset);
if ($left + $context > $total) {
$left = $total - $context;
}
for ($i = 0; $i < $total; $i++) {
$link = HTTP::setGetVar($this->getVar, $i * $this->pageLength);
$num = $i + 1;
$emptyRange = $num != 1 && $num != $total && (
$num == $left - 1 || $num == $left + $context + 1
);
if ($emptyRange) {
$result->push(new ArrayData(array(
'PageNum' => null,
'Link' => null,
'CurrentBool' => false
)));
} elseif ($num == 1 || $num == $total || in_array($num, $range)) {
$result->push(new ArrayData(array(
'PageNum' => $num,
'Link' => $link,
'CurrentBool' => $current == $num
)));
}
}
return $result;
}
/**
* @return int
*/
public function CurrentPage() {
return floor($this->getPageStart() / $this->pageLength) + 1;
}
/**
* @return int
*/
public function TotalPages() {
return ceil($this->getTotalItems() / $this->pageLength);
}
/**
* @return bool
*/
public function MoreThanOnePage() {
return $this->TotalPages() > 1;
}
/**
* @return bool
*/
public function NotFirstPage() {
return $this->CurrentPage() != 1;
}
/**
* @return bool
*/
public function NotLastPage() {
return $this->CurrentPage() != $this->TotalPages();
}
/**
* Returns the number of the first item being displayed on the current
* page. This is useful for things like "displaying 10-20".
*
* @return int
*/
public function FirstItem() {
return ($start = $this->getPageStart()) ? $start + 1 : 1;
}
/**
* Returns the number of the last item being displayed on this page.
*
* @return int
*/
public function LastItem() {
if ($start = $this->getPageStart()) {
return min($start + $this->pageLength, $this->getTotalItems());
} else {
return min($this->pageLength, $this->getTotalItems());
}
}
/**
* Returns a link to the first page.
*
* @return string
*/
public function FirstLink() {
return HTTP::setGetVar($this->getVar, 0);
}
/**
* Returns a link to the last page.
*
* @return string
*/
public function LastLink() {
return HTTP::setGetVar($this->getVar, ($this->TotalPages() - 1) * $this->pageLength);
}
/**
* Returns a link to the next page, if there is another page after the
* current one.
*
* @return string
*/
public function NextLink() {
if ($this->NotLastPage()) {
return HTTP::setGetVar($this->getVar, $this->getPageStart() + $this->pageLength);
}
}
/**
* Returns a link to the previous page, if the first page is not currently
* active.
*
* @return string
*/
public function PrevLink() {
if ($this->NotFirstPage()) {
return HTTP::setGetVar($this->getVar, $this->getPageStart() - $this->pageLength);
}
}
// DEPRECATED --------------------------------------------------------------
/**
* @deprecated 3.0 Use individual getter methods.
*/
public function getPageLimits() {
return array(
'pageStart' => $this->getPageStart(),
'pageLength' => $this->pageLength,
'totalSize' => $this->getTotalItems(),
);
}
/**
* @deprecated 3.0 Use individual setter methods.
*/
public function setPageLimits($pageStart, $pageLength, $totalSize) {
$this->setPageStart($pageStart);
$this->setPageLength($pageLength);
$this->setTotalSize($totalSize);
}
}

View File

@ -35,6 +35,13 @@ class SS_ClassLoader {
return $this->manifests[count($this->manifests) - 1];
}
/**
* Returns true if this class loader has a manifest.
*/
public function hasManifest() {
return (bool)$this->manifests;
}
/**
* Pushes a class manifest instance onto the top of the stack. This will
* also include any module configuration files at the same time.

View File

@ -140,15 +140,7 @@ abstract class BulkLoader extends ViewableData {
//get all instances of the to be imported data object
if($this->deleteExistingRecords) {
$q = singleton($this->objectClass)->buildSQL();
$q->select = array('"ID"');
$ids = $q->execute()->column('ID');
foreach($ids as $id) {
$obj = DataObject::get_by_id($this->objectClass, $id);
$obj->delete();
$obj->destroy();
unset($obj);
}
DataObject::get($this->objectClass)->removeAll();
}
return $this->processAll($filepath);
@ -407,7 +399,7 @@ class BulkLoader_Result extends Object {
* @return DataObjectSet
*/
protected function mapToDataObjectSet($arr) {
$set = new DataObjectSet();
$set = new ArrayList();
foreach($arr as $arrItem) {
$obj = DataObject::get_by_id($arrItem['ClassName'], $arrItem['ID']);
$obj->_BulkLoaderMessage = $arrItem['Message'];

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

@ -43,7 +43,7 @@ class ModelViewer extends Controller {
function Models() {
$classes = ClassInfo::subclassesFor('DataObject');
array_shift($classes);
$output = new DataObjectSet();
$output = new ArrayList();
foreach($classes as $class) {
$output->push(new ModelViewer_Model($class));
}
@ -60,7 +60,7 @@ class ModelViewer extends Controller {
$modules = array();
foreach($classes as $class) {
$model = new ModelViewer_Model($class);
if(!isset($modules[$model->Module])) $modules[$model->Module] = new DataObjectSet();
if(!isset($modules[$model->Module])) $modules[$model->Module] = new ArrayList();
$modules[$model->Module]->push($model);
}
ksort($modules);
@ -70,7 +70,7 @@ class ModelViewer extends Controller {
$modules = array($this->module => $modules[$this->module]);
}
$output = new DataObjectSet();
$output = new ArrayList();
foreach($modules as $moduleName => $models) {
$output->push(new ArrayData(array(
'Link' => 'dev/viewmodel/' . $moduleName,
@ -149,7 +149,7 @@ class ModelViewer_Model extends ViewableData {
}
function Fields() {
$output = new DataObjectSet();
$output = new ArrayList();
$output->push(new ModelViewer_Field($this,'ID', 'PrimaryKey'));
if(!$this->ParentModel) {
@ -165,7 +165,7 @@ class ModelViewer_Model extends ViewableData {
}
function Relations() {
$output = new DataObjectSet();
$output = new ArrayList();
foreach(array('has_one','has_many','many_many') as $relType) {
$items = singleton($this->className)->uninherited($relType,true);

View File

@ -120,6 +120,8 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
*/
protected $fixtures;
protected $model;
function setUp() {
// We cannot run the tests on this abstract class.
if(get_class($this) == "SapphireTest") self::$skip_test = true;
@ -167,6 +169,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$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()) {
if(substr(DB::getConn()->currentDatabase(), 0, strlen($prefix) + 5) != strtolower(sprintf('%stmpdb', $prefix))) {
@ -200,7 +205,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
@ -679,7 +684,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
*/
private function dataObjectArrayMatch($item, $match) {
foreach($match as $k => $v) {
if(!isset($item[$k]) || $item[$k] != $v) return false;
if(!array_key_exists($k, $item) || $item[$k] != $v) return false;
}
return true;
}

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

@ -0,0 +1,66 @@
# Paginating A List
Adding pagination to a `[api:DataList]` or `[DataObjectSet]` is quite simple. All
you need to do is wrap the object in a `[api:PaginatedList]` decorator, which takes
care of fetching a sub-set of the total list and presenting it to the template.
In order to create a paginated list, you can create a method on your controller
that first creates a `DataList` that will return all pages, and then wraps it
in a `[api:PaginatedSet]` object. The `PaginatedList` object is also passed the
HTTP request object so it can read the current page information from the
"?start=" GET var.
The paginator will automatically set up query limits and read the request for
information.
:::php
/**
* Returns a paginated list of all pages in the site.
*/
public function PaginatedPages() {
$pages = DataList::create('Page');
return new PaginatedList($pages, $this->request);
}
## Setting Up The Template
Now all that remains is to render this list into a template, along with pagination
controls. There are two ways to generate pagination controls:
`[api:PaginatedSet->Pages()]` and `[api:PaginatedSet->PaginationSummary()]`. In
this example we will use `PaginationSummary()`.
The first step is to simply list the objects in the template:
:::ss
<ul>
<% control PaginatedPages %>
<li><a href="$Link">$Title</a></li>
<% end_control %>
</ul>
By default this will display 10 pages at a time. The next step is to add pagination
controls below this so the user can switch between pages:
:::ss
<% if PaginatedPages.MoreThanOnePage %>
<% if PaginatedPages.NotFirstPage %>
<a class="prev" href="$PaginatedPages.PrevLink">Prev</a>
<% end_if %>
<% control PaginatedPages.Pages %>
<% if CurrentBool %>
$PageNum
<% else %>
<% if Link %>
<a href="$Link">$PageNum</a>
<% else %>
...
<% end_if %>
<% end_if %>
<% end_control %>
<% if PaginatedPages.NotLastPage %>
<a class="next" href="$PaginatedPages.NextLink">Next</a>
<% end_if %>
<% end_if %>
If there is more than one page, this block will render a set of pagination
controls in the form `[1] ... [3] [4] [[5]] [6] [7] ... [10]`.

View File

@ -321,7 +321,7 @@ a quick reference (not all of them are described above):
$NexPageLink, $Link, $RelativeLink, $ChildrenOf, $Page, $Level, $Menu, $Section2, $LoginForm, $SilverStripeNavigator,
$PageComments, $Now, $LinkTo, $AbsoluteLink, $CurrentMember, $PastVisitor, $PastMember, $XML_val, $RAW_val, $SQL_val,
$JS_val, $ATT_val, $First, $Last, $FirstLast, $MiddleString, $Middle, $Even, $Odd, $EvenOdd, $Pos, $TotalItems,
$BaseHref, $Debug, $CurrentPage, $Top
$BaseHref, $Debug, $Top
### All fields available in Page_Controller
@ -335,8 +335,8 @@ $LinkToID, $VersionID, $CopyContentFromID, $RecordClassName
$Link, $LinkOrCurrent, $LinkOrSection, $LinkingMode, $ElementName, $InSection, $Comments, $Breadcrumbs, $NestedTitle,
$MetaTags, $ContentSource, $MultipleParents, $TreeTitle, $CMSTreeClasses, $Now, $LinkTo, $AbsoluteLink, $CurrentMember,
$PastMember, $XML_val, $RAW_val, $SQL_val, $JS_val, $ATT_val, $First, $Last, $FirstLast, $MiddleString,
$Middle, $Even, $Odd, $EvenOdd, $Pos, $TotalItems, $BaseHref, $CurrentPage, $Top
$PastVisitor, $PastMember, $XML_val, $RAW_val, $SQL_val, $JS_val, $ATT_val, $First, $Last, $FirstLast, $MiddleString,
$Middle, $Even, $Odd, $EvenOdd, $Pos, $TotalItems, $BaseHref, $Top
### All fields available in Page

View File

@ -89,21 +89,25 @@ method, we're building our own `getCustomSearchContext()` variant.
### Pagination
For paginating records on multiple pages, you need to get the generated `SQLQuery` before firing off the actual
search. This way we can set the "page limits" on the result through `setPageLimits()`, and only retrieve a fraction of
the whole result set.
For pagination records on multiple pages, you need to wrap the results in a
`PaginatedList` object. This object is also passed the generated `SQLQuery`
in order to read page limit information. It is also passed the current
`SS_HTTPRequest` object so it can read the current page from a GET var.
:::php
function getResults($searchCriteria = array()) {
public function getResults($searchCriteria = array()) {
$start = ($this->request->getVar('start')) ? (int)$this->request->getVar('start') : 0;
$limit = 10;
$context = singleton('MyDataObject')->getCustomSearchContext();
$query = $context->getQuery($searchCriteria, null, array('start'=>$start,'limit'=>$limit));
$records = $context->getResults($searchCriteria, null, array('start'=>$start,'limit'=>$limit));
if($records) {
$records->setPageLimits($start, $limit, $query->unlimitedRowCount());
$records = new PaginatedList($records, $this->request);
$records->setPageStart($start);
$records->setPageSize($limit);
$records->setTotalSize($query->unlimitedRowCount());
}
return $records;
@ -135,7 +139,7 @@ For more information on how to paginate your results within the template, see [T
to show the results of your custom search you need at least this content in your template, notice that
Results.PaginationSummary(4) defines how many pages the search will show in the search results. something like:
**Next 1 2 *3* 4 5 558**
**Next 1 2 *3* 4 5 <EFBFBD><EFBFBD><EFBFBD> 558**
:::ss

View File

@ -28,40 +28,181 @@ Note: You need to be logged in as an administrator to perform this command.
## Querying Data
There are static methods available for querying data. They automatically compile the necessary SQL to query the database
so they are very helpful. In case you need to fall back to plain-jane SQL, have a look at `[api:SQLQuery]`.
Every query to data starts with a `DataList::create($class)` call. For example, this query would return all of the Member objects:
:::php
$records = DataObject::get($obj, $filter, $sort, $join, $limit);
$members = DataList::create('Member');
The ORM uses a "fluent" syntax, where you specify a query by chaining together different methods. Two common methods
are filter() and sort():
:::php
$record = DataObject::get_one($obj, $filter);
$members = DataList::create('Member')->filter(array('FirstName' => 'Sam'))->sort('Surname');
Those of you who know a bit about SQL might be thinking "it looks like you're querying all members, and then filtering
to those with a first name of 'Sam'. Isn't this very slow?" Is isn't, because the ORM doesn't actually execute the
query until you iterate on the result with a `foreach()` or `<% control %>`.
:::php
$record = DataObject::get_by_id($obj, $id);
// The SQL query isn't executed here...
$members = DataList::create('Member');
// ...or here
$members = $members->filter(array('FirstName' => 'Sam'));
// ...or even here
$members = $members->sort('Surname');
**CAUTION: Please make sure to properly escape your SQL-snippets (see [security](/topics/security).**
// *This* is where the query is executed
foreach($members as $member) {
echo "<p>$member->FirstName $member->Surname</p>";
}
## Joining
This also means that getting the count of a list of objects will be done with a single, efficient query.
:::php
$members = DataList::create('Member')->filter(array('FirstName' => 'Sam'))->sort('Surname');
// This will create an single SELECT COUNT query.
echo $members->Count();
All of this lets you focus on writing your application, and not worrying too much about whether or not your queries are efficient.
### Returning a single DataObject
There are a couple of ways of getting a single DataObject from the ORM. If you know the ID number of the object, you can use `byID($id)`:
:::php
$member = DataList::create('Member')->byID(5);
If you have constructed a query that you know should return a single record, you can call `First()`:
:::php
$member = DataList::create('Member')->filter(array('FirstName' => 'Sam', 'Surname' => 'Minnee'))->First();
### Filters
**FUN FACT:** This isn't implemented in the code yet, but will be shortly.
As you might expect, the `filter()` method filters the list of objects that gets returned. The previous example
included this filter, which returns all Members with a first name of "Sam".
:::php
$members = DataList::create('Member')->filter(array('FirstName' => 'Sam'));
In SilverStripe 2, we would have passed `"\"FirstName\" = 'Sam'` to make this query. Now, we pass an array,
`array('FirstName' => 'Sam')`, to minimise the risk of SQL injection bugs. The format of this array follows a few
rules:
* Each element of the array specifies a filter. You can specify as many filters as you like, and they **all** must be
true.
* The key in the filter corresponds to the field that you want to filter by.
* The value in the filter corresponds to the value that you want to filter to.
So, this would return only those members called "Sam Minnée".
:::php
$members = DataList::create('Member')->filter(array(
'FirstName' => 'Sam',
'Surname' => 'Minnée',
));
By default, these filters specify case-insensitive exact matches. There are a number of suffixes that you can put on
field names to change this: `":StartsWith"`, `":EndsWith"`, `":Contains"`, `":GreaterThan"`, `":LessThan"`, `":Not"`,
and `":MatchCase"`. `":Not"` and `":MatchCase"` are special in that you can add it to any of the other filters.
This query will return everyone whose first name doesn't start with S, who have logged on since 1/1/2011.
:::php
$members = DataList::create('Member')->filter(array(
'FirstName:StartsWith:Not' => 'S'
'LastVisited:GreaterThan' => '2011-01-01'
));
If you wish to match against any of a number of columns, you can list several field names, separated by commas. This
will return all members whose first name or surname contain the string 'sam'.
:::php
$members = DataList::create('Member')->filter(array(
'FirstName,Surname:Contains' => 'sam'
));
If you wish to match against any of a number of values, you can pass an array as the value. This will return all
members whose first name is either Sam or Ingo.
:::php
$members = DataList::create('Member')->filter(array(
'FirstName' => array('sam', 'ingo'),
));
### Relation filters
So far we have only filtered a data list by fields on the object that you're requesting. For simple cases, this might
be okay, but often, a data model is made up of a number of related objects. For example, in SilverStripe each member
can be placed in a number of groups, and each group has a number of permissions.
For this, Sapphire ORM supports **Relation Filters**. Any ORM request can be filtered by fields on a related object by
specifying the filter key as `<relation-name>.<field-in-related-object>`. You can chain relations together as many
times as is necessary.
For example, this will return all members assigned ot a group that has a permission record with the code "ADMIN". In other words, it will return all administrators.
:::php
$members = DataList::create('Member')->filter(array(
'Groups.Permissions.Code' => 'ADMIN',
));
Note that we are just joining to these tables to filter the records. Even if a member is in more than 1 administrator group, unique members will still be returned by this query.
The other features of filters can be applied to relation filters as well. This will return all members in groups whose
names start with 'A' or 'B'.
:::php
$members = DataList::create('Member')->filter(array(
'Groups.Title:StartsWith' => array('A', 'B'),
));
You can even follow a relation back to the original model class! This will return all members are in at least 1 group that also has a member called Sam.
:::php
$members = DataList::create('Member')->filter(array(
'Groups.Members.FirstName' => 'Sam'
));
### Raw SQL options for advanced users
Occassionally, the system described above won't let you do exactly what you need to do. In these situtations, we have
methods that manipulate the SQL query at a lower level. When using these, please ensure that all table & field names
are escaped with double quotes, otherwise some DB back-ends (e.g. PostgreSQL) won't work.
In general, we advise against using these methods unless it's absolutely necessary. If the ORM doesn't do quite what
you need it to, you may also consider extending the ORM with new data types or filter modifiers (that documentation still needs to be written)
#### Where clauses
You can specify a WHERE clause fragment (that will be combined with other filters using AND) with the `where()` method:
:: php
$members = DataList::create('Member')->where("\"FirstName\" = 'Sam'")
#### Joining
You can specify a join with the innerJoin and leftJoin methods. Both of these methods have the same arguments:
* The name of the table to join to
* The filter clause for the join
* An optional alias
For example:
:: php
// Without an alias
$members = DataList::create('Member')->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
$members = DataList::create('Member')->innerJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "REl");
Passing a *$join* statement to DataObject::get will filter results further by the JOINs performed against the foreign
table. **It will NOT return the additionally joined data.** The returned *$records* will always be a
`[api:DataObject]`.
When using *$join* statements be sure the string is in the proper format for the respective database engine. In MySQL
the use of back-ticks may be necessary when referring Table Names and potentially Columns. (see [MySQL
Identifiers](http://dev.mysql.com/doc/refman/5.0/en/identifiers.html)):
:::php
// Example from the forums: http://www.silverstripe.org/archive/show/79865#post79865
// Note the use of backticks on table names
$links = DataObject::get("SiteTree",
"ShowInMenus = 1 AND ParentID = 23",
"",
"LEFT JOIN `ConsultationPaperHolder` ON `ConsultationPaperHolder`.ID = `SiteTree`.ID",
"0, 10");
## Properties
@ -312,7 +453,7 @@ Inside sapphire it doesn't matter if you're editing a *has_many*- or a *many_man
}
### Custom Relation Getters
### Custom Relations
You can use the flexible datamodel to get a filtered result-list without writing any SQL. For example, this snippet gets
you the "Players"-relation on a team, but only containing active players. (See `[api:DataObject::$has_many]` for more info on
@ -324,8 +465,8 @@ the described relations).
"Players" => "Player"
);
// can be accessed by $myTeam->ActivePlayers
function getActivePlayers() {
// can be accessed by $myTeam->ActivePlayers()
function ActivePlayers() {
return $this->Players("Status='Active'");
}
}

View File

@ -718,7 +718,6 @@ class File extends DataObject {
$records = $query->execute();
$ret = $this->buildDataObjectSet($records, $containerClass);
if($ret) $ret->parseQueryLimit($query);
return $ret;
}
@ -784,7 +783,7 @@ class File extends DataObject {
* @return FieldSet
*/
function uploadMetadataFields() {
$fields = new FieldSet();
$fields = new FieldList();
$fields->push(new TextField('Title', $this->fieldLabel('Title')));
$this->extend('updateUploadMetadataFields', $fields);

View File

@ -386,7 +386,7 @@ class Folder extends File {
/**
* Return the FieldSet used to edit this folder in the CMS.
* You can modify this fieldset by subclassing folder, or by creating a {@link DataExtension}
* and implemeting updateCMSFields(FieldSet $fields) on that extension.
* and implemeting updateCMSFields(FieldList $fields) on that extension.
*/
function getCMSFields() {
$fileList = new AssetTableField(
@ -407,7 +407,7 @@ class Folder extends File {
$deleteButton = new HiddenField('deletemarked');
}
$fields = new FieldSet(
$fields = new FieldList(
new HiddenField("Name"),
new TabSet("Root",
new Tab("Files", _t('Folder.FILESTAB', "Files"),

View File

@ -164,8 +164,7 @@ class CheckboxSetField extends OptionsetField {
// If we're not passed a value directly, we can look for it in a relation method on the object passed as a second arg
if(!$value && $obj && $obj instanceof DataObject && $obj->hasMethod($this->name)) {
$funcName = $this->name;
$selected = $obj->$funcName();
$value = $selected->toDropdownMap('ID', 'ID');
$value = $obj->$funcName()->getIDList();
}
parent::setValue($value, $obj);

View File

@ -39,7 +39,7 @@ class ComplexTableField extends TableListField {
protected $detailFormFields;
protected $viewAction, $sourceJoin, $sourceItems;
protected $viewAction;
/**
* @var Controller
@ -119,14 +119,6 @@ class ComplexTableField extends TableListField {
*/
protected $detailFormValidator = null;
/**
* Automatically detect a has-one relationship
* in the popup (=child-class) and save the relation ID.
*
* @var boolean
*/
protected $relationAutoSetting = true;
/**
* Default size for the popup box
*/
@ -197,7 +189,7 @@ class ComplexTableField extends TableListField {
* @param string $name
* @param string $sourceClass
* @param array $fieldList
* @param FieldSet $detailFormFields
* @param FieldList $detailFormFields
* @param string $sourceFilter
* @param string $sourceSort
* @param string $sourceJoin
@ -210,31 +202,6 @@ class ComplexTableField extends TableListField {
parent::__construct($name, $sourceClass, $fieldList, $sourceFilter, $sourceSort, $sourceJoin);
}
/**
* Return the record filter for this table.
* It will automatically add a relation filter if relationAutoSetting is true, and it can determine an appropriate
* filter.
*/
function sourceFilter() {
$sourceFilter = parent::sourceFilter();
if($this->relationAutoSetting
&& $this->getParentClass()
&& ($filterKey = $this->getParentIdName($this->getParentClass(), $this->sourceClass()))
&& ($filterValue = $this->sourceID()) ) {
$newFilter = "\"$filterKey\" = '" . Convert::raw2sql($filterValue) . "'";
if($sourceFilter && is_array($sourceFilter)) {
// Note that the brackets below are taken into account when building this
$sourceFilter = implode(") AND (", $sourceFilter);
}
$sourceFilter = $sourceFilter ? "($sourceFilter) AND ($newFilter)" : $newFilter;
}
return $sourceFilter;
}
function isComposite() {
return false;
}
@ -277,26 +244,21 @@ JS;
return $this->renderWith($this->template);
}
function sourceClass() {
return $this->sourceClass;
}
/**
* @return DataObjectSet
*/
function Items() {
$this->sourceItems = $this->sourceItems();
$sourceItems = $this->sourceItems();
if(!$this->sourceItems) {
if(!$sourceItems) {
return null;
}
$pageStart = (isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0;
$this->sourceItems->setPageLimits($pageStart, $this->pageSize, $this->totalCount);
$output = new DataObjectSet();
foreach($this->sourceItems as $pageIndex=>$item) {
$output->push(new $this->itemClass($item, $this));
$output = new ArrayList();
foreach($sourceItems as $pageIndex=>$item) {
$output->push(Object::create($this->itemClass,$item, $this, $pageStart+$pageIndex));
}
return $output;
}
@ -369,7 +331,7 @@ JS;
* @return FieldSet
*/
function createFieldSet() {
$fieldset = new FieldSet();
$fieldset = new FieldList();
foreach($this->fieldTypes as $key => $fieldType){
$fieldset->push(new $fieldType($key));
}
@ -380,85 +342,6 @@ JS;
$this->controller = $controller;
}
/**
* Determines on which relation-class the DetailForm is saved
* by looking at the surrounding form-record.
*
* @return String
*/
function getParentClass() {
if($this->parentClass === false) {
// purposely set parent-relation to false
return false;
} elseif(!empty($this->parentClass)) {
return $this->parentClass;
} elseif($this->form && $this->form->getRecord()) {
return $this->form->getRecord()->ClassName;
}
}
/**
* Return the record in which the CTF resides, if it exists.
*/
function getParentRecord() {
if($this->form && $record = $this->form->getRecord()) {
return $record;
} else {
$parentID = (int)$this->sourceID();
$parentClass = $this->getParentClass();
if($parentClass) {
if($parentID) return DataObject::get_by_id($parentClass, $parentID);
else return singleton($parentClass);
}
}
}
/**
* (Optional) Setter for a correct parent-relation-class.
* Defaults to the record loaded into the surrounding form as a fallback.
* Caution: Please use the classname, not the actual column-name in the database.
*
* @param $className string
*/
function setParentClass($className) {
$this->parentClass = $className;
}
/**
* Returns the db-fieldname of the currently used has_one-relationship.
*/
function getParentIdName($parentClass, $childClass) {
return $this->getParentIdNameRelation($childClass, $parentClass, 'has_one');
}
/**
* Manually overwrites the parent-ID relations.
* @see setParentClass()
*
* @param String $str Example: FamilyID (when one Individual has_one Family)
*/
function setParentIdName($str) {
$this->parentIdName = $str;
}
/**
* Returns the db-fieldname of the currently used relationship.
* Note: constructed resolve ambiguous cases in the same manner as
* DataObject::getComponentJoinField()
*/
function getParentIdNameRelation($parentClass, $childClass, $relation) {
if($this->parentIdName) return $this->parentIdName;
$relations = array_flip(singleton($parentClass)->$relation());
$classes = array_reverse(ClassInfo::ancestry($childClass));
foreach($classes as $class) {
if(isset($relations[$class])) return $relations[$class] . 'ID';
}
return false;
}
function setTemplatePopup($template) {
$this->templatePopup = $template;
}
@ -495,45 +378,8 @@ JS;
}
function getFieldsFor($childData) {
$hasManyRelationName = null;
$manyManyRelationName = null;
// See if our parent class has any many_many relations by this source class
if($parentClass = $this->getParentRecord()) {
$manyManyRelations = $parentClass->many_many();
$manyManyRelationName = null;
$manyManyComponentSet = null;
$hasManyRelations = $parentClass->has_many();
$hasManyRelationName = null;
$hasManyComponentSet = null;
if($manyManyRelations) foreach($manyManyRelations as $relation => $class) {
if($class == $this->sourceClass()) {
$manyManyRelationName = $relation;
}
}
if($hasManyRelations) foreach($hasManyRelations as $relation => $class) {
if($class == $this->sourceClass()) {
$hasManyRelationName = $relation;
}
}
}
// Add the relation value to related records
if(!$childData->ID && $this->getParentClass()) {
// make sure the relation-link is existing, even if we just add the sourceClass and didn't save it
$parentIDName = $this->getParentIdName($this->getParentClass(), $this->sourceClass());
$childData->$parentIDName = $this->sourceID();
}
$detailFields = $this->getCustomFieldsFor($childData);
if($this->getParentClass() && $hasManyRelationName && $childData->ID) {
$hasManyComponentSet = $parentClass->getComponents($hasManyRelationName);
}
// the ID field confuses the Controller-logic in finding the right view for ReferencedField
$detailFields->removeByName('ID');
@ -542,34 +388,18 @@ JS;
$detailFields->push(new HiddenField('ctf[childID]', '', $childData->ID));
}
// add a namespaced ID instead thats "converted" by saveComplexTableField()
$detailFields->push(new HiddenField('ctf[ClassName]', '', $this->sourceClass()));
/* TODO: Figure out how to implement this
if($this->getParentClass()) {
$detailFields->push(new HiddenField('ctf[parentClass]', '', $this->getParentClass()));
if($manyManyRelationName && $this->relationAutoSetting) {
$detailFields->push(new HiddenField('ctf[manyManyRelation]', '', $manyManyRelationName));
}
if($hasManyRelationName && $this->relationAutoSetting) {
$detailFields->push(new HiddenField('ctf[hasManyRelation]', '', $hasManyRelationName));
}
if($manyManyRelationName || $hasManyRelationName) {
$detailFields->push(new HiddenField('ctf[sourceID]', '', $this->sourceID()));
}
$parentIdName = $this->getParentIdName($this->getParentClass(), $this->sourceClass());
if($parentIdName) {
if($this->relationAutoSetting) {
// Hack for model admin: model admin will have included a dropdown for the relation itself
$parentIdName = $this->getParentIdName($this->getParentClass(), $this->sourceClass());
if($parentIdName) {
$detailFields->removeByName($parentIdName);
$detailFields->push(new HiddenField($parentIdName, '', $this->sourceID()));
}
}
}
*/
return $detailFields;
}
@ -614,16 +444,10 @@ JS;
}
/**
* By default, a ComplexTableField will assume that the field name is the name of a has-many relation on the object being
* edited. It will identify the foreign key in the object being listed, and filter on that column, as well as auto-setting
* that column for newly created records.
*
* Calling $this->setRelationAutoSetting(false) will disable this functionality.
*
* @param boolean $value Should the relation auto-setting functionality be enabled?
* @deprecated
*/
function setRelationAutoSetting($value) {
$this->relationAutoSetting = $value;
user_error("ComplexTableField::setRelationAutoSetting() is deprecated; manipulate the DataList instead", E_USER_WARNING);
}
/**
@ -648,21 +472,8 @@ JS;
return Director::redirectBack();
}
// Save the many many relationship if it's available
if(isset($data['ctf']['manyManyRelation'])) {
$parentRecord = DataObject::get_by_id($data['ctf']['parentClass'], (int) $data['ctf']['sourceID']);
$relationName = $data['ctf']['manyManyRelation'];
$componentSet = $parentRecord ? $parentRecord->getManyManyComponents($relationName) : null;
if($componentSet) $componentSet->add($childData);
}
if(isset($data['ctf']['hasManyRelation'])) {
$parentRecord = DataObject::get_by_id($data['ctf']['parentClass'], (int) $data['ctf']['sourceID']);
$relationName = $data['ctf']['hasManyRelation'];
$componentSet = $parentRecord ? $parentRecord->getComponents($relationName) : null;
if($componentSet) $componentSet->add($childData);
}
// Save this item into the given relationship
$this->getDataList()->add($childData);
$referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
@ -729,7 +540,7 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest {
*/
/* this doesn't actually work :-(
function Paginator() {
$paginatingSet = new DataObjectSet(array($this->dataObj()));
$paginatingSet = new ArrayList(array($this->dataObj()));
$start = isset($_REQUEST['ctf']['start']) ? $_REQUEST['ctf']['start'] : 0;
$paginatingSet->setPageLimits($start, 1, $this->ctf->TotalCount());
return $paginatingSet;
@ -760,7 +571,7 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest {
return false;
}
$this->dataObj()->delete();
$this->ctf->getDataList()->removeByID($this->itemID);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
@ -831,13 +642,8 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest {
return Director::redirectBack();
}
// Save the many many relationship if it's available
if(isset($data['ctf']['manyManyRelation'])) {
$parentRecord = DataObject::get_by_id($data['ctf']['parentClass'], (int) $data['ctf']['sourceID']);
$relationName = $data['ctf']['manyManyRelation'];
$componentSet = $parentRecord->getManyManyComponents($relationName);
$componentSet->add($dataObject);
}
// Save this item into the given relationship
$this->ctf->getDataList()->add($dataObject);
$referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
@ -874,16 +680,16 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest {
}
function PopupLastLink() {
if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->totalCount-1) {
if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->TotalCount()-1) {
return null;
}
$start = $this->totalCount - 1;
$start = $this->TotalCount - 1;
return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}");
}
function PopupNextLink() {
if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->totalCount-1) {
if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->TotalCount()-1) {
return null;
}
@ -909,16 +715,16 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest {
function Pagination() {
$this->pageSize = 9;
$currentItem = $this->PopupCurrentItem();
$result = new DataObjectSet();
$result = new ArrayList();
if($currentItem < 6) {
$offset = 1;
} elseif($this->totalCount - $currentItem <= 4) {
$offset = $currentItem - (10 - ($this->totalCount - $currentItem));
} elseif($this->TotalCount() - $currentItem <= 4) {
$offset = $currentItem - (10 - ($this->TotalCount() - $currentItem));
$offset = $offset <= 0 ? 1 : $offset;
} else {
$offset = $currentItem - 5;
}
for($i = $offset;$i <= $offset + $this->pageSize && $i <= $this->totalCount;$i++) {
for($i = $offset;$i <= $offset + $this->pageSize && $i <= $this->TotalCount();$i++) {
$start = $i - 1;
$links['link'] = Controller::join_links($this->Link() . "$this->methodName?ctf[start]={$start}");
$links['number'] = $i;
@ -939,13 +745,6 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest {
* #################################
*/
/**
* Returns the db-fieldname of the currently used has_one-relationship.
*/
function getParentIdName($parentClass, $childClass) {
return $this->getParentIdNameRelation($childClass, $parentClass, 'has_one');
}
/**
* Manually overwrites the parent-ID relations.
* @see setParentClass()
@ -953,23 +752,7 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest {
* @param String $str Example: FamilyID (when one Individual has_one Family)
*/
function setParentIdName($str) {
$this->parentIdName = $str;
}
/**
* Returns the db-fieldname of the currently used relationship.
*/
function getParentIdNameRelation($parentClass, $childClass, $relation) {
if($this->parentIdName) return $this->parentIdName;
$relations = singleton($parentClass)->$relation();
$classes = ClassInfo::ancestry($childClass);
if($relations) {
foreach($relations as $k => $v) {
if(array_key_exists($v, $classes)) return $k . 'ID';
}
}
return false;
throw new Exception("setParentIdName is no longer necessary");
}
function setTemplatePopup($template) {
@ -1030,7 +813,7 @@ class ComplexTableField_Popup extends Form {
Requirements::clear();
Requirements::unblock_all();
$actions = new FieldSet();
$actions = new FieldList();
if(!$readonly) {
$actions->push(
$saveAction = new FormAction(

View File

@ -9,7 +9,7 @@
class CompositeField extends FormField {
/**
* @var FieldSet
* @var FieldList
*/
protected $children;
@ -29,13 +29,13 @@ class CompositeField extends FormField {
protected $columnCount = null;
public function __construct($children = null) {
if($children instanceof FieldSet) {
if($children instanceof FieldList) {
$this->children = $children;
} elseif(is_array($children)) {
$this->children = new FieldSet($children);
$this->children = new FieldList($children);
} else {
$children = is_array(func_get_args()) ? func_get_args() : array();
$this->children = new FieldSet($children);
$this->children = new FieldList($children);
}
$this->children->setContainerField($this);
@ -46,10 +46,17 @@ class CompositeField extends FormField {
}
/**
* Returns all the sub-fields, suitable for <% control FieldSet %>
* Returns all the sub-fields, suitable for <% control FieldList %>
*/
public function FieldList() {
return $this->children;
}
/**
* @deprecated 3.0 Please use {@link FieldList()}.
*/
public function FieldSet() {
return $this->children;
return $this->FieldList();
}
public function setID($id) {
@ -62,14 +69,14 @@ class CompositeField extends FormField {
/**
* Accessor method for $this->children
* @return FieldSet
* @return FieldList
*/
public function getChildren() {
return $this->children;
}
/**
* @param FieldSet $children
* @param FieldList $children
*/
public function setChildren($children) {
$this->children = $children;
@ -79,7 +86,7 @@ class CompositeField extends FormField {
* Returns the fields nested inside another DIV
*/
function FieldHolder() {
$fs = $this->FieldSet();
$fs = $this->FieldList();
$idAtt = isset($this->id) ? " id=\"{$this->id}\"" : '';
$className = ($this->columnCount) ? "field CompositeField {$this->extraClass()} multicolumn" : "field CompositeField {$this->extraClass()}";
$content = "<div class=\"$className\"$idAtt>\n";
@ -102,7 +109,7 @@ class CompositeField extends FormField {
* Returns the fields in the restricted field holder inside a DIV.
*/
function SmallFieldHolder() {//return $this->FieldHolder();
$fs = $this->FieldSet();
$fs = $this->FieldList();
$idAtt = isset($this->id) ? " id=\"{$this->id}\"" : '';
$className = ($this->columnCount) ? "field CompositeField {$this->extraClass()} multicolumn" : "field CompositeField {$this->extraClass()}";
$content = "<div class=\"$className\"$idAtt>";
@ -168,7 +175,7 @@ class CompositeField extends FormField {
}
/**
* @uses FieldSet->insertBefore()
* @uses FieldList->insertBefore()
*/
public function insertBefore($field, $insertBefore) {
$ret = $this->children->insertBefore($field, $insertBefore);
@ -209,7 +216,7 @@ class CompositeField extends FormField {
* versions of all the children
*/
public function performReadonlyTransformation() {
$newChildren = new FieldSet();
$newChildren = new FieldList();
$clone = clone $this;
foreach($clone->getChildren() as $idx => $child) {
if(is_object($child)) $child = $child->transform(new ReadonlyTransformation());
@ -226,7 +233,7 @@ class CompositeField extends FormField {
* versions of all the children
*/
public function performDisabledTransformation($trans) {
$newChildren = new FieldSet();
$newChildren = new FieldList();
$clone = clone $this;
if($clone->getChildren()) foreach($clone->getChildren() as $idx => $child) {
if(is_object($child)) {

View File

@ -69,7 +69,7 @@ class ConfirmedPasswordField extends FormField {
*/
function __construct($name, $title = null, $value = "", $form = null, $showOnClick = false, $titleConfirmField = null) {
// naming with underscores to prevent values from actually being saved somewhere
$this->children = new FieldSet(
$this->children = new FieldList(
new PasswordField(
"{$name}[_Password]",
(isset($title)) ? $title : _t('Member.PASSWORD', 'Password')

556
forms/FieldList.php Executable file
View File

@ -0,0 +1,556 @@
<?php
/**
* A list designed to hold form field instances.
*
* @package forms
* @subpackage fields-structural
*/
class FieldList extends ArrayList {
/**
* Cached flat representation of all fields in this set,
* including fields nested in {@link CompositeFields}.
*
* @uses self::collateDataFields()
* @var array
*/
protected $sequentialSet;
/**
* @var array
*/
protected $sequentialSaveableSet;
/**
* @todo Documentation
*/
protected $containerField;
public function __construct($items = array()) {
if (!is_array($items) || func_num_args() > 1) {
$items = func_get_args();
}
parent::__construct($items);
foreach ($items as $item) {
if ($item instanceof FormField) $item->setContainerFieldSet($this);
}
}
/**
* Return a sequential set of all fields that have data. This excludes wrapper composite fields
* as well as heading / help text fields.
*/
public function dataFields() {
if(!$this->sequentialSet) $this->collateDataFields($this->sequentialSet);
return $this->sequentialSet;
}
public function saveableFields() {
if(!$this->sequentialSaveableSet) $this->collateDataFields($this->sequentialSaveableSet, true);
return $this->sequentialSaveableSet;
}
protected function flushFieldsCache() {
$this->sequentialSet = null;
$this->sequentialSaveableSet = null;
}
protected function collateDataFields(&$list, $saveableOnly = false) {
foreach($this as $field) {
if($field->isComposite()) $field->collateDataFields($list, $saveableOnly);
if($saveableOnly) {
$isIncluded = ($field->hasData() && !$field->isReadonly() && !$field->isDisabled());
} else {
$isIncluded = ($field->hasData());
}
if($isIncluded) {
$name = $field->Name();
if(isset($list[$name])) {
$errSuffix = "";
if($this->form) $errSuffix = " in your '{$this->form->class}' form called '" . $this->form->Name() . "'";
else $errSuffix = '';
user_error("collateDataFields() I noticed that a field called '$name' appears twice$errSuffix.", E_USER_ERROR);
}
$list[$name] = $field;
}
}
}
/**
* Add an extra field to a tab within this fieldset.
* This is most commonly used when overloading getCMSFields()
*
* @param string $tabName The name of the tab or tabset. Subtabs can be referred to as TabSet.Tab or TabSet.Tab.Subtab.
* This function will create any missing tabs.
* @param FormField $field The {@link FormField} object to add to the end of that tab.
* @param string $insertBefore The name of the field to insert before. Optional.
*/
public function addFieldToTab($tabName, $field, $insertBefore = null) {
// This is a cache that must be flushed
$this->flushFieldsCache();
// Find the tab
$tab = $this->findOrMakeTab($tabName);
// Add the field to the end of this set
if($insertBefore) $tab->insertBefore($field, $insertBefore);
else $tab->push($field);
}
/**
* Add a number of extra fields to a tab within this fieldset.
* This is most commonly used when overloading getCMSFields()
*
* @param string $tabName The name of the tab or tabset. Subtabs can be referred to as TabSet.Tab or TabSet.Tab.Subtab.
* This function will create any missing tabs.
* @param array $fields An array of {@link FormField} objects.
*/
public function addFieldsToTab($tabName, $fields, $insertBefore = null) {
$this->flushFieldsCache();
// Find the tab
$tab = $this->findOrMakeTab($tabName);
// Add the fields to the end of this set
foreach($fields as $field) {
// Check if a field by the same name exists in this tab
if($insertBefore) {
$tab->insertBefore($field, $insertBefore);
} elseif($tab->fieldByName($field->Name())) {
// It exists, so we need to replace the old one
$this->replaceField($field->Name(), $field);
} else {
$tab->push($field);
}
}
}
/**
* Remove the given field from the given tab in the field.
*
* @param string $tabName The name of the tab
* @param string $fieldName The name of the field
*/
public function removeFieldFromTab($tabName, $fieldName) {
$this->flushFieldsCache();
// Find the tab
$tab = $this->findOrMakeTab($tabName);
$tab->removeByName($fieldName);
}
/**
* Removes a number of fields from a Tab/TabSet within this FieldSet.
*
* @param string $tabName The name of the Tab or TabSet field
* @param array $fields A list of fields, e.g. array('Name', 'Email')
*/
public function removeFieldsFromTab($tabName, $fields) {
$this->flushFieldsCache();
// Find the tab
$tab = $this->findOrMakeTab($tabName);
// Add the fields to the end of this set
foreach($fields as $field) $tab->removeByName($field);
}
/**
* Remove a field from this FieldSet by Name.
* The field could also be inside a CompositeField.
*
* @param string $fieldName The name of the field or tab
* @param boolean $dataFieldOnly If this is true, then a field will only
* be removed if it's a data field. Dataless fields, such as tabs, will
* be left as-is.
*/
public function removeByName($fieldName, $dataFieldOnly = false) {
if(!$fieldName) {
user_error('FieldSet::removeByName() was called with a blank field name.', E_USER_WARNING);
}
$this->flushFieldsCache();
foreach($this->items as $i => $child) {
if(is_object($child)){
$childName = $child->Name();
if(!$childName) $childName = $child->Title();
if(($childName == $fieldName) && (!$dataFieldOnly || $child->hasData())) {
array_splice( $this->items, $i, 1 );
break;
} else if($child->isComposite()) {
$child->removeByName($fieldName, $dataFieldOnly);
}
}
}
}
/**
* Replace a single field with another. Ignores dataless fields such as Tabs and TabSets
*
* @param string $fieldName The name of the field to replace
* @param FormField $newField The field object to replace with
* @return boolean TRUE field was successfully replaced
* FALSE field wasn't found, nothing changed
*/
public function replaceField($fieldName, $newField) {
$this->flushFieldsCache();
foreach($this->items as $i => $field) {
if(is_object($field)) {
if($field->Name() == $fieldName && $field->hasData()) {
$this->items[$i] = $newField;
return true;
} else if($field->isComposite()) {
if($field->replaceField($fieldName, $newField)) return true;
}
}
}
return false;
}
/**
* Rename the title of a particular field name in this set.
*
* @param string $fieldName Name of field to rename title of
* @param string $newFieldTitle New title of field
* @return boolean
*/
function renameField($fieldName, $newFieldTitle) {
$field = $this->dataFieldByName($fieldName);
if(!$field) return false;
$field->setTitle($newFieldTitle);
return $field->Title() == $newFieldTitle;
}
/**
* @return boolean
*/
public function hasTabSet() {
foreach($this->items as $i => $field) {
if(is_object($field) && $field instanceof TabSet) {
return true;
}
}
return false;
}
/**
* Returns the specified tab object, creating it if necessary.
*
* @todo Support recursive creation of TabSets
*
* @param string $tabName The tab to return, in the form "Tab.Subtab.Subsubtab".
* Caution: Does not recursively create TabSet instances, you need to make sure everything
* up until the last tab in the chain exists.
* @param string $title Natural language title of the tab. If {@link $tabName} is passed in dot notation,
* the title parameter will only apply to the innermost referenced tab.
* The title is only changed if the tab doesn't exist already.
* @return Tab The found or newly created Tab instance
*/
public function findOrMakeTab($tabName, $title = null) {
$parts = explode('.',$tabName);
// We could have made this recursive, but I've chosen to keep all the logic code within FieldSet rather than add it to TabSet and Tab too.
$currentPointer = $this;
foreach($parts as $k => $part) {
$parentPointer = $currentPointer;
$currentPointer = $currentPointer->fieldByName($part);
// Create any missing tabs
if(!$currentPointer) {
if(is_a($parentPointer, 'TabSet')) {
// use $title on the innermost tab only
if($title && $k == count($parts)-1) {
$currentPointer = new Tab($part, $title);
} else {
$currentPointer = new Tab($part);
}
$parentPointer->push($currentPointer);
} else {
$withName = ($parentPointer->hasMethod('Name')) ? " named '{$parentPointer->Name()}'" : null;
user_error("FieldSet::addFieldToTab() Tried to add a tab to object '{$parentPointer->class}'{$withName} - '$part' didn't exist.", E_USER_ERROR);
}
}
}
return $currentPointer;
}
/**
* Returns a named field.
* You can use dot syntax to get fields from child composite fields
*
* @todo Implement similiarly to dataFieldByName() to support nested sets - or merge with dataFields()
*/
public function fieldByName($name) {
if(strpos($name,'.') !== false) list($name, $remainder) = explode('.',$name,2);
else $remainder = null;
foreach($this->items as $child) {
if(trim($name) == trim($child->Name()) || $name == $child->id) {
if($remainder) {
if($child->isComposite()) {
return $child->fieldByName($remainder);
} else {
user_error("Trying to get field '$remainder' from non-composite field $child->class.$name", E_USER_WARNING);
return null;
}
} else {
return $child;
}
}
}
}
/**
* Returns a named field in a sequential set.
* Use this if you're using nested FormFields.
*
* @param string $name The name of the field to return
* @return FormField instance
*/
public function dataFieldByName($name) {
if($dataFields = $this->dataFields()) {
foreach($dataFields as $child) {
if(trim($name) == trim($child->Name()) || $name == $child->id) return $child;
}
}
}
/**
* Inserts a field before a particular field in a FieldSet.
*
* @param FormField $item The form field to insert
* @param string $name Name of the field to insert before
*/
public function insertBefore($item, $name) {
$this->onBeforeInsert($item);
$item->setContainerFieldSet($this);
$i = 0;
foreach($this->items as $child) {
if($name == $child->Name() || $name == $child->id) {
array_splice($this->items, $i, 0, array($item));
return $item;
} elseif($child->isComposite()) {
$ret = $child->insertBefore($item, $name);
if($ret) return $ret;
}
$i++;
}
return false;
}
/**
* Inserts a field after a particular field in a FieldSet.
*
* @param FormField $item The form field to insert
* @param string $name Name of the field to insert after
*/
public function insertAfter($item, $name) {
$this->onBeforeInsert($item);
$item->setContainerFieldSet($this);
$i = 0;
foreach($this->items as $child) {
if($name == $child->Name() || $name == $child->id) {
array_splice($this->items, $i+1, 0, array($item));
return $item;
} elseif($child->isComposite()) {
$ret = $child->insertAfter($item, $name);
if($ret) return $ret;
}
$i++;
}
return false;
}
/**
* Push a single field into this FieldSet instance.
*
* @param FormField $item The FormField to add
* @param string $key An option array key (field name)
*/
public function push($item, $key = null) {
$this->onBeforeInsert($item);
$item->setContainerFieldSet($this);
return parent::push($item, $key = null);
}
/**
* Handler method called before the FieldSet is going to be manipulated.
*/
protected function onBeforeInsert($item) {
$this->flushFieldsCache();
if($item->Name()) $this->rootFieldSet()->removeByName($item->Name(), true);
}
/**
* Set the Form instance for this FieldSet.
*
* @param Form $form The form to set this FieldSet to
*/
public function setForm($form) {
foreach($this as $field) $field->setForm($form);
}
/**
* Load the given data into this form.
*
* @param data An map of data to load into the FieldSet
*/
public function setValues($data) {
foreach($this->dataFields() as $field) {
$fieldName = $field->Name();
if(isset($data[$fieldName])) $field->setValue($data[$fieldName]);
}
}
/**
* Return all <input type="hidden"> fields
* in a form - including fields nested in {@link CompositeFields}.
* Useful when doing custom field layouts.
*
* @return FieldSet
*/
function HiddenFields() {
$hiddenFields = new HiddenFieldSet();
$dataFields = $this->dataFields();
if($dataFields) foreach($dataFields as $field) {
if($field instanceof HiddenField) $hiddenFields->push($field);
}
return $hiddenFields;
}
/**
* Transform this FieldSet with a given tranform method,
* e.g. $this->transform(new ReadonlyTransformation())
*
* @return FieldSet
*/
function transform($trans) {
$this->flushFieldsCache();
$newFields = new FieldList();
foreach($this as $field) {
$newFields->push($field->transform($trans));
}
return $newFields;
}
/**
* Returns the root field set that this belongs to
*/
function rootFieldSet() {
if($this->containerField) return $this->containerField->rootFieldSet();
else return $this;
}
function setContainerField($field) {
$this->containerField = $field;
}
/**
* Transforms this FieldSet instance to readonly.
*
* @return FieldSet
*/
function makeReadonly() {
return $this->transform(new ReadonlyTransformation());
}
/**
* Transform the named field into a readonly feld.
*
* @param string|FormField
*/
function makeFieldReadonly($field) {
$fieldName = ($field instanceof FormField) ? $field->Name() : $field;
$srcField = $this->dataFieldByName($fieldName);
$this->replaceField($fieldName, $srcField->performReadonlyTransformation());
}
/**
* Change the order of fields in this FieldSet by specifying an ordered list of field names.
* This works well in conjunction with SilverStripe's scaffolding functions: take the scaffold, and
* shuffle the fields around to the order that you want.
*
* Please note that any tabs or other dataless fields will be clobbered by this operation.
*
* @param array $fieldNames Field names can be given as an array, or just as a list of arguments.
*/
function changeFieldOrder($fieldNames) {
// Field names can be given as an array, or just as a list of arguments.
if(!is_array($fieldNames)) $fieldNames = func_get_args();
// Build a map of fields indexed by their name. This will make the 2nd step much easier.
$fieldMap = array();
foreach($this->dataFields() as $field) $fieldMap[$field->Name()] = $field;
// Iterate through the ordered list of names, building a new array to be put into $this->items.
// While we're doing this, empty out $fieldMap so that we can keep track of leftovers.
// Unrecognised field names are okay; just ignore them
$fields = array();
foreach($fieldNames as $fieldName) {
if(isset($fieldMap[$fieldName])) {
$fields[] = $fieldMap[$fieldName];
unset($fieldMap[$fieldName]);
}
}
// Add the leftover fields to the end of the list.
$fields = $fields + array_values($fieldMap);
// Update our internal $this->items parameter.
$this->items = $fields;
$this->flushFieldsCache();
}
/**
* Find the numerical position of a field within
* the children collection. Doesn't work recursively.
*
* @param string|FormField
* @return Position in children collection (first position starts with 0). Returns FALSE if the field can't be found.
*/
function fieldPosition($field) {
if(is_object($field)) $field = $field->Name();
$i = 0;
foreach($this->dataFields() as $child) {
if($child->Name() == $field) return $i;
$i++;
}
return false;
}
}
/**
* A field list designed to store a list of hidden fields. When inserted into a template, only the
* input tags will be included
*
* @package forms
* @subpackage fields-structural
*/
class HiddenFieldList extends FieldList {
function forTemplate() {
$output = "";
foreach($this as $field) {
$output .= $field->Field();
}
return $output;
}
}

View File

@ -1,559 +1,17 @@
<?php
/**
* DataObjectSet designed for form fields.
* It extends the DataObjectSet with the ability to get a sequential set of fields.
* @package forms
* @subpackage fields-structural
*/
class FieldSet extends DataObjectSet {
/**
* Cached flat representation of all fields in this set,
* including fields nested in {@link CompositeFields}.
*
* @uses self::collateDataFields()
* @var array
*/
protected $sequentialSet;
/**
* @var array
*/
protected $sequentialSaveableSet;
/**
* @todo Documentation
*/
protected $containerField;
public function __construct($items = null) {
// if the first parameter is not an array, or we have more than one parameter, collate all parameters to an array
// otherwise use the passed array
$itemsArr = (!is_array($items) || count(func_get_args()) > 1) ? func_get_args() : $items;
parent::__construct($itemsArr);
if(isset($this->items) && count($this->items)) {
foreach($this->items as $item) {
if(isset($item) && is_a($item, 'FormField')) {
$item->setContainerFieldSet($this);
}
}
}
}
/**
* Return a sequential set of all fields that have data. This excludes wrapper composite fields
* as well as heading / help text fields.
*/
public function dataFields() {
if(!$this->sequentialSet) $this->collateDataFields($this->sequentialSet);
return $this->sequentialSet;
}
public function saveableFields() {
if(!$this->sequentialSaveableSet) $this->collateDataFields($this->sequentialSaveableSet, true);
return $this->sequentialSaveableSet;
}
protected function flushFieldsCache() {
$this->sequentialSet = null;
$this->sequentialSaveableSet = null;
}
protected function collateDataFields(&$list, $saveableOnly = false) {
foreach($this as $field) {
if($field->isComposite()) $field->collateDataFields($list, $saveableOnly);
if($saveableOnly) {
$isIncluded = ($field->hasData() && !$field->isReadonly() && !$field->isDisabled());
} else {
$isIncluded = ($field->hasData());
}
if($isIncluded) {
$name = $field->Name();
if(isset($list[$name])) {
$errSuffix = "";
if($this->form) $errSuffix = " in your '{$this->form->class}' form called '" . $this->form->Name() . "'";
else $errSuffix = '';
user_error("collateDataFields() I noticed that a field called '$name' appears twice$errSuffix.", E_USER_ERROR);
}
$list[$name] = $field;
}
}
}
/**
* Add an extra field to a tab within this fieldset.
* This is most commonly used when overloading getCMSFields()
*
* @param string $tabName The name of the tab or tabset. Subtabs can be referred to as TabSet.Tab or TabSet.Tab.Subtab.
* This function will create any missing tabs.
* @param FormField $field The {@link FormField} object to add to the end of that tab.
* @param string $insertBefore The name of the field to insert before. Optional.
*/
public function addFieldToTab($tabName, $field, $insertBefore = null) {
// This is a cache that must be flushed
$this->flushFieldsCache();
// Find the tab
$tab = $this->findOrMakeTab($tabName);
// Add the field to the end of this set
if($insertBefore) $tab->insertBefore($field, $insertBefore);
else $tab->push($field);
}
/**
* Add a number of extra fields to a tab within this fieldset.
* This is most commonly used when overloading getCMSFields()
*
* @param string $tabName The name of the tab or tabset. Subtabs can be referred to as TabSet.Tab or TabSet.Tab.Subtab.
* This function will create any missing tabs.
* @param array $fields An array of {@link FormField} objects.
*/
public function addFieldsToTab($tabName, $fields, $insertBefore = null) {
$this->flushFieldsCache();
// Find the tab
$tab = $this->findOrMakeTab($tabName);
// Add the fields to the end of this set
foreach($fields as $field) {
// Check if a field by the same name exists in this tab
if($insertBefore) {
$tab->insertBefore($field, $insertBefore);
} elseif($tab->fieldByName($field->Name())) {
// It exists, so we need to replace the old one
$this->replaceField($field->Name(), $field);
} else {
$tab->push($field);
}
}
}
/**
* Remove the given field from the given tab in the field.
*
* @param string $tabName The name of the tab
* @param string $fieldName The name of the field
*/
public function removeFieldFromTab($tabName, $fieldName) {
$this->flushFieldsCache();
// Find the tab
$tab = $this->findOrMakeTab($tabName);
$tab->removeByName($fieldName);
}
/**
* Removes a number of fields from a Tab/TabSet within this FieldSet.
*
* @param string $tabName The name of the Tab or TabSet field
* @param array $fields A list of fields, e.g. array('Name', 'Email')
*/
public function removeFieldsFromTab($tabName, $fields) {
$this->flushFieldsCache();
// Find the tab
$tab = $this->findOrMakeTab($tabName);
// Add the fields to the end of this set
foreach($fields as $field) $tab->removeByName($field);
}
/**
* Remove a field from this FieldSet by Name.
* The field could also be inside a CompositeField.
*
* @param string $fieldName The name of the field or tab
* @param boolean $dataFieldOnly If this is true, then a field will only
* be removed if it's a data field. Dataless fields, such as tabs, will
* be left as-is.
*/
public function removeByName($fieldName, $dataFieldOnly = false) {
if(!$fieldName) {
user_error('FieldSet::removeByName() was called with a blank field name.', E_USER_WARNING);
}
$this->flushFieldsCache();
foreach($this->items as $i => $child) {
if(is_object($child)){
if(($child->Name() == $fieldName || $child->Title() == $fieldName) && (!$dataFieldOnly || $child->hasData())) {
array_splice( $this->items, $i, 1 );
break;
} else if($child->isComposite()) {
$child->removeByName($fieldName, $dataFieldOnly);
}
}
}
}
/**
* Replace a single field with another. Ignores dataless fields such as Tabs and TabSets
*
* @param string $fieldName The name of the field to replace
* @param FormField $newField The field object to replace with
* @return boolean TRUE field was successfully replaced
* FALSE field wasn't found, nothing changed
*/
public function replaceField($fieldName, $newField) {
$this->flushFieldsCache();
foreach($this->items as $i => $field) {
if(is_object($field)) {
if($field->Name() == $fieldName && $field->hasData()) {
$this->items[$i] = $newField;
return true;
} else if($field->isComposite()) {
if($field->replaceField($fieldName, $newField)) return true;
}
}
}
return false;
}
/**
* Rename the title of a particular field name in this set.
*
* @param string $fieldName Name of field to rename title of
* @param string $newFieldTitle New title of field
* @return boolean
*/
function renameField($fieldName, $newFieldTitle) {
$field = $this->dataFieldByName($fieldName);
if(!$field) return false;
$field->setTitle($newFieldTitle);
return $field->Title() == $newFieldTitle;
}
/**
* @return boolean
*/
public function hasTabSet() {
foreach($this->items as $i => $field) {
if(is_object($field) && $field instanceof TabSet) {
return true;
}
}
return false;
}
/**
* Returns the specified tab object, creating it if necessary.
*
* @todo Support recursive creation of TabSets
*
* @param string $tabName The tab to return, in the form "Tab.Subtab.Subsubtab".
* Caution: Does not recursively create TabSet instances, you need to make sure everything
* up until the last tab in the chain exists.
* @param string $title Natural language title of the tab. If {@link $tabName} is passed in dot notation,
* the title parameter will only apply to the innermost referenced tab.
* The title is only changed if the tab doesn't exist already.
* @return Tab The found or newly created Tab instance
*/
public function findOrMakeTab($tabName, $title = null) {
$parts = explode('.',$tabName);
// We could have made this recursive, but I've chosen to keep all the logic code within FieldSet rather than add it to TabSet and Tab too.
$currentPointer = $this;
foreach($parts as $k => $part) {
$parentPointer = $currentPointer;
$currentPointer = $currentPointer->fieldByName($part);
// Create any missing tabs
if(!$currentPointer) {
if(is_a($parentPointer, 'TabSet')) {
// use $title on the innermost tab only
if($title && $k == count($parts)-1) {
$currentPointer = new Tab($part, $title);
} else {
$currentPointer = new Tab($part);
}
$parentPointer->push($currentPointer);
} else {
$withName = ($parentPointer->hasMethod('Name')) ? " named '{$parentPointer->Name()}'" : null;
user_error("FieldSet::addFieldToTab() Tried to add a tab to object '{$parentPointer->class}'{$withName} - '$part' didn't exist.", E_USER_ERROR);
}
}
}
return $currentPointer;
}
/**
* Returns a named field.
* You can use dot syntax to get fields from child composite fields
*
* @todo Implement similarly to dataFieldByName() to support nested sets - or merge with dataFields()
*/
public function fieldByName($name) {
if(strpos($name,'.') !== false) list($name, $remainder) = explode('.',$name,2);
else $remainder = null;
foreach($this->items as $child) {
if(trim($name) == trim($child->Name()) || $name == $child->id) {
if($remainder) {
if($child->isComposite()) {
return $child->fieldByName($remainder);
} else {
user_error("Trying to get field '$remainder' from non-composite field $child->class.$name", E_USER_WARNING);
return null;
}
} else {
return $child;
}
}
}
}
/**
* Returns a named field in a sequential set.
* Use this if you're using nested FormFields.
*
* @param string $name The name of the field to return
* @return FormField instance
*/
public function dataFieldByName($name) {
if($dataFields = $this->dataFields()) {
foreach($dataFields as $child) {
if(trim($name) == trim($child->Name()) || $name == $child->id) return $child;
}
}
}
/**
* Inserts a field before a particular field in a FieldSet.
*
* @param FormField $item The form field to insert
* @param string $name Name of the field to insert before
*/
public function insertBefore($item, $name) {
$this->onBeforeInsert($item);
$item->setContainerFieldSet($this);
$i = 0;
foreach($this->items as $child) {
if($name == $child->Name() || $name == $child->id) {
array_splice($this->items, $i, 0, array($item));
return $item;
} elseif($child->isComposite()) {
$ret = $child->insertBefore($item, $name);
if($ret) return $ret;
}
$i++;
}
return false;
}
/**
* Inserts a field after a particular field in a FieldSet.
*
* @param FormField $item The form field to insert
* @param string $name Name of the field to insert after
*/
public function insertAfter($item, $name) {
$this->onBeforeInsert($item);
$item->setContainerFieldSet($this);
$i = 0;
foreach($this->items as $child) {
if($name == $child->Name() || $name == $child->id) {
array_splice($this->items, $i+1, 0, array($item));
return $item;
} elseif($child->isComposite()) {
$ret = $child->insertAfter($item, $name);
if($ret) return $ret;
}
$i++;
}
return false;
}
/**
* Push a single field into this FieldSet instance.
*
* @param FormField $item The FormField to add
* @param string $key An option array key (field name)
*/
public function push($item, $key = null) {
$this->onBeforeInsert($item);
$item->setContainerFieldSet($this);
return parent::push($item, $key = null);
}
/**
* Handler method called before the FieldSet is going to be manipulated.
*/
protected function onBeforeInsert($item) {
$this->flushFieldsCache();
if($item->Name()) $this->rootFieldSet()->removeByName($item->Name(), true);
}
/**
* Set the Form instance for this FieldSet.
*
* @param Form $form The form to set this FieldSet to
*/
public function setForm($form) {
foreach($this as $field) $field->setForm($form);
}
/**
* Load the given data into this form.
*
* @param data An map of data to load into the FieldSet
*/
public function setValues($data) {
foreach($this->dataFields() as $field) {
$fieldName = $field->Name();
if(isset($data[$fieldName])) $field->setValue($data[$fieldName]);
}
}
/**
* Return all <input type="hidden"> fields
* in a form - including fields nested in {@link CompositeFields}.
* Useful when doing custom field layouts.
*
* @return FieldSet
*/
function HiddenFields() {
$hiddenFields = new HiddenFieldSet();
$dataFields = $this->dataFields();
if($dataFields) foreach($dataFields as $field) {
if($field instanceof HiddenField) $hiddenFields->push($field);
}
return $hiddenFields;
}
/**
* Transform this FieldSet with a given tranform method,
* e.g. $this->transform(new ReadonlyTransformation())
*
* @return FieldSet
*/
function transform($trans) {
$this->flushFieldsCache();
$newFields = new FieldSet();
foreach($this as $field) {
$newFields->push($field->transform($trans));
}
return $newFields;
}
/**
* Returns the root field set that this belongs to
*/
function rootFieldSet() {
if($this->containerField) return $this->containerField->rootFieldSet();
else return $this;
}
function setContainerField($field) {
$this->containerField = $field;
}
/**
* Transforms this FieldSet instance to readonly.
*
* @return FieldSet
*/
function makeReadonly() {
return $this->transform(new ReadonlyTransformation());
}
/**
* Transform the named field into a readonly feld.
*
* @param string|FormField
*/
function makeFieldReadonly($field) {
$fieldName = ($field instanceof FormField) ? $field->Name() : $field;
$srcField = $this->dataFieldByName($fieldName);
$this->replaceField($fieldName, $srcField->performReadonlyTransformation());
}
/**
* Change the order of fields in this FieldSet by specifying an ordered list of field names.
* This works well in conjunction with SilverStripe's scaffolding functions: take the scaffold, and
* shuffle the fields around to the order that you want.
*
* Please note that any tabs or other dataless fields will be clobbered by this operation.
*
* @param array $fieldNames Field names can be given as an array, or just as a list of arguments.
*/
function changeFieldOrder($fieldNames) {
// Field names can be given as an array, or just as a list of arguments.
if(!is_array($fieldNames)) $fieldNames = func_get_args();
// Build a map of fields indexed by their name. This will make the 2nd step much easier.
$fieldMap = array();
foreach($this->dataFields() as $field) $fieldMap[$field->Name()] = $field;
// Iterate through the ordered list of names, building a new array to be put into $this->items.
// While we're doing this, empty out $fieldMap so that we can keep track of leftovers.
// Unrecognised field names are okay; just ignore them
$fields = array();
foreach($fieldNames as $fieldName) {
if(isset($fieldMap[$fieldName])) {
$fields[] = $fieldMap[$fieldName];
unset($fieldMap[$fieldName]);
}
}
// Add the leftover fields to the end of the list.
$fields = $fields + array_values($fieldMap);
// Update our internal $this->items parameter.
$this->items = $fields;
$this->flushFieldsCache();
}
/**
* Find the numerical position of a field within
* the children collection. Doesn't work recursively.
*
* @param string|FormField
* @return Position in children collection (first position starts with 0). Returns FALSE if the field can't be found.
*/
function fieldPosition($field) {
if(is_object($field)) $field = $field->Name();
$i = 0;
foreach($this->dataFields() as $child) {
if($child->Name() == $field) return $i;
$i++;
}
return false;
}
}
/**
* A fieldset designed to store a list of hidden fields. When inserted into a template, only the
* input tags will be included
* @deprecated 3.0 Please use {@link FieldList}.
*
* @package forms
* @subpackage fields-structural
*/
class HiddenFieldSet extends FieldSet {
function forTemplate() {
$output = "";
foreach($this as $field) {
$output .= $field->Field();
}
return $output;
class FieldSet extends FieldList {
public function __construct($items = array()) {
user_error(
'FieldSet is deprecated, please use FieldList instead.', E_USER_NOTICE
);
parent::__construct(!is_array($items) || func_num_args() > 1 ? func_get_args(): $items);
}
}
?>

View File

@ -17,11 +17,11 @@
* class ExampleForm_Controller extends Page_Controller {
*
* public function Form() {
* $fields = new FieldSet(
* $fields = new FieldList(
* new TextField('MyName'),
* new FileField('MyFile')
* );
* $actions = new FieldSet(
* $actions = new FieldList(
* new FormAction('doUpload', 'Upload file')
* );
* $validator = new RequiredFields(array('MyName', 'MyFile'));

View File

@ -145,7 +145,7 @@ class FileIFrameField extends FileField {
$fileSources["existing//$selectFile"] = new TreeDropdownField('ExistingFile', '', 'File');
$fields = new FieldSet (
$fields = new FieldList (
new HeaderField('EditFileHeader', $title),
new SelectionGroup('FileSource', $fileSources)
);
@ -159,7 +159,7 @@ class FileIFrameField extends FileField {
$this,
'EditFileForm',
$fields,
new FieldSet(
new FieldList(
new FormAction('save', $title)
)
);
@ -231,10 +231,10 @@ class FileIFrameField extends FileField {
$form = new Form (
$this,
'DeleteFileForm',
new FieldSet (
new FieldList (
new HiddenField('DeleteFile', null, false)
),
new FieldSet (
new FieldList (
$deleteButton = new FormAction (
'delete', sprintf(_t('FileIFrameField.DELETE', 'Delete %s'), $this->FileTypeName())
)

View File

@ -142,15 +142,15 @@ class Form extends RequestHandler {
*
* @param Controller $controller The parent controller, necessary to create the appropriate form action tag.
* @param String $name The method on the controller that will return this form object.
* @param FieldSet $fields All of the fields in the form - a {@link FieldSet} of {@link FormField} objects.
* @param FieldSet $actions All of the action buttons in the form - a {@link FieldSet} of {@link FormAction} objects
* @param FieldList $fields All of the fields in the form - a {@link FieldSet} of {@link FormField} objects.
* @param FieldList $actions All of the action buttons in the form - a {@link FieldSet} of {@link FormAction} objects
* @param Validator $validator Override the default validator instance (Default: {@link RequiredFields})
*/
function __construct($controller, $name, FieldSet $fields, FieldSet $actions, $validator = null) {
function __construct($controller, $name, FieldList $fields, FieldList $actions, $validator = null) {
parent::__construct();
if(!$fields instanceof FieldSet) throw new InvalidArgumentException('$fields must be a valid FieldSet instance');
if(!$actions instanceof FieldSet) throw new InvalidArgumentException('$fields must be a valid FieldSet instance');
if(!$fields instanceof FieldList) throw new InvalidArgumentException('$fields must be a valid FieldList instance');
if(!$actions instanceof FieldList) throw new InvalidArgumentException('$fields must be a valid FieldList instance');
if($validator && !$validator instanceof Validator) throw new InvalidArgumentException('$validator must be a Valdidator instance');
$fields->setForm($this);
@ -398,13 +398,13 @@ class Form extends RequestHandler {
}
function transform(FormTransformation $trans) {
$newFields = new FieldSet();
$newFields = new FieldList();
foreach($this->fields as $field) {
$newFields->push($field->transform($trans));
}
$this->fields = $newFields;
$newActions = new FieldSet();
$newActions = new FieldList();
foreach($this->actions as $action) {
$newActions->push($action->transform($trans));
}
@ -445,7 +445,7 @@ class Form extends RequestHandler {
* Convert this form to another format.
*/
function transformTo(FormTransformation $format) {
$newFields = new FieldSet();
$newFields = new FieldList();
foreach($this->fields as $field) {
$newFields->push($field->transformTo($format));
}
@ -463,7 +463,7 @@ class Form extends RequestHandler {
* @return FieldSet
*/
public function getExtraFields() {
$extraFields = new FieldSet();
$extraFields = new FieldList();
$token = $this->getSecurityToken();
$tokenField = $token->updateFieldSet($this->fields);
@ -507,7 +507,7 @@ class Form extends RequestHandler {
/**
* Setter for the form fields.
*
* @param FieldSet $fields
* @param FieldList $fields
*/
function setFields($fields) {
$this->fields = $fields;
@ -540,7 +540,7 @@ class Form extends RequestHandler {
/**
* Setter for the form actions.
*
* @param FieldSet $actions
* @param FieldList $actions
*/
function setActions($actions) {
$this->actions = $actions;
@ -550,7 +550,7 @@ class Form extends RequestHandler {
* Unset all form actions
*/
function unsetAllActions(){
$this->actions = new FieldSet();
$this->actions = new FieldList();
}
/**

View File

@ -119,10 +119,17 @@ class FormField extends RequestHandler {
*
* @return string
*/
function Name() {
function getName() {
return $this->name;
}
/**
* @deprecated 3.0 Use {@link getName()}.
*/
public function Name() {
return $this->getName();
}
function attrName() {
return $this->name;
}
@ -649,7 +656,7 @@ HTML;
/**
* Set the fieldset that contains this field.
*
* @param FieldSet $containerFieldSet
* @param FieldList $containerFieldSet
*/
function setContainerFieldSet($containerFieldSet) {
$this->containerFieldSet = $containerFieldSet;

View File

@ -65,7 +65,7 @@ class FormScaffolder extends Object {
* @return FieldSet
*/
public function getFieldSet() {
$fields = new FieldSet();
$fields = new FieldList();
// tabbed or untabbed
if($this->tabbed) {
@ -124,15 +124,15 @@ class FormScaffolder extends Object {
);
}
$relationshipFields = singleton($component)->summaryFields();
$foreignKey = $this->obj->getRemoteJoinField($relationship);
$fieldClass = (isset($this->fieldClasses[$relationship])) ? $this->fieldClasses[$relationship] : 'ComplexTableField';
$ctf = new $fieldClass(
$this,
$relationship,
$component,
null,
$relationshipFields,
"getCMSFields",
"\"$foreignKey\" = " . $this->obj->ID
"getCMSFields"
);
$ctf->setPermissions(TableListField::permissions_for_object($component));
if($this->tabbed) {
@ -153,19 +153,15 @@ class FormScaffolder extends Object {
}
$relationshipFields = singleton($component)->summaryFields();
$filterWhere = $this->obj->getManyManyFilter($relationship, $component);
$filterJoin = $this->obj->getManyManyJoin($relationship, $component);
$fieldClass = (isset($this->fieldClasses[$relationship])) ? $this->fieldClasses[$relationship] : 'ComplexTableField';
$ctf = new $fieldClass(
$this,
$relationship,
$component,
null,
$relationshipFields,
"getCMSFields",
$filterWhere,
'',
$filterJoin
"getCMSFields"
);
$ctf->setPermissions(TableListField::permissions_for_object($component));
$ctf->popupClass = "ScaffoldingComplexTableField_Popup";
if($this->tabbed) {
@ -195,6 +191,4 @@ class FormScaffolder extends Object {
'ajaxSafe' => $this->ajaxSafe
);
}
}
?>

View File

@ -142,8 +142,7 @@ class HtmlEditorField extends TextareaField {
// Save file & link tracking data.
if(class_exists('SiteTree')) {
if($record->ID && $record->many_many('LinkTracking') && $tracker = $record->LinkTracking()) {
$filter = sprintf('"FieldName" = \'%s\' AND "SiteTreeID" = %d', $this->name, $record->ID);
DB::query("DELETE FROM \"$tracker->tableName\" WHERE $filter");
$tracker->removeByFilter(sprintf('"FieldName" = \'%s\' AND "SiteTreeID" = %d', $this->name, $record->ID));
if($linkedPages) foreach($linkedPages as $item) {
$SQL_fieldName = Convert::raw2sql($this->name);
@ -153,8 +152,7 @@ class HtmlEditorField extends TextareaField {
}
if($record->ID && $record->many_many('ImageTracking') && $tracker = $record->ImageTracking()) {
$filter = sprintf('"FieldName" = \'%s\' AND "SiteTreeID" = %d', $this->name, $record->ID);
DB::query("DELETE FROM \"$tracker->tableName\" WHERE $filter");
$tracker->removeByFilter(sprintf('"FieldName" = \'%s\' AND "SiteTreeID" = %d', $this->name, $record->ID));
$fieldName = $this->name;
if($linkedFiles) foreach($linkedFiles as $item) {
@ -237,7 +235,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
$form = new Form(
$this->controller,
"{$this->name}/LinkForm",
new FieldSet(
new FieldList(
new LiteralField(
'Heading',
sprintf('<h3>%s</h3>', _t('HtmlEditorField.LINK', 'Link'))
@ -265,7 +263,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
new HiddenField('Locale', null, $this->controller->Locale)
)
),
new FieldSet(
new FieldList(
new FormAction('insert', _t('HtmlEditorField.BUTTONINSERTLINK', 'Insert link')),
new FormAction('remove', _t('HtmlEditorField.BUTTONREMOVELINK', 'Remove link'))
)
@ -293,7 +291,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
throw new Exception('ThumbnailStripField class required for HtmlEditorField->ImageForm()');
}
$fields = new FieldSet(
$fields = new FieldList(
new LiteralField(
'Heading',
sprintf('<h3>%s</h3>', _t('HtmlEditorField.IMAGE', 'Image'))
@ -301,7 +299,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
$contentComposite = new CompositeField(
new TreeDropdownField('FolderID', _t('HtmlEditorField.FOLDER', 'Folder'), 'Folder'),
new CompositeField(new FieldSet(
new CompositeField(new FieldList(
new LiteralField('ShowUpload', '<p class="showUploadField"><a href="#">'. _t('HtmlEditorField.SHOWUPLOADFORM', 'Upload File') .'</a></p>'),
new FileField("Files[0]" , _t('AssetAdmin.CHOOSEFILE','Choose file: ')),
new LiteralField('Response', '<div id="UploadFormResponse"></div>'),
@ -329,7 +327,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
)
);
$actions = new FieldSet(
$actions = new FieldList(
new FormAction('insertimage', _t('HtmlEditorField.BUTTONINSERTIMAGE', 'Insert image'))
);
@ -361,7 +359,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
$form = new Form(
$this->controller,
"{$this->name}/FlashForm",
new FieldSet(
new FieldList(
new LiteralField(
'Heading',
sprintf('<h3>%s</h3>', _t('HtmlEditorField.FLASH', 'Flash'))
@ -376,7 +374,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
)
)
),
new FieldSet(
new FieldList(
new FormAction("insertflash", _t('HtmlEditorField.BUTTONINSERTFLASH', 'Insert Flash'))
)
);

View File

@ -34,7 +34,7 @@
* $map = $myDoSet->toDropDownMap();
*
* // Instantiate the OptionsetField
* $fieldset = new Fieldset(
* $fieldset = new FieldList(
* new OptionsetField(
* $name = "Foobar",
* $title = "FooBar's optionset",
@ -136,7 +136,7 @@ class OptionsetField extends DropdownField {
}
function ExtraOptions() {
return new DataObjectSet();
return new ArrayList();
}
}
?>

View File

@ -16,7 +16,7 @@ class ScaffoldingComplexTableField_Popup extends ComplexTableField_Popup {
Requirements::clear();
$actions = new FieldSet();
$actions = new FieldList();
if(!$readonly) {
$actions->push(
$saveAction = new FormAction("saveComplexTableField", "Save")

View File

@ -42,7 +42,7 @@ class SelectionGroup extends CompositeField {
$newChildren[$idx] = $child;
}
$clone->setChildren(new FieldSet($newChildren));
$clone->setChildren(new FieldList($newChildren));
$clone->setReadonly(true);
return $clone;
}
@ -74,7 +74,7 @@ class SelectionGroup extends CompositeField {
$firstSelected = $checked ="";
}
return new DataObjectSet($newItems);
return new ArrayList($newItems);
}
function hasData() {

View File

@ -32,12 +32,12 @@
*
* <code>
* function Form() {
* return new Form($this, "Form", new FieldSet(
* return new Form($this, "Form", new FieldList(
* new SimpleImageField (
* $name = "FileTypeID",
* $title = "Upload your FileType"
* )
* ), new FieldSet(
* ), new FieldList(
*
* // List the action buttons here - doform executes the function 'doform' below
* new FormAction("doform", "Submit")

View File

@ -28,10 +28,6 @@
class TableField extends TableListField {
protected $sourceClass;
protected $sourceFilter;
protected $fieldList;
/**
@ -53,10 +49,6 @@ class TableField extends TableListField {
*/
protected $fieldTypes;
protected $sourceSort;
protected $sourceJoin;
/**
* @var $template string Template-Overrides
*/
@ -105,8 +97,6 @@ class TableField extends TableListField {
*
* @var boolean
*/
protected $relationAutoSetting = true;
function __construct($name, $sourceClass, $fieldList = null, $fieldTypes, $filterField = null,
$sourceFilter = null, $editExisting = true, $sourceSort = null, $sourceJoin = null) {
@ -138,7 +128,7 @@ class TableField extends TableListField {
$headings[] = new ArrayData(array("Name" => $fieldName, "Title" => $fieldTitle, "Class" => $class));
$i++;
}
return new DataObjectSet($headings);
return new ArrayList($headings);
}
/**
@ -161,19 +151,23 @@ class TableField extends TableListField {
*/
function Items() {
// holds TableField_Item instances
$items = new DataObjectSet();
$items = new ArrayList();
$sourceItems = $this->sourceItems();
// either load all rows from the field value,
// (e.g. when validation failed), or from sourceItems()
if($this->value) {
if(!$sourceItems) $sourceItems = new DataObjectSet();
if(!$sourceItems) $sourceItems = new ArrayList();
// get an array keyed by rows, rather than values
$rows = $this->sortData(ArrayLib::invert($this->value));
// ignore all rows which are already saved
if(isset($rows['new'])) {
if($sourceItems instanceof DataList) {
$sourceItems = new ArrayList($sourceItems->toArray());
}
$newRows = $this->sortData($rows['new']);
// iterate over each value (not each row)
$i = 0;
@ -189,8 +183,8 @@ class TableField extends TableListField {
}
// generate a temporary DataObject container (not saved in the database)
$sourceClass = $this->sourceClass;
$sourceItems->push(new $sourceClass($newRow));
$sourceClass = $this->sourceClass();
$sourceItems->add(new $sourceClass($newRow));
$i++;
}
@ -223,11 +217,11 @@ class TableField extends TableListField {
$this,
null,
$this->FieldSetForRow(),
new FieldSet()
new FieldList()
);
$form->loadDataFrom($dataObj);
// Add the item to our new DataObjectSet, with a wrapper class.
// Add the item to our new ArrayList, with a wrapper class.
return new TableField_Item($dataObj, $this, $form, $this->fieldTypes);
}
@ -264,16 +258,9 @@ class TableField extends TableListField {
$savedObjIds = $this->saveData($newFields,false);
}
// Optionally save the newly created records into a relationship
// on $record. This assumes the name of this formfield instance
// is set to a relationship name on $record.
if($this->relationAutoSetting) {
$relationName = $this->Name();
if($record->has_many($relationName) || $record->many_many($relationName)) {
// Add the new records to the DataList
if($savedObjIds) foreach($savedObjIds as $id => $status) {
$record->$relationName()->add($id);
}
}
$this->getDataList()->add($id);
}
// Update the internal source items cache
@ -294,7 +281,7 @@ class TableField extends TableListField {
* @return FieldSet
*/
function FieldSetForRow() {
$fieldset = new FieldSet();
$fieldset = new FieldList();
if($this->fieldTypes){
foreach($this->fieldTypes as $key => $fieldType) {
if(isset($fieldType->class) && is_subclass_of($fieldType, 'FormField')) {
@ -373,7 +360,7 @@ class TableField extends TableListField {
}
}
$form = new Form($this, null, $fieldset, new FieldSet());
$form = new Form($this, null, $fieldset, new FieldList());
foreach ($dataObjects as $objectid => $fieldValues) {
// 'new' counts as an empty column, don't save it
@ -386,9 +373,9 @@ class TableField extends TableListField {
// either look for an existing object, or create a new one
if($existingValues) {
$obj = DataObject::get_by_id($this->sourceClass, $objectid);
$obj = DataObject::get_by_id($this->sourceClass(), $objectid);
} else {
$sourceClass = $this->sourceClass;
$sourceClass = $this->sourceClass();
$obj = new $sourceClass();
}
@ -491,13 +478,6 @@ class TableField extends TableListField {
return $this->renderWith($this->template);
}
/**
* @return Int
*/
function sourceID() {
return $this->filterField;
}
function setTransformationConditions($conditions) {
$this->transformationConditions = $conditions;
}
@ -601,20 +581,6 @@ JS;
function setRequiredFields($fields) {
$this->requiredFields = $fields;
}
/**
* @param boolean $value
*/
function setRelationAutoSetting($value) {
$this->relationAutoSetting = $value;
}
/**
* @return boolean
*/
function getRelationAutoSetting() {
return $this->relationAutoSetting;
}
}
/**
@ -626,7 +592,7 @@ JS;
class TableField_Item extends TableListField_Item {
/**
* @var FieldSet $fields
* @var FieldList $fields
*/
protected $fields;
@ -753,7 +719,7 @@ class TableField_Item extends TableListField_Item {
$i++;
}
}
return new FieldSet($this->fields);
return new FieldList($this->fields);
}
function Fields() {

View File

@ -21,19 +21,10 @@
* @subpackage fields-relational
*/
class TableListField extends FormField {
/**
* @var $cachedSourceItems DataObjectSet Prevent {@sourceItems()} from being called multiple times.
* The {@link DataList} object defining the source data for this view/
*/
protected $cachedSourceItems;
protected $sourceClass;
protected $sourceFilter = "";
protected $sourceSort = "";
protected $sourceJoin = array();
protected $dataList;
protected $fieldList;
@ -138,27 +129,11 @@ class TableListField extends FormField {
*/
public $defaultAction = '';
/**
* @var $customQuery Specify custom query, e.g. for complicated having/groupby-constructs.
* Caution: TableListField automatically selects the ID from the {@sourceClass}, because it relies
* on this information e.g. in saving a TableField. Please use a custom select if you want to filter
* for other IDs in joined tables: $query->select[] = "MyJoinedTable.ID AS MyJoinedTableID"
*/
protected $customQuery;
/**
* @var $customCsvQuery Query for CSV-export (might need different fields or further filtering)
*/
protected $customCsvQuery;
/**
* @var $customSourceItems DataObjectSet Use the manual setting of a result-set only as a last-resort
* for sets which can't be resolved in a single query.
*
* @todo Add pagination support for customSourceItems.
*/
protected $customSourceItems;
/**
* Character to seperate exported columns in the CSV file
*/
@ -183,12 +158,6 @@ class TableListField extends FormField {
"\n"=>"",
);
/**
* @var int Shows total count regardless or pagination
*/
protected $totalCount;
/**
* @var boolean Trigger pagination
*/
@ -257,26 +226,34 @@ class TableListField extends FormField {
protected $__cachedQuery;
function __construct($name, $sourceClass, $fieldList = null, $sourceFilter = null,
/**
* This is a flag that enables some backward-compatibility helpers.
*/
private $getDataListFromForm;
function __construct($name, $sourceClass = null, $fieldList = null, $sourceFilter = null,
$sourceSort = null, $sourceJoin = null) {
$this->fieldList = ($fieldList) ? $fieldList : singleton($sourceClass)->summaryFields();
$this->sourceClass = $sourceClass;
$this->sourceFilter = $sourceFilter;
$this->sourceSort = $sourceSort;
$this->sourceJoin = $sourceJoin;
if($sourceClass) {
// You can optionally pass a list
if($sourceClass instanceof SS_List) {
$this->dataList = $sourceClass;
} else {
$this->dataList = DataObject::get($sourceClass)->where($sourceFilter)
->sort($sourceSort)->join($sourceJoin);
// Grab it from the form relation, if available.
$this->getDataListFromForm = true;
}
}
$this->fieldList = ($fieldList) ? $fieldList : singleton($this->sourceClass())->summaryFields();
$this->readOnly = false;
parent::__construct($name);
}
/**
* Get the filter
*/
function sourceFilter() {
return $this->sourceFilter;
}
function index() {
return $this->FieldHolder();
}
@ -287,7 +264,10 @@ class TableListField extends FormField {
);
function sourceClass() {
return $this->sourceClass;
$list = $this->getDataList();
if(method_exists($list, 'dataClass')) return $list->dataClass();
// Failover for DataObjectSet
else return get_class($list->First());
}
function handleItem($request) {
@ -351,14 +331,14 @@ JS
$headings[] = new ArrayData(array(
"Name" => $fieldName,
"Title" => ($this->sourceClass) ? singleton($this->sourceClass)->fieldLabel($fieldTitle) : $fieldTitle,
"Title" => ($this->sourceClass()) ? singleton($this->sourceClass())->fieldLabel($fieldTitle) : $fieldTitle,
"IsSortable" => $isSortable,
"SortLink" => $sortLink,
"SortBy" => $isSorted,
"SortDirection" => (isset($_REQUEST['ctf'][$this->Name()]['dir'])) ? $_REQUEST['ctf'][$this->Name()]['dir'] : null
));
}
return new DataObjectSet($headings);
return new ArrayList($headings);
}
function disableSorting($to = true) {
@ -374,13 +354,10 @@ JS
* @return bool
*/
function isFieldSortable($fieldName) {
if($this->customSourceItems || $this->disableSorting) {
return false;
}
if(!$this->__cachedQuery) $this->__cachedQuery = $this->getQuery();
return $this->__cachedQuery->canSortBy($fieldName);
if($this->disableSorting) return false;
$list = $this->getDataList();
if(method_exists($list,'canSortBy')) return $list->canSortBy($fieldName);
else return false;
}
/**
@ -390,7 +367,7 @@ JS
* @return DataObjectSet
*/
function Actions() {
$allowedActions = new DataObjectSet();
$allowedActions = new ArrayList();
foreach($this->actions as $actionName => $actionSettings) {
if($this->Can($actionName)) {
$allowedActions->push(new ViewableData());
@ -403,80 +380,64 @@ JS
/**
* Provide a custom query to compute sourceItems. This is the preferred way to using
* {@setSourceItems}, because we can still paginate.
* Caution: Other parameters such as {@sourceFilter} will be ignored.
* Please use this only as a fallback for really complex queries (e.g. involving HAVING and GROUPBY).
*
* @param $query SS_Query
* @param $query DataList
*/
function setCustomQuery(SQLQuery $query) {
// The type-hinting above doesn't seem to work consistently
if($query instanceof SQLQuery) {
$this->customQuery = $query;
} else {
user_error('TableList::setCustomQuery() should be passed a SQLQuery', E_USER_WARNING);
}
function setCustomQuery(DataList $dataList) {
$this->dataList = $dataList;
}
function setCustomCsvQuery(SQLQuery $query) {
// The type-hinting above doesn't seem to work consistently
if($query instanceof SQLQuery) {
function setCustomCsvQuery(DataList $dataList) {
$this->customCsvQuery = $query;
} else {
user_error('TableList::setCustomCsvQuery() should be passed a SQLQuery', E_USER_WARNING);
}
}
function setCustomSourceItems(DataObjectSet $items) {
function setCustomSourceItems(SS_List $items) {
user_error('TableList::setCustomSourceItems() deprecated, just pass the items into the constructor', E_USER_WARNING);
// The type-hinting above doesn't seem to work consistently
if($items instanceof DataObjectSet) {
$this->customSourceItems = $items;
$this->dataList = $items;
} else {
user_error('TableList::setCustomSourceItems() should be passed a DataObjectSet', E_USER_WARNING);
}
}
/**
* Get items, with sort & limit applied
*/
function sourceItems() {
$SQL_limit = ($this->showPagination && $this->pageSize) ? "{$this->pageSize}" : null;
// get items (this may actually be a DataObjectSet)
$items = clone $this->getDataList();
// TODO: Sorting could be implemented on regular DataObjectSets.
if(method_exists($items,'canSortBy') && isset($_REQUEST['ctf'][$this->Name()]['sort'])) {
$sort = $_REQUEST['ctf'][$this->Name()]['sort'];
// TODO: sort direction
if($items->canSortBy($sort)) $items = $items->sort($sort);
}
// Determine pagination limit, offset
// To disable pagination, set $this->showPagination to false.
if($this->showPagination && $this->pageSize) {
$SQL_limit = (int)$this->pageSize;
if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) {
$SQL_start = (isset($_REQUEST['ctf'][$this->Name()]['start'])) ? intval($_REQUEST['ctf'][$this->Name()]['start']) : "0";
} else {
$SQL_start = 0;
}
if(isset($this->customSourceItems)) {
if($this->showPagination && $this->pageSize) {
$items = $this->customSourceItems->getRange($SQL_start, $SQL_limit);
} else {
$items = $this->customSourceItems;
}
} elseif(isset($this->cachedSourceItems)) {
$items = $this->cachedSourceItems;
} else {
// get query
$dataQuery = $this->getQuery();
// we don't limit when doing certain actions T
$methodName = isset($_REQUEST['url']) ? array_pop(explode('/', $_REQUEST['url'])) : null;
if(!$methodName || !in_array($methodName,array('printall','export'))) {
$dataQuery->limit(array(
'limit' => $SQL_limit,
'start' => (isset($SQL_start)) ? $SQL_start : null
));
}
// get data
$records = $dataQuery->execute();
$sourceClass = $this->sourceClass;
$dataobject = new $sourceClass();
$items = $dataobject->buildDataObjectSet($records, 'DataObjectSet');
$this->cachedSourceItems = $items;
$items = $items->getRange($SQL_start, $SQL_limit);
}
return $items;
}
/**
* Return a DataObjectSet of TableListField_Item objects, suitable for display in the template.
*/
function Items() {
$fieldItems = new DataObjectSet();
$fieldItems = new ArrayList();
if($items = $this->sourceItems()) foreach($items as $item) {
if($item) $fieldItems->push(new $this->itemClass($item, $this));
}
@ -484,40 +445,48 @@ JS
}
/**
* Generates the query for sourceitems (without pagination/limit-clause)
*
* @return string
* Returns the DataList for this field.
*/
function getDataList() {
// If we weren't passed in a DataList to begin with, try and get the datalist from the form
if($this->form && $this->getDataListFromForm) {
$this->getDataListFromForm = false;
$relation = $this->name;
if($record = $this->form->getRecord()) {
if($record->hasMethod($relation)) $this->dataList = $record->$relation();
}
}
if(!$this->dataList) {
user_error(get_class($this). ' is missing a DataList', E_USER_ERROR);
}
return $this->dataList;
}
function getCsvDataList() {
if($this->customCsvQuery) return $this->customCsvQuery;
else return $this->getDataList();
}
/**
* @deprecated Use getDataList() instead.
*/
function getQuery() {
if($this->customQuery) {
$query = clone $this->customQuery;
$baseClass = ClassInfo::baseDataClass($this->sourceClass);
} else {
$query = singleton($this->sourceClass)->extendedSQL($this->sourceFilter(), $this->sourceSort, null, $this->sourceJoin);
}
if(!empty($_REQUEST['ctf'][$this->Name()]['sort'])) {
$column = $_REQUEST['ctf'][$this->Name()]['sort'];
$dir = 'ASC';
if(!empty($_REQUEST['ctf'][$this->Name()]['dir'])) {
$dir = $_REQUEST['ctf'][$this->Name()]['dir'];
if(strtoupper(trim($dir)) == 'DESC') $dir = 'DESC';
}
if($query->canSortBy($column)) $query->orderby = $column.' '.$dir;
}
return $query;
$list = $this->getDataList();
if(method_exists($list,'dataQuery')) {
return $this->getDataList()->dataQuery()->query();
}
}
/**
* @deprecated Use getCsvDataList() instead.
*/
function getCsvQuery() {
$baseClass = ClassInfo::baseDataClass($this->sourceClass);
if($this->customCsvQuery || $this->customQuery) {
$query = $this->customCsvQuery ? $this->customCsvQuery : $this->customQuery;
} else {
$query = singleton($this->sourceClass)->extendedSQL($this->sourceFilter(), $this->sourceSort, null, $this->sourceJoin);
$list = $this->getCsvDataList();
if(method_exists($list,'dataQuery')) {
return $list->dataQuery()->query();
}
return clone $query;
}
function FieldList() {
@ -573,8 +542,7 @@ JS
$childId = Convert::raw2sql($_REQUEST['ctf']['childID']);
if (is_numeric($childId)) {
$childObject = DataObject::get_by_id($this->sourceClass, $childId);
if($childObject) $childObject->delete();
$this->getDataList()->removeById($childId);
}
// TODO return status in JSON etc.
@ -662,7 +630,7 @@ JS
'Title' => DBField::create('Varchar', $fieldTitle),
));
}
return new DataObjectSet($summaryFields);
return new ArrayList($summaryFields);
}
function HasGroupedItems() {
@ -680,9 +648,9 @@ JS
}
$groupedItems = $items->groupBy($this->groupByField);
$groupedArrItems = new DataObjectSet();
$groupedArrItems = new ArrayList();
foreach($groupedItems as $key => $group) {
$fieldItems = new DataObjectSet();
$fieldItems = new ArrayList();
foreach($group as $item) {
if($item) $fieldItems->push(new $this->itemClass($item, $this));
}
@ -895,19 +863,21 @@ JS
}
}
/**
* @ignore
*/
private $_cache_TotalCount;
/**
* Return the total number of items in the source DataList
*/
function TotalCount() {
if($this->totalCount) {
return $this->totalCount;
if($this->_cache_TotalCount === null) {
$this->_cache_TotalCount = $this->getDataList()->Count();
}
if($this->customSourceItems) {
return $this->customSourceItems->Count();
return $this->_cache_TotalCount;
}
$this->totalCount = $this->getQuery()->unlimitedRowCount();
return $this->totalCount;
}
/**
* #################################
@ -971,6 +941,14 @@ JS
$now = Date("d-m-Y-H-i");
$fileName = "export-$now.csv";
// No pagination for export
$oldShowPagination = $this->showPagination;
$this->showPagination = false;
$result = $this->renderWith(array($this->template . '_printable', 'TableListField_printable'));
$this->showPagination = $oldShowPagination;
if($fileData = $this->generateExportFileData($numColumns, $numRows)){
return SS_HTTPRequest::send_file($fileData, $fileName);
}else{
@ -983,7 +961,7 @@ JS
$csvColumns = ($this->fieldListCsv) ? $this->fieldListCsv : $this->fieldList;
$fileData = '';
$columnData = array();
$fieldItems = new DataObjectSet();
$fieldItems = new ArrayList();
if($this->csvHasHeader) {
$fileData .= "\"" . implode("\"{$separator}\"", array_values($csvColumns)) . "\"";
@ -1086,7 +1064,7 @@ JS
* #################################
*/
function Utility() {
$links = new DataObjectSet();
$links = new ArrayList();
if($this->can('export')) {
$links->push(new ArrayData(array(
'Title' => _t('TableListField.CSVEXPORT', 'Export to CSV'),
@ -1151,19 +1129,19 @@ JS
// adding this to TODO probably add a method to the classes
// to return they're translated string
// added by ruibarreiros @ 27/11/2007
return $this->sourceClass ? singleton($this->sourceClass)->singular_name() : $this->Name();
return $this->sourceClass() ? singleton($this->sourceClass())->singular_name() : $this->Name();
}
function NameSingular() {
// same as Title()
// added by ruibarreiros @ 27/11/2007
return $this->sourceClass ? singleton($this->sourceClass)->singular_name() : $this->Name();
return $this->sourceClass() ? singleton($this->sourceClass())->singular_name() : $this->Name();
}
function NamePlural() {
// same as Title()
// added by ruibarreiros @ 27/11/2007
return $this->sourceClass ? singleton($this->sourceClass)->plural_name() : $this->Name();
return $this->sourceClass() ? singleton($this->sourceClass())->plural_name() : $this->Name();
}
function setTemplate($template) {
@ -1210,17 +1188,6 @@ JS
return $this->Link();
}
/**
* @return Int
*/
function sourceID() {
$idField = $this->form->dataFieldByName('ID');
if(!isset($idField)) {
user_error("TableListField needs a formfield named 'ID' to be present", E_USER_ERROR);
}
return $idField->Value();
}
/**
* Helper method to determine permissions for a scaffolded
* TableListField (or subclasses) - currently used in {@link ModelAdmin} and {@link DataObject->scaffoldFormFields()}.
@ -1313,7 +1280,7 @@ JS
function SelectOptions(){
if(!$this->selectOptions) return;
$selectOptionsSet = new DataObjectSet();
$selectOptionsSet = new ArrayList();
foreach($this->selectOptions as $k => $v) {
$selectOptionsSet->push(new ArrayData(array(
'Key' => $k,
@ -1410,7 +1377,7 @@ class TableListField_Item extends ViewableData {
"CsvSeparator" => $this->parent->getCsvSeparator(),
));
}
return new DataObjectSet($fields);
return new ArrayList($fields);
}
function Markable() {
@ -1463,7 +1430,7 @@ class TableListField_Item extends ViewableData {
* @return DataObjectSet
*/
function Actions() {
$allowedActions = new DataObjectSet();
$allowedActions = new ArrayList();
foreach($this->parent->actions as $actionName => $actionSettings) {
if($this->parent->Can($actionName)) {
$allowedActions->push(new ArrayData(array(
@ -1593,42 +1560,11 @@ class TableListField_ItemRequest extends RequestHandler {
// used to discover fields if requested and for population of field
if(is_numeric($this->itemID)) {
// we have to use the basedataclass, otherwise we might exclude other subclasses
return DataObject::get_by_id(ClassInfo::baseDataClass(Object::getCustomClass($this->ctf->sourceClass())), $this->itemID);
return $this->ctf->getDataList()->byId($this->itemID);
}
}
/**
* Returns the db-fieldname of the currently used has_one-relationship.
*/
function getParentIdName( $parentClass, $childClass ) {
return $this->getParentIdNameRelation( $childClass, $parentClass, 'has_one' );
}
/**
* Manually overwrites the parent-ID relations.
* @see setParentClass()
*
* @param String $str Example: FamilyID (when one Individual has_one Family)
*/
function setParentIdName($str) {
$this->parentIdName = $str;
}
/**
* Returns the db-fieldname of the currently used relationship.
*/
function getParentIdNameRelation($parentClass, $childClass, $relation) {
if($this->parentIdName) return $this->parentIdName;
$relations = singleton($parentClass)->$relation();
$classes = ClassInfo::ancestry($childClass);
foreach($relations as $k => $v) {
if(array_key_exists($v, $classes)) return $k . 'ID';
}
return false;
}
/**
* @return TableListField
*/

View File

@ -59,7 +59,7 @@ class TreeMultiselectField extends TreeDropdownField {
// Otherwise, look data up from the linked relation
} if($this->value != 'unchanged' && is_string($this->value)) {
$items = new DataObjectSet();
$items = new ArrayList();
$ids = explode(',', $this->value);
foreach($ids as $id) {
if(!is_numeric($id)) continue;

View File

@ -84,7 +84,7 @@
var self = this, processDfd = new $.Deferred();
// CSS
if(xhr.getResponseHeader('X-Include-CSS')) {
if(xhr.getResponseHeader && xhr.getResponseHeader('X-Include-CSS')) {
var cssIncludes = xhr.getResponseHeader('X-Include-CSS').split(',');
for(var i=0;i<cssIncludes.length;i++) {
// Syntax: "URL:##:media"
@ -99,7 +99,7 @@
// JavaScript
var newJsIncludes = [];
if(xhr.getResponseHeader('X-Include-JS')) {
if(xhr.getResponseHeader && xhr.getResponseHeader('X-Include-JS')) {
var jsIncludes = xhr.getResponseHeader('X-Include-JS').split(',');
for(var i=0;i<jsIncludes.length;i++) {
if(!$.isItemLoaded(jsIncludes[i])) {

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');

246
model/ArrayList.php Normal file
View File

@ -0,0 +1,246 @@
<?php
/**
* A list object that wraps around an array of objects or arrays.
*
* @package sapphire
* @subpackage model
*/
class ArrayList extends ViewableData implements SS_List {
/**
* @var array
*/
protected $items;
public function __construct(array $items = array()) {
$this->items = $items;
parent::__construct();
}
public function count() {
return count($this->items);
}
public function exists() {
return (bool) count($this);
}
public function getIterator() {
return new ArrayIterator($this->items);
}
public function toArray() {
return $this->items;
}
public function toNestedArray() {
$result = array();
foreach ($this->items as $item) {
if (is_object($item)) {
if (method_exists($item, 'toMap')) {
$result[] = $item->toMap();
} else {
$result[] = (array) $item;
}
} else {
$result[] = $item;
}
}
return $result;
}
public function getRange($offset, $length) {
return new ArrayList(array_slice($this->items, $offset, $length));
}
public function add($item) {
$this->push($item);
}
public function remove($item) {
foreach ($this->items as $key => $value) {
if ($item === $value) unset($this->items[$key]);
}
}
/**
* Replaces an item in this list with another item.
*
* @param array|object $item
* @param array|object $with
*/
public function replace($item, $with) {
foreach ($this->items as $key => $candidate) {
if ($candidate === $item) {
$this->items[$key] = $with;
return;
}
}
}
/**
* Merges with another array or list by pushing all the items in it onto the
* end of this list.
*
* @param array|object $with
*/
public function merge($with) {
foreach ($with as $item) $this->push($item);
}
/**
* Removes items from this list which have a duplicate value for a certain
* field. This is especially useful when combining lists.
*
* @param string $field
*/
public function removeDuplicates($field = 'ID') {
$seen = array();
foreach ($this->items as $key => $item) {
$value = $this->extract($item, $field);
if (array_key_exists($value, $seen)) {
unset($this->items[$key]);
}
$seen[$value] = true;
}
}
/**
* Pushes an item onto the end of this list.
*
* @param array|object $item
*/
public function push($item) {
$this->items[] = $item;
}
/**
* Pops the last element off the end of the list and returns it.
*
* @return array|object
*/
public function pop() {
return array_pop($this->items);
}
/**
* Unshifts an item onto the beginning of the list.
*
* @param array|object $item
*/
public function unshift($item) {
array_unshift($this->items, $item);
}
/**
* Shifts the item off the beginning of the list and returns it.
*
* @return array|object
*/
public function shift() {
return array_shift($this->items);
}
public function first() {
return reset($this->items);
}
public function last() {
return end($this->items);
}
public function map($keyfield, $titlefield) {
$map = array();
foreach ($this->items as $item) {
$map[$this->extract($item, $keyfield)] = $this->extract($item, $titlefield);
}
return $map;
}
public function find($key, $value) {
foreach ($this->items as $item) {
if ($this->extract($item, $key) == $value) return $item;
}
}
public function column($field = 'ID') {
$result = array();
foreach ($this->items as $item) {
$result[] = $this->extract($item, $field);
}
return $result;
}
public function canSortBy($by) {
return true;
}
/**
* Sorts this list by one or more fields. You can either pass in a single
* field name and direction, or a map of field names to sort directions.
*
* @param string|array $by
* @param string $dir
* @see SS_List::sort()
*/
public function sort($by, $dir = 'ASC') {
$sorts = array();
if (!is_array($by)) {
$by = array($by => $dir);
}
foreach ($by as $field => $dir) {
$dir = strtoupper($dir) == 'DESC' ? SORT_DESC : SORT_ASC;
$vals = array();
foreach ($this->items as $item) {
$vals[] = $this->extract($item, $field);
}
$sorts[] = $vals;
$sorts[] = $dir;
}
$sorts[] = &$this->items;
call_user_func_array('array_multisort', $sorts);
}
public function offsetExists($offset) {
return array_key_exists($offset, $this->items);
}
public function offsetGet($offset) {
if ($this->offsetExists($offset)) return $this->items[$offset];
}
public function offsetSet($offset, $value) {
$this->items[$offset] = $value;
}
public function offsetUnset($offset) {
unset($this->items[$offset]);
}
/**
* Extracts a value from an item in the list, where the item is either an
* object or array.
*
* @param array|object $item
* @param string $key
* @return mixed
*/
protected function extract($item, $key) {
if (is_object($item)) {
return $item->$key;
} else {
if (array_key_exists($key, $item)) return $item[$key];
}
}
}

View File

@ -1,307 +1,10 @@
<?php
/**
* This is a special kind of DataObjectSet used to represent the items linked to in a 1-many or many-many
* join. It provides add and remove methods that will update the database.
* @package sapphire
* @subpackage model
* @deprecated 3.0 Use ManyManyList or HasManyList
*/
class ComponentSet extends DataObjectSet {
/**
* Type of relationship (eg '1-1', '1-many').
* @var string
*/
protected $type;
/**
* Object that owns this set.
* @var DataObject
*/
protected $ownerObj;
/**
* Class of object that owns this set.
* @var string
*/
protected $ownerClass;
/**
* Table that holds this relationship.
* @var string
*/
protected $tableName;
/**
* Class of child side of the relationship.
* @var string
*/
protected $childClass;
/**
* Field to join on.
* @var string
*/
protected $joinField;
/**
* Set the ComponentSet specific information.
* @param string $type Type of relationship (eg '1-1', '1-many').
* @param DataObject $ownerObj Object that owns this set.
* @param string $ownerClass Class of object that owns this set.
* @param string $tableName Table that holds this relationship.
* @param string $childClass Class of child side of the relationship.
* @param string $joinField Field to join on.
*/
function setComponentInfo($type, $ownerObj, $ownerClass, $tableName, $childClass, $joinField = null) {
$this->type = $type;
$this->ownerObj = $ownerObj;
$this->ownerClass = $ownerClass ? $ownerClass : $ownerObj->class;
$this->tableName = $tableName;
$this->childClass = $childClass;
$this->joinField = $joinField;
}
/**
* Get the ComponentSet specific information
*
* Returns an array on the format array(
* 'type' => <string>,
* 'ownerObj' => <Object>,
* 'ownerClass' => <string>,
* 'tableName' => <string>,
* 'childClass' => <string>,
* 'joinField' => <string>|null );
*
* @return array
*/
public function getComponentInfo() {
return array(
'type' => $this->type,
'ownerObj' => $this->ownerObj,
'ownerClass' => $this->ownerClass,
'tableName' => $this->tableName,
'childClass' => $this->childClass,
'joinField' => $this->joinField
);
}
/**
* Get an array of all the IDs in this component set, where the keys are the same as the
* values.
* @return array
*/
function getIdList() {
$list = array();
foreach($this->items as $item) {
$list[$item->ID] = $item->ID;
}
return $list;
}
/**
* Add an item to this set.
* @param DataObject|int|string $item Item to add, either as a DataObject or as the ID.
* @param array $extraFields A map of extra fields to add.
*/
function add($item, $extraFields = null) {
if(!isset($item)) {
user_error("ComponentSet::add() Not passed an object or ID", E_USER_ERROR);
}
if(is_object($item)) {
if(!is_a($item, $this->childClass)) {
user_error("ComponentSet::add() Tried to add an '{$item->class}' object, but a '{$this->childClass}' object expected", E_USER_ERROR);
}
} else {
if(!$this->childClass) {
user_error("ComponentSet::add() \$this->childClass not set", E_USER_ERROR);
}
$item = DataObject::get_by_id($this->childClass, $item);
if(!$item) return;
}
// If we've already got a database object, then update the database
if($this->ownerObj->ID && is_numeric($this->ownerObj->ID)) {
$this->loadChildIntoDatabase($item, $extraFields);
}
// In either case, add something to $this->items
$this->items[] = $item;
}
/**
* Method to save many-many join data into the database for the given $item.
* Used by add() and write().
* @param DataObject|string|int The item to save, as either a DataObject or the ID.
* @param array $extraFields Map of extra fields.
*/
protected function loadChildIntoDatabase($item, $extraFields = null) {
if($this->type == '1-to-many') {
$child = DataObject::get_by_id($this->childClass,$item->ID);
if (!$child) $child = $item;
$joinField = $this->joinField;
$child->$joinField = $this->ownerObj->ID;
$child->write();
} else {
$parentField = $this->ownerClass . 'ID';
$childField = ($this->childClass == $this->ownerClass) ? "ChildID" : ($this->childClass . 'ID');
DB::query( "DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" = {$item->ID}" );
$extraKeys = $extraValues = '';
if($extraFields) foreach($extraFields as $k => $v) {
$extraKeys .= ", \"$k\"";
$extraValues .= ", '" . Convert::raw2sql($v) . "'";
}
DB::query("INSERT INTO \"$this->tableName\" (\"$parentField\",\"$childField\" $extraKeys) VALUES ({$this->ownerObj->ID}, {$item->ID} $extraValues)");
}
}
/**
* Add a number of items to the component set.
* @param array $items Items to add, as either DataObjects or IDs.
*/
function addMany($items) {
foreach($items as $item) {
$this->add($item);
}
}
/**
* Sets the ComponentSet to be the given ID list.
* Records will be added and deleted as appropriate.
* @param array $idList List of IDs.
*/
function setByIDList($idList) {
$has = array();
// Index current data
if($this->items) foreach($this->items as $item) {
$has[$item->ID] = true;
}
// Keep track of items to delete
$itemsToDelete = $has;
// add items in the list
// $id is the database ID of the record
if($idList) foreach($idList as $id) {
$itemsToDelete[$id] = false;
if($id && !isset($has[$id])) $this->add($id);
}
// delete items not in the list
$removeList = array();
foreach($itemsToDelete as $id => $actuallyDelete) {
if($actuallyDelete) $removeList[] = $id;
}
$this->removeMany($removeList);
}
/**
* Remove an item from this set.
*
* @param DataObject|string|int $item Item to remove, either as a DataObject or as the ID.
*/
function remove($item) {
if(is_object($item)) {
if(!is_a($item, $this->childClass)) {
user_error("ComponentSet::remove() Tried to remove an '{$item->class}' object, but a '{$this->childClass}' object expected", E_USER_ERROR);
}
} else {
$item = DataObject::get_by_id($this->childClass, $item);
}
// Manipulate the database, if it's in there
if($this->ownerObj->ID && is_numeric($this->ownerObj->ID)) {
if($this->type == '1-to-many') {
$child = DataObject::get_by_id($this->childClass,$item->ID);
$joinField = $this->joinField;
if($child->$joinField == $this->ownerObj->ID) {
$child->$joinField = null;
$child->write();
}
} else {
$parentField = $this->ownerClass . 'ID';
$childField = ($this->childClass == $this->ownerClass) ? "ChildID" : ($this->childClass . 'ID');
DB::query("DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" = {$item->ID}");
}
}
// Manipulate the in-memory array of items
if($this->items) foreach($this->items as $i => $candidateItem) {
if($candidateItem->ID == $item->ID) {
unset($this->items[$i]);
break;
}
}
}
/**
* Remove many items from this set.
* @param array $itemList The items to remove, as a numerical array with IDs or as a DataObjectSet
*/
function removeMany($itemList) {
if(!count($itemList)) return false;
if($this->type == '1-to-many') {
foreach($itemList as $item) $this->remove($item);
} else {
$itemCSV = implode(", ", $itemList);
$parentField = $this->ownerClass . 'ID';
$childField = ($this->childClass == $this->ownerClass) ? "ChildID" : ($this->childClass . 'ID');
DB::query("DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" IN ($itemCSV)");
}
}
/**
* Remove all items in this set.
*/
function removeAll() {
if(!empty($this->tableName)) {
$parentField = $this->ownerClass . 'ID';
DB::query("DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID}");
} else {
foreach($this->items as $item) {
$this->remove($item);
}
}
}
/**
* Write this set to the database.
* Called by DataObject::write().
* @param boolean $firstWrite This should be set to true if it the first time the set is being written.
*/
function write($firstWrite = false) {
if($firstWrite) {
foreach($this->items as $item) {
$this->loadChildIntoDatabase($item);
}
}
}
/**
* Returns information about this set in HTML format for debugging.
*
* @return string
*/
function debug() {
$size = count($this->items);
$output = <<<OUT
<h3>ComponentSet</h3>
<ul>
<li>Type: {$this->type}</li>
<li>Size: $size</li>
</ul>
OUT;
return $output;
user_error("ComponentSet is deprecated; use HasManyList or ManyManyList", E_USER_WARNING);
}
}
?>

View File

@ -96,7 +96,7 @@ class DataDifferencer extends ViewableData {
* - To: The newer version of the field
*/
function ChangedFields() {
$changedFields = new DataObjectSet();
$changedFields = new ArrayList();
if($this->fromRecord) {
$base = $this->fromRecord;

View File

@ -166,11 +166,11 @@ abstract class DataExtension extends Extension {
* should just be used to add or modify tabs, or fields which
* are specific to the CMS-context.
*
* Caution: Use {@link FieldSet->addFieldToTab()} to add fields.
* Caution: Use {@link FieldList->addFieldToTab()} to add fields.
*
* @param FieldSet $fields FieldSet with a contained TabSet
* @param FieldList $fields FieldSet with a contained TabSet
*/
function updateCMSFields(FieldSet &$fields) {
function updateCMSFields(FieldList $fields) {
}
/**
@ -179,18 +179,18 @@ abstract class DataExtension extends Extension {
*
* Caution: Use {@link FieldSet->push()} to add fields.
*
* @param FieldSet $fields FieldSet without TabSet nesting
* @param FieldList $fields FieldSet without TabSet nesting
*/
function updateFrontEndFields(FieldSet &$fields) {
function updateFrontEndFields(FieldList $fields) {
}
/**
* This is used to provide modifications to the form actions
* used in the CMS. {@link DataObject->getCMSActions()}.
*
* @param FieldSet $actions FieldSet
* @param FieldList $actions FieldSet
*/
function updateCMSActions(FieldSet &$actions) {
function updateCMSActions(FieldList $actions) {
}
/**

472
model/DataList.php Normal file
View File

@ -0,0 +1,472 @@
<?php
/**
* Implements a "lazy loading" DataObjectSet.
* Uses {@link DataQuery} to do the actual query generation.
*/
class DataList extends ViewableData implements SS_List {
/**
* The DataObject class name that this data list is querying
*/
protected $dataClass;
/**
* The {@link DataQuery} object responsible for getting this DataObjectSet's records
*/
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
* new DataList("SiteTree")->sort("Title") is not.
*/
static function create($dataClass) {
return new DataList($dataClass);
}
/**
* Create a new DataList.
* No querying is done on construction, but the initial query schema is set up.
* @param $dataClass The DataObject class to query.
*/
public function __construct($dataClass) {
$this->dataClass = $dataClass;
$this->dataQuery = new DataQuery($this->dataClass);
parent::__construct();
}
public function setModel(DataModel $model) {
$this->model = $model;
}
public function dataClass() {
return $this->dataClass;
}
/**
* Clone this object
*/
function __clone() {
$this->dataQuery = clone $this->dataQuery;
}
/**
* Return the internal {@link DataQuery} object for direct manipulation
*/
public function dataQuery() {
return $this->dataQuery;
}
/**
* Returns the SQL query that will be used to get this DataList's records. Good for debugging. :-)
*/
public function sql() {
return $this->dataQuery->query()->sql();
}
/**
* Add a WHERE clause to the query.
*
* @param string $filter
*/
public function where($filter) {
$this->dataQuery->where($filter);
return $this;
}
/**
* Set the sort order of this data list
*/
public function sort($sort, $direction = "ASC") {
if($direction && strtoupper($direction) != 'ASC') $sort = "$sort $direction";
$this->dataQuery->sort($sort);
return $this;
}
/**
* Returns true if this DataList can be sorted by the given field.
*/
public function canSortBy($field) {
return $this->dataQuery()->query()->canSortBy($field);
}
/**
* Add an join clause to this data list's query.
*/
public function join($join) {
$this->dataQuery->join($join);
return $this;
}
/**
* Restrict the records returned in this query by a limit clause
*/
public function limit($limit) {
$this->dataQuery->limit($limit);
return $this;
}
/**
* Add an inner join clause to this data list's query.
*/
public function innerJoin($table, $onClause, $alias = null) {
$this->dataQuery->innerJoin($table, $onClause, $alias);
return $this;
}
/**
* Add an left join clause to this data list's query.
*/
public function leftJoin($table, $onClause, $alias = null) {
$this->dataQuery->leftJoin($table, $onClause, $alias);
return $this;
}
/**
* Return an array of the actual items that this DataList contains at this stage.
* This is when the query is actually executed.
*
* @return array
*/
public function toArray() {
$query = $this->dataQuery->query();
$rows = $query->execute();
$results = array();
foreach($rows as $row) {
$results[] = $this->createDataObject($row);
}
return $results;
}
public function toNestedArray() {
$result = array();
foreach ($this as $item) {
$result[] = $item->toMap();
}
return $result;
}
public function map($keyfield = 'ID', $titlefield = 'Title') {
$map = array();
foreach ($this as $item) {
$map[$item->$keyfield] = $item->$titlefield;
}
return $map;
}
/**
* Create a data object from the given SQL row
*/
protected function createDataObject($row) {
$defaultClass = $this->dataClass;
// Failover from RecordClassName to ClassName
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'])) $item = new $row['RecordClassName']($row, false, $this->model);
else $item = new $defaultClass($row, false, $this->model);
return $item;
}
/**
* Returns an Iterator for this DataObjectSet.
* This function allows you to use DataObjectSets in foreach loops
* @return DataObjectSet_Iterator
*/
public function getIterator() {
return new ArrayIterator($this->toArray());
}
/**
* Return the number of items in this DataList
*/
function count() {
return $this->dataQuery->count();
}
/**
* Return the maximum value of the given field in this DataList
*/
function Max($field) {
return $this->dataQuery->max($field);
}
/**
* Return the minimum value of the given field in this DataList
*/
function Min($field) {
return $this->dataQuery->min($field);
}
/**
* Return the average value of the given field in this DataList
*/
function Avg($field) {
return $this->dataQuery->avg($field);
}
/**
* Return the sum of the values of the given field in this DataList
*/
function Sum($field) {
return $this->dataQuery->sum($field);
}
/**
* Returns the first item in this DataList
*/
function First() {
foreach($this->dataQuery->firstRow()->execute() as $row) {
return $this->createDataObject($row);
}
}
/**
* Returns the last item in this DataList
*/
function Last() {
foreach($this->dataQuery->lastRow()->execute() as $row) {
return $this->createDataObject($row);
}
}
/**
* Returns true if this DataList has items
*/
function exists() {
return $this->count() > 0;
}
/**
* Get a sub-range of this dataobjectset as an array
*/
public function getRange($offset, $length) {
return $this->limit(array('start' => $offset, 'limit' => $length));
}
/**
* Find an element of this DataList where the given key = value
*/
public function find($key, $value) {
return $this->where("\"$key\" = '" . Convert::raw2sql($value) . "'")->First();
}
/**
* Filter this list to only contain the given IDs
*/
public function byIDs(array $ids) {
$baseClass = ClassInfo::baseDataClass($this->dataClass);
$this->where("\"$baseClass\".\"ID\" IN (" . implode(',', $ids) .")");
return $this;
}
/**
* Return the item of the given ID
*/
public function byID($id) {
$baseClass = ClassInfo::baseDataClass($this->dataClass);
return $this->where("\"$baseClass\".\"ID\" = " . (int)$id)->First();
}
/**
* Return a single column from this DataList.
* @param $colNum The DataObject field to return.
*/
function column($colName = "ID") {
return $this->dataQuery->column($colName);
}
// Member altering methods
/**
* Sets the ComponentSet to be the given ID list.
* Records will be added and deleted as appropriate.
* @param array $idList List of IDs.
*/
function setByIDList($idList) {
$has = array();
// Index current data
foreach($this->column() as $id) {
$has[$id] = true;
}
// Keep track of items to delete
$itemsToDelete = $has;
// add items in the list
// $id is the database ID of the record
if($idList) foreach($idList as $id) {
unset($itemsToDelete[$id]);
if($id && !isset($has[$id])) $this->add($id);
}
// Remove any items that haven't been mentioned
$this->removeMany(array_keys($itemsToDelete));
}
/**
* Returns an array with both the keys and values set to the IDs of the records in this list.
*/
function getIDList() {
$ids = $this->column("ID");
return $ids ? array_combine($ids, $ids) : array();
}
/**
* Returns a HasManyList or ManyMany list representing the querying of a relation across all
* objects in this data list. For it to work, the relation must be defined on the data class
* that you used to create this DataList.
*
* Example: Get members from all Groups:
*
* DataObject::get("Group")->relation("Members")
*/
function relation($relationName) {
$ids = $this->column('ID');
return singleton($this->dataClass)->$relationName()->forForeignID($ids);
}
/**
* Add a number of items to the component set.
* @param array $items Items to add, as either DataObjects or IDs.
*/
function addMany($items) {
foreach($items as $item) {
$this->add($item);
}
}
/**
* Remove the items from this list with the given IDs
*/
function removeMany($idList) {
foreach($idList as $id) {
$this->removeByID($id);
}
}
/**
* Remove every element in this DataList matching the given $filter.
*/
function removeByFilter($filter) {
foreach($this->where($filter) as $item) {
$this->remove($item);
}
}
/**
* Remove every element in this DataList.
*/
function removeAll() {
foreach($this as $item) {
$this->remove($item);
}
}
// These methods are overloaded by HasManyList and ManyMany list to perform
// more sophisticated list manipulation
function add($item) {
// 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
// an "ActiveItems" DataList by chaning the status to inactive.
// By default, we remove an item from a DataList by deleting it.
if($item instanceof $this->dataClass) $item->delete();
}
/**
* Remove an item from this DataList by ID
*/
function removeByID($itemID) {
$item = $this->byID($itemID);
if($item) return $item->delete();
}
// Methods that won't function on DataLists
function push($item) {
user_error("Can't call DataList::push() because its data comes from a specific query.", E_USER_ERROR);
}
function insertFirst($item) {
user_error("Can't call DataList::insertFirst() because its data comes from a specific query.", E_USER_ERROR);
}
function shift() {
user_error("Can't call DataList::shift() because its data comes from a specific query.", E_USER_ERROR);
}
function replace() {
user_error("Can't call DataList::replace() because its data comes from a specific query.", E_USER_ERROR);
}
function merge() {
user_error("Can't call DataList::merge() because its data comes from a specific query.", E_USER_ERROR);
}
function removeDuplicates() {
user_error("Can't call DataList::removeDuplicates() because its data comes from a specific query.", E_USER_ERROR);
}
/**
* Necessary for interface ArrayAccess. Returns whether an item with $key exists
* @param mixed $key
* @return bool
*/
public function offsetExists($key) {
return ($this->getRange($key, 1)->First() != null);
}
/**
* Necessary for interface ArrayAccess. Returns item stored in array with index $key
* @param mixed $key
* @return DataObject
*/
public function offsetGet($key) {
return $this->getRange($key, 1)->First();
}
/**
* Necessary for interface ArrayAccess. Set an item with the key in $key
* @param mixed $key
* @param mixed $value
*/
public function offsetSet($key, $value) {
throw new Exception("Can't alter items in a DataList using array-access");
}
/**
* Necessary for interface ArrayAccess. Unset an item with the key in $key
* @param mixed $key
*/
public function offsetUnset($key) {
throw new Exception("Can't alter items in a DataList using array-access");
}
}
?>

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) {
@ -444,7 +458,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
private function duplicateRelations($sourceObject, $destinationObject, $name) {
$relations = $sourceObject->$name();
if ($relations) {
if ($relations instanceOf ComponentSet) { //many-to-something relation
if ($relations instanceOf RelationList) { //many-to-something relation
if ($relations->Count() > 0) { //with more than one thing it is related to
foreach($relations as $relation) {
$destinationObject->$name()->add($relation);
@ -497,7 +511,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
'ClassName' => $originalClass,
'RecordClassName' => $originalClass,
)
));
), false, $this->model);
if($newClassName != $originalClass) {
$newInstance->setClassName($newClassName);
@ -1136,7 +1150,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->extend('onAfterSkippedWrite');
}
// Write ComponentSets as necessary
// Write relations as necessary
if($writeComponents) {
$this->writeComponents(true);
}
@ -1187,15 +1201,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Deleting a record without an ID shouldn't do anything
if(!$this->ID) throw new Exception("DataObject::delete() called on a DataObject without an ID");
foreach($this->getClassAncestry() as $ancestor) {
if(self::has_own_table($ancestor)) {
$sql = new SQLQuery();
$sql->delete = true;
$sql->from[$ancestor] = "\"$ancestor\"";
$sql->where[] = "\"ID\" = $this->ID";
$this->extend('augmentSQL', $sql);
$sql->execute();
}
// TODO: This is quite ugly. To improve:
// - 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, $this->model)->where("ID = $this->ID")->dataQuery()->query();
foreach($srcQuery->queriedTables() as $table) {
$query = new SQLQuery("*", array('"'.$table.'"'));
$query->where("\"ID\" = $this->ID");
$query->delete = true;
$query->execute();
}
// Remove this item out of any caches
$this->flushCache();
@ -1263,11 +1278,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');
@ -1278,7 +1293,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 {
@ -1296,10 +1311,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
protected $componentCache;
/**
* Returns a one-to-many component, as a ComponentSet.
* The return value will be cached on this object instance,
* but only when no related objects are found (to avoid unnecessary empty checks in the database).
* If related objects exist, no caching is applied.
* Returns a one-to-many relation as a HasManyList
*
* @param string $componentName Name of the component
* @param string $filter A filter to be inserted into the WHERE clause
@ -1307,36 +1319,22 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned.
* @param string|array $limit A limit expression to be inserted into the LIMIT clause
*
* @return ComponentSet The components of the one-to-many relationship.
* @return HasManyList The components of the one-to-many relationship.
*/
public function getComponents($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
$result = null;
$sum = md5("{$filter}_{$sort}_{$join}_{$limit}");
if(isset($this->componentCache[$componentName . '_' . $sum]) && false != $this->componentCache[$componentName . '_' . $sum]) {
return $this->componentCache[$componentName . '_' . $sum];
}
if(!$componentClass = $this->has_many($componentName)) {
user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName' on class '$this->class'", E_USER_ERROR);
}
$joinField = $this->getRemoteJoinField($componentName, 'has_many');
if($this->isInDB()) { //Check to see whether we should query the db
$query = $this->getComponentsQuery($componentName, $filter, $sort, $join, $limit);
$result = $this->buildDataObjectSet($query->execute(), 'ComponentSet', $query, $componentClass);
if($result) $result->parseQueryLimit($query);
}
$result = new HasManyList($componentClass, $joinField);
if($this->model) $result->setModel($this->model);
if($this->ID) $result->setForeignID($this->ID);
if(!$result) {
// If this record isn't in the database, then we want to hold onto this specific ComponentSet,
// because it's the only copy of the data that we have.
$result = new ComponentSet();
$this->setComponent($componentName . '_' . $sum, $result);
}
$result->setComponentInfo("1-to-many", $this, null, null, $componentClass, $joinField);
$result = $result->where($filter)->limit($limit)->sort($sort)->join($join);
return $result;
}
@ -1344,11 +1342,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/**
* 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
@ -1408,149 +1401,31 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* and {@link getManyManyComponents()}.
*
* @param string $componentName Name of the component
* @param DataObject|ComponentSet $componentValue Value of the component
* @param DataObject|HasManyList|ManyManyList $componentValue Value of the component
*/
public function setComponent($componentName, $componentValue) {
$this->componentCache[$componentName] = $componentValue;
}
/**
* Returns a many-to-many component, as a ComponentSet.
* The return value will be cached on this object instance,
* but only when no related objects are found (to avoid unnecessary empty checks in the database).
* If related objects exist, no caching is applied.
*
* Returns a many-to-many component, as a ManyManyList.
* @param string $componentName Name of the many-many component
* @return ComponentSet The set of components
* @return ManyManyList The set of components
*
* @todo Implement query-params
*/
public function getManyManyComponents($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
$sum = md5("{$filter}_{$sort}_{$join}_{$limit}");
if(isset($this->componentCache[$componentName . '_' . $sum]) && false != $this->componentCache[$componentName . '_' . $sum]) {
return $this->componentCache[$componentName . '_' . $sum];
}
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);
$result = new ManyManyList($componentClass, $table, $componentField, $parentField,
$this->many_many_extraFields($componentName));
if($this->model) $result->setModel($this->model);
if($this->ID && is_numeric($this->ID)) {
// If this is called on a singleton, then we return an 'orphaned relation' that can have the
// foreignID set elsewhere.
if($this->ID) $result->setForeignID($this->ID);
if($componentClass) {
$query = $this->getManyManyComponentsQuery($componentName, $filter, $sort, $join, $limit);
$records = $query->execute();
$result = $this->buildDataObjectSet($records, "ComponentSet", $query, $componentBaseClass);
if($result) $result->parseQueryLimit($query); // for pagination support
if(!$result) {
$result = new ComponentSet();
}
}
} else {
$result = new ComponentSet();
}
$result->setComponentInfo("many-to-many", $this, $parentClass, $table, $componentClass);
// If this record isn't in the database, then we want to hold onto this specific ComponentSet,
// because it's the only copy of the data that we have.
if(!$this->isInDB()) {
$this->setComponent($componentName . '_' . $sum, $result);
}
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
);
foreach((array)$this->many_many_extraFields($componentName) as $extraField => $extraFieldType) {
$query->select[] = "\"$table\".\"$extraField\"";
$query->groupby[] = "\"$table\".\"$extraField\"";
}
if($filter) $query->where[] = $filter;
if($join) $query->from[] = $join;
return $query;
}
/**
* 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);
$baseComponentClass = ClassInfo::baseDataClass($componentClass);
if($baseTable == $parentClass) {
return "LEFT JOIN \"$table\" ON (\"$table\".\"$parentField\" = \"$parentClass\".\"ID\" AND \"$table\".\"$componentField\" = '{$this->ID}')";
} else {
return "LEFT JOIN \"$table\" ON (\"$table\".\"$componentField\" = \"$baseComponentClass\".\"ID\" AND \"$table\".\"$parentField\" = '{$this->ID}')";
}
}
function getManyManyFilter($componentName, $baseTable) {
list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
return "\"$table\".\"$parentField\" = '{$this->ID}'";
}
/**
* Return an aggregate object. An aggregate object returns the result of running some SQL aggregate function on a field of
* this dataobject type.
*
* It can be called with no arguments, in which case it returns an object that calculates aggregates on this object's type,
* or with an argument (possibly statically), in which case it returns an object for that type
*/
function Aggregate($type = null, $filter = '') {
return new Aggregate($type ? $type : $this->class, $filter);
}
/**
* Return an relationship aggregate object. A relationship aggregate does the same thing as an aggregate object, but operates
* on a has_many rather than directly on the type specified
*/
function RelationshipAggregate($object = null, $relationship = '', $filter = '') {
if (is_string($object)) { $filter = $relationship; $relationship = $object; $object = $this; }
return new Aggregate_Relationship($object ? $object : $this->owner, $relationship, $filter);
return $result->where($filter)->sort($sort)->limit($limit);
}
/**
@ -1897,7 +1772,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
),
(array)$_params
);
$fields = new FieldSet();
$fields = new FieldList();
foreach($this->searchableFields() as $fieldName => $spec) {
if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue;
@ -2014,7 +1889,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return an Empty FieldSet(); need to be overload by solid subclass
*/
public function getCMSActions() {
$actions = new FieldSet();
$actions = new FieldList();
$this->extend('updateCMSActions', $actions);
return $actions;
}
@ -2549,7 +2424,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$object = $component->dbObject($fieldName);
if (!($object instanceof DBField) && !($object instanceof ComponentSet)) {
if (!($object instanceof DBField) && !($object instanceof DataList)) {
// Todo: come up with a broader range of exception objects to describe differnet kinds of errors programatically
throw new Exception("Unable to traverse to related object field [$fieldPath] on [$this->class]");
}
@ -2580,162 +2455,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
/**
* Build a {@link SQLQuery} object to perform the given query.
*
* @param string $filter A filter to be inserted into the WHERE 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.
*
* @return SQLQuery Query built.
* @deprecated 2.5 Use DataObject::get() instead, with the new data mapper there's no reason not to.
*/
public function buildSQL($filter = "", $sort = "", $limit = "", $join = "", $restrictClasses = true, $having = "") {
// Cache the big hairy part of buildSQL
if(!isset(self::$cache_buildSQL_query[$this->class])) {
// Get the tables to join to
$tableClasses = ClassInfo::dataClassesFor($this->class);
if(!$tableClasses) {
if (!DB::getConn()) {
throw new Exception('DataObjects have been requested before'
. ' a DB connection has been made. Please ensure you'
. ' are not querying the database in _config.php.');
} else {
user_error("DataObject::buildSQL: Can't find data classes (classes linked to tables) for $this->class. Please ensure you run dev/build after creating a new DataObject.", E_USER_ERROR);
}
}
user_error("DataObject::buildSQL() deprecated; just use DataObject::get() with the new data mapper", E_USER_NOTICE);
return $this->extendedSQL($filter, $sort, $limit, $join, $having);
$baseClass = array_shift($tableClasses);
// $collidingFields will keep a list fields that appear in mulitple places in the class
// heirarchy for this table. They will be dealt with more explicitly in the SQL query
// to ensure that junk data from other tables doesn't corrupt data objects
$collidingFields = array();
// Build our intial query
$query = new SQLQuery(array());
$query->from("\"$baseClass\"");
// Add SQL for multi-value fields on the base table
$databaseFields = self::database_fields($baseClass);
if($databaseFields) foreach($databaseFields as $k => $v) {
if(!in_array($k, array('ClassName', 'LastEdited', 'Created')) && ClassInfo::classImplements($v, 'CompositeDBField')) {
$this->dbObject($k)->addToQuery($query);
} else {
$query->select[$k] = "\"$baseClass\".\"$k\"";
}
}
// Join all the tables
if($tableClasses && self::$subclass_access) {
foreach($tableClasses as $tableClass) {
$query->from[$tableClass] = "LEFT JOIN \"$tableClass\" ON \"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"";
// Add SQL for multi-value fields
$databaseFields = self::database_fields($tableClass);
$compositeFields = self::composite_fields($tableClass, false);
if($databaseFields) foreach($databaseFields as $k => $v) {
if(!isset($compositeFields[$k])) {
// Update $collidingFields if necessary
if(isset($query->select[$k])) {
if(!isset($collidingFields[$k])) $collidingFields[$k] = array($query->select[$k]);
$collidingFields[$k][] = "\"$tableClass\".\"$k\"";
} else {
$query->select[$k] = "\"$tableClass\".\"$k\"";
}
}
}
if($compositeFields) foreach($compositeFields as $k => $v) {
$dbO = $this->dbObject($k);
if($dbO) $dbO->addToQuery($query);
}
}
}
// Resolve colliding fields
if($collidingFields) {
foreach($collidingFields as $k => $collisions) {
$caseClauses = array();
foreach($collisions as $collision) {
if(preg_match('/^"([^"]+)"/', $collision, $matches)) {
$collisionBase = $matches[1];
$collisionClasses = ClassInfo::subclassesFor($collisionBase);
$caseClauses[] = "WHEN \"$baseClass\".\"ClassName\" IN ('"
. implode("', '", $collisionClasses) . "') THEN $collision";
} else {
user_error("Bad collision item '$collision'", E_USER_WARNING);
}
}
$query->select[$k] = "CASE " . implode( " ", $caseClauses) . " ELSE NULL END"
. " AS \"$k\"";
}
}
$query->select[] = "\"$baseClass\".\"ID\"";
$query->select[] = "CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\" ELSE '$baseClass' END AS \"RecordClassName\"";
// Get the ClassName values to filter to
$classNames = ClassInfo::subclassesFor($this->class);
if(!$classNames) {
user_error("DataObject::get() Can't find data sub-classes for '$callerClass'");
}
// If querying the base class, don't bother filtering on class name
if($restrictClasses && $this->class != $baseClass) {
// Get the ClassName values to filter to
$classNames = ClassInfo::subclassesFor($this->class);
if(!$classNames) {
user_error("DataObject::get() Can't find data sub-classes for '$callerClass'");
}
$query->where[] = "\"$baseClass\".\"ClassName\" IN ('" . implode("','", $classNames) . "')";
}
self::$cache_buildSQL_query[$this->class] = clone $query;
} else {
$query = clone self::$cache_buildSQL_query[$this->class];
}
// Find a default sort
if(!$sort) {
$sort = $this->stat('default_sort');
}
// Add quoting to sort expression if it's a simple column name
if(preg_match('/^[A-Z][A-Z0-9_]*$/i', $sort)) $sort = "\"$sort\"";
$query->where($filter);
$query->orderby($sort);
$query->limit($limit);
if($having) {
$query->having[] = $having;
}
if($join) {
$query->from[] = $join;
// In order to group by unique columns we have to group by everything listed in the select
foreach($query->select as $field) {
// Skip the _SortColumns; these are only going to be aggregate functions
if(preg_match('/AS\s+\"?_SortColumn/', $field, $matches)) {
// Identify columns with aliases, and ignore the alias. Making use of the alias in
// group by was causing problems when those queries were subsequently passed into
// SQLQuery::unlimitedRowCount.
} else if(preg_match('/^(.*)\s+AS\s+(\"[^"]+\")\s*$/', $field, $matches)) {
$query->groupby[] = $matches[1];
// Otherwise just use the field as is
} else {
$query->groupby[] = $field;
}
}
}
return $query;
}
/**
@ -2744,21 +2469,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
private static $cache_buildSQL_query;
/**
* Like {@link buildSQL}, but applies the extension modifications.
*
* @uses DataExtension->augmentSQL()
*
* @param string $filter A filter to be inserted into the WHERE 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.
* @return SQLQuery Query built
* @deprecated 2.5 Use DataObject::get() instead, with the new data mapper there's no reason not to.
*/
public function extendedSQL($filter = "", $sort = "", $limit = "", $join = "", $having = ""){
$query = $this->buildSQL($filter, $sort, $limit, $join, true, $having);
$this->extend('augmentSQL', $query);
return $query;
public function extendedSQL($filter = "", $sort = "", $limit = "", $join = ""){
$dataList = DataObject::get($this->class, $filter, $sort, $join, $limit);
return $dataList->dataQuery()->query();
}
/**
@ -2774,14 +2489,43 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*
* @return mixed The objects matching the filter, in the class specified by $containerClass
*/
public static function get($callerClass, $filter = "", $sort = "", $join = "", $limit = "", $containerClass = "DataObjectSet") {
return singleton($callerClass)->instance_get($filter, $sort, $join, $limit, $containerClass);
public static function get($callerClass, $filter = "", $sort = "", $join = "", $limit = "", $containerClass = "DataList") {
// Deprecated 2.5?
// 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;
}
/**
* @deprecated
*/
public function Aggregate($class = null) {
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;
}
/**
* @deprecated
*/
public function RelationshipAggregate($relationship) {
return $this->$relationship();
}
/**
* The internal function that actually performs the querying for get().
* DataObject::get("Table","filter") is the same as singleton("Table")->instance_get("filter")
*
* @deprecated 2.5 Use DataObject::get()
*
* @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 $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned.
@ -2791,29 +2535,24 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return mixed The objects matching the filter, in the class specified by $containerClass
*/
public function instance_get($filter = "", $sort = "", $join = "", $limit="", $containerClass = "DataObjectSet") {
if(!DB::isActive()) {
user_error("DataObjects have been requested before the database is ready. Please ensure your database connection details are correct, your database has been built, and that you are not trying to query the database in _config.php.", E_USER_ERROR);
}
user_error("instance_get deprecated", E_USER_NOTICE);
return self::get($this->class, $filter, $sort, $join, $limit, $containerClass);
$query = $this->extendedSQL($filter, $sort, $limit, $join);
$records = $query->execute();
$ret = $this->buildDataObjectSet($records, $containerClass, $query, $this->class);
if($ret) $ret->parseQueryLimit($query);
return $ret;
}
/**
* Take a database {@link SS_Query} and instanciate an object for each record.
*
* @deprecated 2.5 Use DataObject::get(), you don't need to side-step it any more
*
* @param SS_Query|array $records The database records, a {@link SS_Query} object or an array of maps.
* @param string $containerClass The class to place all of the objects into.
*
* @return mixed The new objects in an object of type $containerClass
*/
function buildDataObjectSet($records, $containerClass = "DataObjectSet", $query = null, $baseClass = null) {
user_error('buildDataObjectSet is deprecated; use DataList to do your querying', E_USER_NOTICE);
foreach($records as $record) {
if(empty($record['RecordClassName'])) {
$record['RecordClassName'] = $record['ClassName'];
@ -2869,7 +2608,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
] = false;
}
if(!$cache || !isset(DataObject::$cache_get_one[$callerClass][$cacheKey])) {
$item = $SNG->instance_get_one($filter, $orderby);
$dl = DataList::create($callerClass)->where($filter)->sort($orderby);
$dl->setModel(DataModel::inst());
$item = $dl->First();
if($cache) {
DataObject::$cache_get_one[$callerClass][$cacheKey] = $item;
if(!DataObject::$cache_get_one[$callerClass][$cacheKey]) {
@ -2926,6 +2668,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/**
* Does the hard work for get_one()
*
* @deprecated 2.5 Use DataObject::get_one() instead
*
* @uses DataExtension->augmentSQL()
*
* @param string $filter A filter to be inserted into the WHERE clause
@ -2933,35 +2677,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return DataObject The first item matching the query
*/
public function instance_get_one($filter, $orderby = null) {
if(!DB::isActive()) {
user_error("DataObjects have been requested before the database is ready. Please ensure your database connection details are correct, your database has been built, and that you are not trying to query the database in _config.php.", E_USER_ERROR);
}
$query = $this->buildSQL($filter);
$query->limit = "1";
if($orderby) {
$query->orderby = $orderby;
}
$this->extend('augmentSQL', $query);
$records = $query->execute();
$records->rewind();
$record = $records->current();
if($record) {
// Mid-upgrade, the database can have invalid RecordClassName values that need to be guarded against.
if(class_exists($record['RecordClassName'])) {
$record = new $record['RecordClassName']($record);
} else {
$record = new $this->class($record);
}
// Rather than restrict classes at the SQL-query level, we now check once the object has been instantiated
// This lets us check up on weird errors where the class has been incorrectly set, and give warnings to our
// developers
return $record;
}
user_error("DataObjct::instance_get_one is deprecated", E_USER_NOTICE);
return DataObject::get_one($this->class, $filter, true, $orderby);
}
/**
@ -3091,7 +2808,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");
@ -3337,29 +3054,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return is_numeric( $this->ID ) && $this->ID > 0;
}
/**
* Sets a 'context object' that can be used to provide hints about how to process a particular get / get_one request.
* In particular, DataExtensions can use this to amend queries more effectively.
* Care must be taken to unset the context object after you're done with it, otherwise you will have a stale context,
* which could cause horrible bugs.
*/
public static function set_context_obj($obj) {
if($obj && self::$context_obj) user_error("Dataobject::set_context_obj passed " . $obj->class . "." . $obj->ID . " when there is already a context: " . self::$context_obj->class . '.' . self::$context_obj->ID, E_USER_WARNING);
self::$context_obj = $obj;
}
/**
* Retrieve the current context object.
*/
public static function context_obj() {
return self::$context_obj;
}
/**
* @ignore
*/
protected static $context_obj = null;
/*
* @ignore
*/

View File

@ -1,1180 +1,40 @@
<?php
/**
* This class represents a set of {@link ViewableData} subclasses (mostly {@link DataObject} or {@link ArrayData}).
* It is used by the ORM-layer of Silverstripe to return query-results from {@link SQLQuery}.
* @deprecated Please use {@link DataList} or {@link ArrayList} instead.
* @package sapphire
* @subpackage model
*/
class DataObjectSet extends ViewableData implements IteratorAggregate, Countable, ArrayAccess {
/**
* The DataObjects in this set.
* @var array
*/
protected $items = array();
class DataObjectSet extends ArrayList {
protected $odd = 0;
public function __construct($items = array()) {
user_error(
'DataObjectSet is deprecated, please use DataList or ArrayList instead.',
E_USER_NOTICE
);
/**
* True if the current DataObject is the first in this set.
* @var boolean
*/
protected $first = true;
if ($items) {
if (!is_array($items) || func_num_args() > 1) {
$items = func_get_args();
}
/**
* True if the current DataObject is the last in this set.
* @var boolean
*/
protected $last = false;
foreach ($items as $i => $item) {
if ($item instanceof ViewableData) {
continue;
}
/**
* The current DataObject in this set.
* @var DataObject
*/
protected $current = null;
/**
* The number object the current page starts at.
* @var int
*/
protected $pageStart;
/**
* The number of objects per page.
* @var int
*/
protected $pageLength;
/**
* Total number of DataObjects in this set.
* @var int
*/
protected $totalSize;
/**
* The pagination GET variable that controls the start of this set.
* @var string
*/
protected $paginationGetVar = "start";
/**
* Create a new DataObjectSet. If you pass one or more arguments, it will try to convert them into {@link ArrayData} objects.
* @todo Does NOT automatically convert objects with complex datatypes (e.g. converting arrays within an objects to its own DataObjectSet)
*
* @param ViewableData|array|mixed $items Parameters to use in this set, either as an associative array, object with simple properties, or as multiple parameters.
*/
public function __construct($items = null) {
if($items) {
// if the first parameter is not an array, or we have more than one parameter, collate all parameters to an array
// otherwise use the passed array
$itemsArr = (!is_array($items) || count(func_get_args()) > 1) ? func_get_args() : $items;
// We now have support for using the key of a data object set
foreach($itemsArr as $i => $item) {
if(is_subclass_of($item, 'ViewableData')) {
$this->items[$i] = $item;
} elseif(is_object($item) || ArrayLib::is_associative($item)) {
$this->items[$i] = new ArrayData($item);
if (is_object($item) || ArrayLib::is_associative($item)) {
$items[$i] = new ArrayData($item);
} else {
user_error(
"DataObjectSet::__construct: Passed item #{$i} is not an object or associative array,
can't be properly iterated on in templates",
E_USER_WARNING
);
$this->items[$i] = $item;
}
}
}
parent::__construct();
}
/**
* Necessary for interface ArrayAccess. Returns whether an item with $key exists
* @param mixed $key
* @return bool
*/
public function offsetExists($key) {
return isset($this->items[$key]);
}
/**
* Necessary for interface ArrayAccess. Returns item stored in array with index $key
* @param mixed $key
* @return DataObject
*/
public function offsetGet($key) {
return $this->items[$key];
}
/**
* Necessary for interface ArrayAccess. Set an item with the key in $key
* @param mixed $key
* @param mixed $value
*/
public function offsetSet($key, $value) {
$this->items[$key] = $value;
}
/**
* Necessary for interface ArrayAccess. Unset an item with the key in $key
* @param mixed $key
*/
public function offsetUnset($key) {
unset($this->items[$key]);
}
/**
* Destory all of the DataObjects in this set.
*/
public function destroy() {
foreach($this->items as $item) {
$item->destroy();
}
}
/**
* Removes all the items in this set.
*/
public function emptyItems() {
$this->items = array();
}
/**
* Convert this DataObjectSet to an array of DataObjects.
* @param string $index Index the array by this field.
* @return array
*/
public function toArray($index = null) {
if(!$index) {
return $this->items;
}
$map = array();
foreach($this->items as $item) {
$map[$item->$index] = $item;
}
return $map;
}
/**
* Convert this DataObjectSet to an array of maps.
* @param string $index Index the array by this field.
* @return array
*/
public function toNestedArray($index = null){
if(!$index) {
$index = "ID";
}
$map = array();
foreach( $this->items as $item ) {
$map[$item->$index] = $item->getAllFields();
}
return $map;
}
/**
* Returns an array of ID => Title for the items in this set.
*
* This is an alias of {@link DataObjectSet->map()}
*
* @deprecated 2.5 Please use map() instead
*
* @param string $index The field to use as a key for the array
* @param string $titleField The field (or method) to get values for the map
* @param string $emptyString Empty option text e.g "(Select one)"
* @param bool $sort Sort the map alphabetically based on the $titleField value
* @return array
*/
public function toDropDownMap($index = 'ID', $titleField = 'Title', $emptyString = null, $sort = false) {
return $this->map($index, $titleField, $emptyString, $sort);
}
/**
* Set number of objects on each page.
* @param int $length Number of objects per page
*/
public function setPageLength($length) {
$this->pageLength = $length;
}
/**
* Set the page limits.
* @param int $pageStart The start of this page.
* @param int $pageLength Number of objects per page
* @param int $totalSize Total number of objects.
*/
public function setPageLimits($pageStart, $pageLength, $totalSize) {
$this->pageStart = $pageStart;
$this->pageLength = $pageLength;
$this->totalSize = $totalSize;
}
/**
* Get the page limits
* @return array
*/
public function getPageLimits() {
return array(
'pageStart' => $this->pageStart,
'pageLength' => $this->pageLength,
'totalSize' => $this->totalSize,
"DataObjectSet::__construct: Passed item #{$i} is not an"
. ' and object or associative array, can\'t be properly'
. ' iterated on in templates', E_USER_WARNING
);
}
/**
* Use the limit from the given query to add prev/next buttons to this DataObjectSet.
* @param SQLQuery $query The query used to generate this DataObjectSet
*/
public function parseQueryLimit(SQLQuery $query) {
if($query->limit) {
if(is_array($query->limit)) {
$length = $query->limit['limit'];
$start = $query->limit['start'];
} else if(stripos($query->limit, 'OFFSET')) {
list($length, $start) = preg_split("/ +OFFSET +/i", trim($query->limit));
} else {
$result = preg_split("/ *, */", trim($query->limit));
$start = $result[0];
$length = isset($result[1]) ? $result[1] : null;
}
if(!$length) {
$length = $start;
$start = 0;
}
$this->setPageLimits($start, $length, $query->unlimitedRowCount());
}
}
/**
* Returns the number of the current page.
* @return int
*/
public function CurrentPage() {
return floor($this->pageStart / $this->pageLength) + 1;
}
/**
* Returns the total number of pages.
* @return int
*/
public function TotalPages() {
if($this->totalSize == 0) {
$this->totalSize = $this->Count();
}
if($this->pageLength == 0) {
$this->pageLength = 10;
}
return ceil($this->totalSize / $this->pageLength);
}
/**
* Return a datafeed of page-links, good for use in search results, etc.
* $maxPages will put an upper limit on the number of pages to return. It will
* show the pages surrounding the current page, so you can still get to the deeper pages.
* @param int $maxPages The maximum number of pages to return
* @return DataObjectSet
*/
public function Pages($maxPages = 0){
$ret = new DataObjectSet();
if($maxPages) {
$startPage = ($this->CurrentPage() - floor($maxPages / 2)) - 1;
$endPage = $this->CurrentPage() + floor($maxPages / 2);
if($startPage < 0) {
$startPage = 0;
$endPage = $maxPages;
}
if($endPage > $this->TotalPages()) {
$endPage = $this->TotalPages();
$startPage = max(0, $endPage - $maxPages);
}
} else {
$startPage = 0;
$endPage = $this->TotalPages();
}
for($i=$startPage; $i < $endPage; $i++){
$link = HTTP::setGetVar($this->paginationGetVar, $i*$this->pageLength);
$thePage = new ArrayData(array(
"PageNum" => $i+1,
"Link" => $link,
"CurrentBool" => ($this->CurrentPage() == $i+1)?true:false,
)
);
$ret->push($thePage);
}
return $ret;
}
/*
* Display a summarized pagination which limits the number of pages shown
* "around" the currently active page for visual balance.
* In case more paginated pages have to be displayed, only
*
* Example: 25 pages total, currently on page 6, context of 4 pages
* [prev] [1] ... [4] [5] [[6]] [7] [8] ... [25] [next]
*
* Example template usage:
* <code>
* <% if MyPages.MoreThanOnePage %>
* <% if MyPages.NotFirstPage %>
* <a class="prev" href="$MyPages.PrevLink">Prev</a>
* <% end_if %>
* <% control MyPages.PaginationSummary(4) %>
* <% if CurrentBool %>
* $PageNum
* <% else %>
* <% if Link %>
* <a href="$Link">$PageNum</a>
* <% else %>
* ...
* <% end_if %>
* <% end_if %>
* <% end_control %>
* <% if MyPages.NotLastPage %>
* <a class="next" href="$MyPages.NextLink">Next</a>
* <% end_if %>
* <% end_if %>
* </code>
*
* @param integer $context Number of pages to display "around" the current page. Number should be even,
* because its halved to either side of the current page.
* @return DataObjectSet
*/
public function PaginationSummary($context = 4) {
$ret = new DataObjectSet();
// convert number of pages to even number for offset calculation
if($context % 2) $context--;
// find out the offset
$current = $this->CurrentPage();
$totalPages = $this->TotalPages();
// if the first or last page is shown, use all content on one side (either left or right of current page)
// otherwise half the number for usage "around" the current page
$offset = ($current == 1 || $current == $totalPages) ? $context : floor($context/2);
$leftOffset = $current - ($offset);
if($leftOffset < 1) $leftOffset = 1;
if($leftOffset + $context > $totalPages) $leftOffset = $totalPages - $context;
for($i=0; $i < $totalPages; $i++) {
$link = HTTP::setGetVar($this->paginationGetVar, $i*$this->pageLength);
$num = $i+1;
$currentBool = ($current == $i+1) ? true:false;
if(
($num == $leftOffset-1 && $num != 1 && $num != $totalPages)
|| ($num == $leftOffset+$context+1 && $num != 1 && $num != $totalPages)
) {
$ret->push(new ArrayData(array(
"PageNum" => null,
"Link" => null,
"CurrentBool" => $currentBool,
)
));
} else if($num == 1 || $num == $totalPages || in_array($num, range($current-$offset,$current+$offset))) {
$ret->push(new ArrayData(array(
"PageNum" => $num,
"Link" => $link,
"CurrentBool" => $currentBool,
)
));
}
}
return $ret;
}
/**
* Returns true if the current page is not the first page.
* @return boolean
*/
public function NotFirstPage(){
return $this->CurrentPage() != 1;
}
/**
* Returns true if the current page is not the last page.
* @return boolean
*/
public function NotLastPage(){
return $this->CurrentPage() != $this->TotalPages();
}
/**
* Returns true if there is more than one page.
* @return boolean
*/
public function MoreThanOnePage(){
return $this->TotalPages() > 1;
}
function FirstItem() {
return isset($this->pageStart) ? $this->pageStart + 1 : 1;
}
function LastItem() {
if(isset($this->pageStart)) {
return min($this->pageStart + $this->pageLength, $this->totalSize);
} else {
return min($this->pageLength, $this->totalSize);
}
}
/**
* Returns the URL of the previous page.
* @return string
*/
public function PrevLink() {
if($this->pageStart - $this->pageLength >= 0) {
return HTTP::setGetVar($this->paginationGetVar, $this->pageStart - $this->pageLength);
}
}
/**
* Returns the URL of the next page.
* @return string
*/
public function NextLink() {
if($this->pageStart + $this->pageLength < $this->totalSize) {
return HTTP::setGetVar($this->paginationGetVar, $this->pageStart + $this->pageLength);
}
}
/**
* Allows us to use multiple pagination GET variables on the same page (eg. if you have search results and page comments on a single page)
*
* @param string $var The variable to go in the GET string (Defaults to 'start')
*/
public function setPaginationGetVar($var) {
$this->paginationGetVar = $var;
}
/**
* Add an item to the DataObject Set.
* @param DataObject $item Item to add.
* @param string $key Key to index this DataObject by.
*/
public function push($item, $key = null) {
if($key != null) {
unset($this->items[$key]);
$this->items[$key] = $item;
} else {
$this->items[] = $item;
}
}
/**
* Add an item to the beginning of the DataObjectSet
* @param DataObject $item Item to add
* @param string $key Key to index this DataObject by.
*/
public function insertFirst($item, $key = null) {
if($key == null) {
array_unshift($this->items, $item);
} else {
$this->items = array_merge(array($key=>$item), $this->items);
}
}
/**
* Insert a DataObject at the beginning of this set.
* @param DataObject $item Item to insert.
*/
public function unshift($item) {
$this->insertFirst($item);
}
/**
* Remove a DataObject from the beginning of this set and return it.
* This is the equivalent of pop() but acts on the head of the set.
* Opposite of unshift().
*
* @return DataObject (or null if there are no items in the set)
*/
public function shift() {
return array_shift($this->items);
}
/**
* Remove a DataObject from the end of this set and return it.
* This is the equivalent of shift() but acts on the tail of the set.
* Opposite of push().
*
* @return DataObject (or null if there are no items in the set)
*/
public function pop() {
return array_pop($this->items);
}
/**
* Remove a DataObject from this set.
* @param DataObject $itemObject Item to remove.
*/
public function remove($itemObject) {
foreach($this->items as $key=>$item){
if($item === $itemObject){
unset($this->items[$key]);
}
}
}
/**
* Replaces $itemOld with $itemNew
*
* @param DataObject $itemOld
* @param DataObject $itemNew
*/
public function replace($itemOld, $itemNew) {
foreach($this->items as $key => $item) {
if($item === $itemOld) {
$this->items[$key] = $itemNew;
return;
}
}
}
/**
* Merge another set onto the end of this set.
* To merge without causing duplicates, consider calling
* {@link removeDuplicates()} after this method on the new set.
*
* @param DataObjectSet $anotherSet Set to mege onto this set.
*/
public function merge($anotherSet){
if($anotherSet) {
foreach($anotherSet as $item){
$this->push($item);
}
}
}
/**
* Gets a specific slice of an existing set.
*
* @param int $offset
* @param int $length
* @return DataObjectSet
*/
public function getRange($offset, $length) {
$set = array_slice($this->items, (int)$offset, (int)$length);
return new DataObjectSet($set);
}
/**
* Returns an Iterator for this DataObjectSet.
* This function allows you to use DataObjectSets in foreach loops
* @return DataObjectSet_Iterator
*/
public function getIterator() {
return new DataObjectSet_Iterator($this->items);
}
/**
* Returns false if the set is empty.
* @return boolean
*/
public function exists() {
return (bool)$this->items;
}
/**
* Return the first item in the set.
* @return DataObject
*/
public function First() {
if(count($this->items) < 1)
return null;
$keys = array_keys($this->items);
return $this->items[$keys[0]];
}
/**
* Return the last item in the set.
* @return DataObject
*/
public function Last() {
if(count($this->items) < 1)
return null;
$keys = array_keys($this->items);
return $this->items[$keys[sizeof($keys)-1]];
}
/**
* Return the total number of items in this dataset.
* @return int
*/
public function TotalItems() {
return $this->totalSize ? $this->totalSize : sizeof($this->items);
}
/**
* Returns the actual number of items in this dataset.
* @return int
*/
public function Count() {
return sizeof($this->items);
}
/**
* Returns this set as a XHTML unordered list.
* @return string
*/
public function UL() {
if($this->items) {
$result = "<ul id=\"Menu1\">\n";
foreach($this->items as $item) {
$result .= "<li onclick=\"location.href = this.getElementsByTagName('a')[0].href\"><a href=\"$item->Link\">$item->Title</a></li>\n";
}
$result .= "</ul>\n";
return $result;
}
}
/**
* Returns this set as a XHTML unordered list.
* @return string
*/
public function forTemplate() {
return $this->UL();
}
/**
* Returns an array of ID => Title for the items in this set.
*
* @param string $index The field to use as a key for the array
* @param string $titleField The field (or method) to get values for the map
* @param string $emptyString Empty option text e.g "(Select one)"
* @param bool $sort Sort the map alphabetically based on the $titleField value
* @return array
*/
public function map($index = 'ID', $titleField = 'Title', $emptyString = null, $sort = false) {
$map = array();
if($this->items) {
foreach($this->items as $item) {
$map[$item->$index] = ($item->hasMethod($titleField)) ? $item->$titleField() : $item->$titleField;
}
}
if($emptyString) $map = array('' => "$emptyString") + $map;
if($sort) asort($map);
return $map;
}
/**
* Find an item in this list where the field $key is equal to $value
* Eg: $doSet->find('ID', 4);
* @return ViewableData The first matching item.
*/
public function find($key, $value) {
foreach($this->items as $item) {
if($item->$key == $value) return $item;
}
}
/**
* Return a column of the given field
* @param string $value The field name
* @return array
*/
public function column($value = "ID") {
$list = array();
foreach($this->items as $item ){
$list[] = ($item->hasMethod($value)) ? $item->$value() : $item->$value;
}
return $list;
}
/**
* Returns an array of DataObjectSets. The array is keyed by index.
*
* @param string $index The field name to index the array by.
* @return array
*/
public function groupBy($index) {
$result = array();
foreach($this->items as $item) {
$key = ($item->hasMethod($index)) ? $item->$index() : $item->$index;
if(!isset($result[$key])) {
$result[$key] = new DataObjectSet();
}
$result[$key]->push($item);
}
return $result;
}
/**
* Groups the items by a given field.
* Returns a DataObjectSet suitable for use in a nested template.
* @param string $index The field to group by
* @param string $childControl The name of the nested page control
* @return DataObjectSet
*/
public function GroupedBy($index, $childControl = "Children") {
$grouped = $this->groupBy($index);
$groupedAsSet = new DataObjectSet();
foreach($grouped as $group) {
$groupedAsSet->push($group->First()->customise(array(
$childControl => $group
)));
}
return $groupedAsSet;
}
/**
* Returns a nested unordered list out of a "chain" of DataObject-relations,
* using the automagic ComponentSet-relation-methods to find subsequent DataObjectSets.
* The formatting of the list can be different for each level, and is evaluated as an SS-template
* with access to the current DataObjects attributes and methods.
*
* Example: Groups (Level 0, the "calling" DataObjectSet, needs to be queried externally)
* and their Members (Level 1, determined by the Group->Members()-relation).
*
* @param array $nestingLevels
* Defines relation-methods on DataObjects as a string, plus custom
* SS-template-code for the list-output. Use "Root" for the current DataObjectSet (is will not evaluate into
* a function).
* Caution: Don't close the list-elements (determined programatically).
* You need to escape dollar-signs that need to be evaluated as SS-template-code.
* Use $EvenOdd to get appropriate classes for CSS-styling.
* Format:
* array(
* array(
* "dataclass" => "Root",
* "template" => "<li class=\"\$EvenOdd\"><a href=\"admin/crm/show/\$ID\">\$AccountName</a>"
* ),
* array(
* "dataclass" => "GrantObjects",
* "template" => "<li class=\"\$EvenOdd\"><a href=\"admin/crm/showgrant/\$ID\">#\$GrantNumber: \$TotalAmount.Nice, \$ApplicationDate.ShortMonth \$ApplicationDate.Year</a>"
* )
* );
* @param string $ulExtraAttributes Extra attributes
*
* @return string Unordered List (HTML)
*/
public function buildNestedUL($nestingLevels, $ulExtraAttributes = "") {
return $this->getChildrenAsUL($nestingLevels, 0, "", $ulExtraAttributes);
}
/**
* Gets called recursively on the child-objects of the chain.
*
* @param array $nestingLevels see {@buildNestedUL}
* @param int $level Current nesting level
* @param string $template Template for list item
* @param string $ulExtraAttributes Extra attributes
* @return string
*/
public function getChildrenAsUL($nestingLevels, $level = 0, $template = "<li id=\"record-\$ID\" class=\"\$EvenOdd\">\$Title", $ulExtraAttributes = null, &$itemCount = 0) {
$output = "";
$hasNextLevel = false;
$ulExtraAttributes = " $ulExtraAttributes";
$output = "<ul" . eval($ulExtraAttributes) . ">\n";
$currentNestingLevel = $nestingLevels[$level];
// either current or default template
$currentTemplate = (!empty($currentNestingLevel)) ? $currentNestingLevel['template'] : $template;
$myViewer = SSViewer::fromString($currentTemplate);
if(isset($nestingLevels[$level+1]['dataclass'])){
$childrenMethod = $nestingLevels[$level+1]['dataclass'];
}
// sql-parts
$filter = (isset($nestingLevels[$level+1]['filter'])) ? $nestingLevels[$level+1]['filter'] : null;
$sort = (isset($nestingLevels[$level+1]['sort'])) ? $nestingLevels[$level+1]['sort'] : null;
$join = (isset($nestingLevels[$level+1]['join'])) ? $nestingLevels[$level+1]['join'] : null;
$limit = (isset($nestingLevels[$level+1]['limit'])) ? $nestingLevels[$level+1]['limit'] : null;
$having = (isset($nestingLevels[$level+1]['having'])) ? $nestingLevels[$level+1]['having'] : null;
foreach($this as $parent) {
$evenOdd = ($itemCount % 2 == 0) ? "even" : "odd";
$parent->setField('EvenOdd', $evenOdd);
$template = $myViewer->process($parent);
// if no output is selected, fall back to the id to keep the item "clickable"
$output .= $template . "\n";
if(isset($childrenMethod)) {
// workaround for missing groupby/having-parameters in instance_get
// get the dataobjects for the next level
$children = $parent->$childrenMethod($filter, $sort, $join, $limit, $having);
if($children) {
$output .= $children->getChildrenAsUL($nestingLevels, $level+1, $currentTemplate, $ulExtraAttributes);
}
}
$output .= "</li>\n";
$itemCount++;
}
$output .= "</ul>\n";
return $output;
}
/**
* Sorts the current DataObjectSet instance.
* @param string $fieldname The name of the field on the DataObject that you wish to sort the set by.
* @param string $direction Direction to sort by, either "ASC" or "DESC".
*/
public function sort($fieldname, $direction = "ASC") {
if($this->items) {
if (is_string($fieldname) && preg_match('/(.+?)(\s+?)(A|DE)SC$/', $fieldname, $matches)) {
$fieldname = $matches[1];
$direction = $matches[3].'SC';
}
column_sort($this->items, $fieldname, $direction, false);
}
}
/**
* Remove duplicates from this set based on the dataobjects field.
* Assumes all items contained in the set all have that field.
* Useful after merging to sets via {@link merge()}.
*
* @param string $field the field to check for duplicates
*/
public function removeDuplicates($field = 'ID') {
$exists = array();
foreach($this->items as $key => $item) {
if(isset($exists[$fullkey = ClassInfo::baseDataClass($item) . ":" . $item->$field])) {
unset($this->items[$key]);
}
$exists[$fullkey] = true;
}
}
/**
* Returns information about this set in HTML format for debugging.
* @return string
*/
public function debug() {
$val = "<h2>" . $this->class . "</h2><ul>";
foreach($this as $item) {
$val .= "<li style=\"list-style-type: disc; margin-left: 20px\">" . Debug::text($item) . "</li>";
}
$val .= "</ul>";
return $val;
}
/**
* Groups the set by $groupField and returns the parent of each group whose class
* is $groupClassName. If $collapse is true, the group will be collapsed up until an ancestor with the
* given class is found.
* @param string $groupField The field to group by.
* @param string $groupClassName Classname.
* @param string $sortParents SORT clause to insert into the parents SQL.
* @param string $parentField Parent field.
* @param boolean $collapse Collapse up until an ancestor with the given class is found.
* @param string $requiredParents Required parents
* @return DataObjectSet
*/
public function groupWithParents($groupField, $groupClassName, $sortParents = null, $parentField = 'ID', $collapse = false, $requiredParents = null) {
$groupTable = ClassInfo::baseDataClass($groupClassName);
// Each item in this DataObjectSet is grouped into a multidimensional array
// indexed by it's parent. The parent IDs are later used to find the parents
// that make up the returned set.
$groupedSet = array();
// Array to store the subgroups matching the requirements
$resultsArray = array();
// Put this item into the array indexed by $groupField.
// the keys are later used to retrieve the top-level records
foreach( $this->items as $item ) {
$groupedSet[$item->$groupField][] = $item;
}
$parentSet = null;
// retrieve parents for this set
// TODO How will we collapse the hierarchy to bridge the gap?
// if collapse is specified, then find the most direct ancestor of type
// $groupClassName
if($collapse) {
// The most direct ancestors with the type $groupClassName
$parentSet = array();
// get direct parents
$parents = DataObject::get($groupClassName, "\"$groupTable\".\"$parentField\" IN( " . implode( ",", array_keys( $groupedSet ) ) . ")", $sortParents );
// for each of these parents...
foreach($parents as $parent) {
// store the old parent ID. This is required to change the grouped items
// in the $groupSet array
$oldParentID = $parent->ID;
// get the parental stack
$parentObjects= $parent->parentStack();
$parentStack = array();
foreach( $parentObjects as $parentObj )
$parentStack[] = $parentObj->ID;
// is some particular IDs are required, then get the intersection
if($requiredParents && count($requiredParents)) {
$parentStack = array_intersect($requiredParents, $parentStack);
}
$newParent = null;
// If there are no parents, the group can be omitted
if(empty($parentStack)) {
$newParent = new DataObjectSet();
} else {
$newParent = DataObject::get_one( $groupClassName, "\"$groupTable\".\"$parentField\" IN( " . implode( ",", $parentStack ) . ")" );
}
// change each of the descendant's association from the old parent to
// the new parent. This effectively collapses the hierarchy
foreach( $groupedSet[$oldParentID] as $descendant ) {
$groupedSet[$newParent->ID][] = $descendant;
}
// Add the most direct ancestor of type $groupClassName
$parentSet[] = $newParent;
}
// otherwise get the parents of these items
} else {
$requiredIDs = array_keys( $groupedSet );
if( $requiredParents && cont($requiredParents)) {
$requiredIDs = array_intersect($requiredParents, $requiredIDs);
}
if(empty($requiredIDs)) {
$parentSet = new DataObjectSet();
} else {
$parentSet = DataObject::get( $groupClassName, "\"$groupTable\".\"$parentField\" IN( " . implode( ",", $requiredIDs ) . ")", $sortParents );
}
$parentSet = $parentSet->toArray();
}
foreach($parentSet as $parent) {
$resultsArray[] = $parent->customise(array(
"GroupItems" => new DataObjectSet($groupedSet[$parent->$parentField])
));
}
return new DataObjectSet($resultsArray);
}
/**
* Add a field to this set without writing it to the database
* @param DataObject $field Field to add
*/
function addWithoutWrite($field) {
$this->items[] = $field;
}
/**
* Returns true if the DataObjectSet contains all of the IDs givem
* @param $idList An array of object IDs
*/
function containsIDs($idList) {
foreach($idList as $item) $wants[$item] = true;
foreach($this->items as $item) if($item) unset($wants[$item->ID]);
return !$wants;
}
/**
* Returns true if the DataObjectSet contains all of and *only* the IDs given.
* Note that it won't like duplicates very much.
* @param $idList An array of object IDs
*/
function onlyContainsIDs($idList) {
return $this->containsIDs($idList) && sizeof($idList) == sizeof($this->items);
parent::__construct($items);
}
}
/**
* Sort a 2D array by particular column.
* @param array $data The array to sort.
* @param mixed $column The name of the column you wish to sort by, or an array of column=>directions to sort by.
* @param string $direction Direction to sort by, either "ASC" or "DESC".
* @param boolean $preserveIndexes Preserve indexes
*/
function column_sort(&$data, $column, $direction = "ASC", $preserveIndexes = true) {
global $column_sort_field;
// if we were only given a string for column, move it into an array
if (is_string($column)) $column = array($column => $direction);
// convert directions to integers
foreach ($column as $k => $v) {
if ($v == 'ASC') {
$column[$k] = 1;
}
elseif ($v == 'DESC') {
$column[$k] = -1;
}
elseif (!is_numeric($v)) {
$column[$k] = 0;
}
}
$column_sort_field = $column;
if($preserveIndexes) {
uasort($data, "column_sort_callback_basic");
} else {
usort($data, "column_sort_callback_basic");
}
}
/**
* Callback used by column_sort
*/
function column_sort_callback_basic($a, $b) {
global $column_sort_field;
$result = 0;
// loop through each sort field
foreach ($column_sort_field as $field => $multiplier) {
// if A < B then no further examination is necessary
if ($a->$field < $b->$field) {
$result = -1 * $multiplier;
break;
}
// if A > B then no further examination is necessary
elseif ($a->$field > $b->$field) {
$result = $multiplier;
break;
}
// A == B means we need to compare the two using the next field
// if this was the last field, then function returns that objects
// are equivalent
}
return $result;
}
/**
* An Iterator for a DataObjectSet
*
* @package sapphire
* @subpackage model
*/
class DataObjectSet_Iterator implements Iterator {
function __construct($items) {
$this->items = $items;
$this->current = $this->prepareItem(current($this->items));
}
/**
* Prepare an item taken from the internal array for
* output by this iterator. Ensures that it is an object.
* @param DataObject $item Item to prepare
* @return DataObject
*/
protected function prepareItem($item) {
if(is_object($item)) {
$item->iteratorProperties(key($this->items), sizeof($this->items));
}
// This gives some reliablity but it patches over the root cause of the bug...
// else if(key($this->items) !== null) $item = new ViewableData();
return $item;
}
/**
* Return the current object of the iterator.
* @return DataObject
*/
public function current() {
return $this->current;
}
/**
* Return the key of the current object of the iterator.
* @return mixed
*/
public function key() {
return key($this->items);
}
/**
* Return the next item in this set.
* @return DataObject
*/
public function next() {
$this->current = $this->prepareItem(next($this->items));
return $this->current;
}
/**
* Rewind the iterator to the beginning of the set.
* @return DataObject The first item in the set.
*/
public function rewind() {
$this->current = $this->prepareItem(reset($this->items));
return $this->current;
}
/**
* Check the iterator is pointing to a valid item in the set.
* @return boolean
*/
public function valid() {
return $this->current !== false;
}
/**
* Return the next item in this set without progressing the iterator.
* @return DataObject
*/
public function peekNext() {
return $this->getOffset(1);
}
/**
* Return the prvious item in this set, without affecting the iterator.
* @return DataObject
*/
public function peekPrev() {
return $this->getOffset(-1);
}
/**
* Return the object in this set offset by $offset from the iterator pointer.
* @param int $offset The offset.
* @return DataObject|boolean DataObject of offset item, or boolean FALSE if not found
*/
public function getOffset($offset) {
$keys = array_keys($this->items);
foreach($keys as $i => $key) {
if($key == key($this->items)) break;
}
if(isset($keys[$i + $offset])) {
$requiredKey = $keys[$i + $offset];
return $this->items[$requiredKey];
}
return false;
}
}
?>

507
model/DataQuery.php Normal file
View File

@ -0,0 +1,507 @@
<?php
/**
* An object representing a query of data from the DataObject's supporting database.
* Acts as a wrapper over {@link SQLQuery} and performs all of the query generation.
* Used extensively by DataList.
*/
class DataQuery {
protected $dataClass;
protected $query;
protected $collidingFields = array();
private $queryFinalised = false;
// TODO: replace subclass_access with this
protected $querySubclasses = true;
// TODO: replace restrictclasses with this
protected $filterByClassName = true;
/**
* Create a new DataQuery.
* @param $dataClass The name of the DataObject class that you wish to query
*/
function __construct($dataClass) {
$this->dataClass = $dataClass;
$this->initialiseQuery();
}
/**
* Clone this object
*/
function __clone() {
$this->query = clone $this->query;
}
/**
* Return the {@link DataObject} class that is being queried.
*/
function dataClass() {
return $this->dataClass;
}
/**
* Return the {@link SQLQuery} object that represents the current query; note that it will
* be a clone of the object.
*/
function query() {
return $this->getFinalisedQuery();
}
/**
* Remove a filter from the query
*/
function removeFilterOn($fieldExpression) {
$matched = false;
foreach($this->query->where as $i=>$item) {
if(strpos($item, $fieldExpression) !== false) {
unset($this->query->where[$i]);
$matched = true;
}
}
if(!$matched) user_error("Couldn't find $fieldExpression in the query filter.", E_USER_WARNING);
return $this;
}
/**
* Set up the simplest intial query
*/
function initialiseQuery() {
// Get the tables to join to
$tableClasses = ClassInfo::dataClassesFor($this->dataClass);
// Error checking
if(!$tableClasses) {
if(!SS_ClassLoader::instance()->hasManifest()) {
user_error("DataObjects have been requested before the manifest is loaded. Please ensure you are not querying the database in _config.php.", E_USER_ERROR);
} else {
user_error("DataObject::buildSQL: Can't find data classes (classes linked to tables) for $this->dataClass. Please ensure you run dev/build after creating a new DataObject.", E_USER_ERROR);
}
}
$baseClass = array_shift($tableClasses);
$select = array("\"$baseClass\".*");
// Build our intial query
$this->query = new SQLQuery(array());
$this->query->distinct = true;
if($sort = singleton($this->dataClass)->stat('default_sort')) {
$this->sort($sort);
}
$this->query->from("\"$baseClass\"");
$this->selectAllFromTable($this->query, $baseClass);
singleton($this->dataClass)->extend('augmentDataQueryCreation', $this->query, $this);
}
/**
* Ensure that the query is ready to execute.
*/
function getFinalisedQuery() {
$query = clone $this->query;
// Get the tables to join to
$tableClasses = ClassInfo::dataClassesFor($this->dataClass);
$baseClass = array_shift($tableClasses);
$collidingFields = array();
// Join all the tables
if($this->querySubclasses) {
foreach($tableClasses as $tableClass) {
$query->leftJoin($tableClass, "\"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"") ;
$this->selectAllFromTable($query, $tableClass);
}
}
// Resolve colliding fields
if($this->collidingFields) {
foreach($this->collidingFields as $k => $collisions) {
$caseClauses = array();
foreach($collisions as $collision) {
if(preg_match('/^"([^"]+)"/', $collision, $matches)) {
$collisionBase = $matches[1];
$collisionClasses = ClassInfo::subclassesFor($collisionBase);
$caseClauses[] = "WHEN \"$baseClass\".\"ClassName\" IN ('"
. implode("', '", $collisionClasses) . "') THEN $collision";
} else {
user_error("Bad collision item '$collision'", E_USER_WARNING);
}
}
$query->select[$k] = "CASE " . implode( " ", $caseClauses) . " ELSE NULL END"
. " AS \"$k\"";
}
}
if($this->filterByClassName) {
// If querying the base class, don't bother filtering on class name
if($this->dataClass != $baseClass) {
// Get the ClassName values to filter to
$classNames = ClassInfo::subclassesFor($this->dataClass);
if(!$classNames) user_error("DataObject::get() Can't find data sub-classes for '$callerClass'");
$query->where[] = "\"$baseClass\".\"ClassName\" IN ('" . implode("','", $classNames) . "')";
}
}
$query->select[] = "\"$baseClass\".\"ID\"";
$query->select[] = "CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\" ELSE '$baseClass' END AS \"RecordClassName\"";
// TODO: Versioned, Translatable, SiteTreeSubsites, etc, could probably be better implemented as subclasses of DataQuery
singleton($this->dataClass)->extend('augmentSQL', $query, $this);
return $query;
}
/**
* Execute the query and return the result as {@link Query} object.
*/
function execute() {
return $this->getFinalisedQuery()->execute();
}
/**
* Return this query's SQL
*/
function sql() {
return $this->getFinalisedQuery()->sql();
}
/**
* Return the number of records in this query.
* Note that this will issue a separate SELECT COUNT() query.
*/
function count() {
$baseClass = ClassInfo::baseDataClass($this->dataClass);
return $this->getFinalisedQuery()->count("DISTINCT \"$baseClass\".\"ID\"");
}
/**
* Return the maximum value of the given field in this DataList
*/
function Max($field) {
return $this->getFinalisedQuery()->aggregate("MAX(\"$field\")")->execute()->value();
}
/**
* Return the minimum value of the given field in this DataList
*/
function Min($field) {
return $this->getFinalisedQuery()->aggregate("MIN(\"$field\")")->execute()->value();
}
/**
* Return the average value of the given field in this DataList
*/
function Avg($field) {
return $this->getFinalisedQuery()->aggregate("AVG(\"$field\")")->execute()->value();
}
/**
* Return the sum of the values of the given field in this DataList
*/
function Sum($field) {
return $this->getFinalisedQuery()->aggregate("SUM(\"$field\")")->execute()->value();
}
/**
* Return the first row that would be returned by this full DataQuery
* Note that this will issue a separate SELECT ... LIMIT 1 query.
*/
function firstRow() {
return $this->getFinalisedQuery()->firstRow();
}
/**
* Return the last row that would be returned by this full DataQuery
* Note that this will issue a separate SELECT ... LIMIT query.
*/
function lastRow() {
return $this->getFinalisedQuery()->lastRow();
}
/**
* Update the SELECT clause of the query with the columns from the given table
*/
protected function selectAllFromTable(SQLQuery &$query, $tableClass) {
// Add SQL for multi-value fields
$databaseFields = DataObject::database_fields($tableClass);
$compositeFields = DataObject::composite_fields($tableClass, false);
if($databaseFields) foreach($databaseFields as $k => $v) {
if(!isset($compositeFields[$k])) {
// Update $collidingFields if necessary
if(isset($query->select[$k])) {
if(!isset($this->collidingFields[$k])) $this->collidingFields[$k] = array($query->select[$k]);
$this->collidingFields[$k][] = "\"$tableClass\".\"$k\"";
} else {
$query->select[$k] = "\"$tableClass\".\"$k\"";
}
}
}
if($compositeFields) foreach($compositeFields as $k => $v) {
if($v) {
$dbO = Object::create_from_string($v, $k);
$dbO->addToQuery($query);
}
}
}
/**
* Set the HAVING clause of this query
*/
function having($having) {
if($having) {
$clone = $this;
$clone->query->having[] = $having;
return $clone;
} else {
return $this;
}
}
/**
* Set the WHERE clause of this query
*/
function where($filter) {
if($filter) {
$clone = $this;
$clone->query->where($filter);
return $clone;
} else {
return $this;
}
}
/**
* Set the ORDER BY clause of this query
*/
function sort($sort) {
if($sort) {
$clone = $this;
// Add quoting to sort expression if it's a simple column name
if(!is_array($sort) && preg_match('/^[A-Z][A-Z0-9_]*$/i', $sort)) $sort = "\"$sort\"";
$clone->query->orderby($sort);
return $clone;
} else {
return $this;
}
}
/**
* Set the limit of this query
*/
function limit($limit) {
if($limit) {
$clone = $this;
$clone->query->limit($limit);
return $clone;
} else {
return $this;
}
}
/**
* Add a join clause to this query
* @deprecated Use innerJoin() or leftJoin() instead.
*/
function join($join) {
if($join) {
$clone = $this;
$clone->query->from[] = $join;
// TODO: This needs to be resolved for all databases
if(DB::getConn() instanceof MySQLDatabase) $clone->query->groupby[] = reset($clone->query->from) . ".\"ID\"";
return $clone;
} else {
return $this;
}
}
/**
* Add an INNER JOIN clause to this queyr
* @param $table The table to join to.
* @param $onClause The filter for the join.
*/
public function innerJoin($table, $onClause, $alias = null) {
if($table) {
$clone = $this;
$clone->query->innerJoin($table, $onClause, $alias);
return $clone;
} else {
return $this;
}
}
/**
* Add a LEFT JOIN clause to this queyr
* @param $table The table to join to.
* @param $onClause The filter for the join.
*/
public function leftJoin($table, $onClause, $alias = null) {
if($table) {
$clone = $this;
$clone->query->leftJoin($table, $onClause, $alias);
return $clone;
} else {
return $this;
}
}
/**
* Traverse the relationship fields, and add the table
* mappings to the query object state. This has to be called
* in any overloaded {@link SearchFilter->apply()} methods manually.
*
* @param $relation The array/dot-syntax relation to follow
* @return The model class of the related item
*/
function applyRelation($relation) {
// NO-OP
if(!$relation) return $this->dataClass;
if(is_string($relation)) $relation = explode(".", $relation);
$modelClass = $this->dataClass;
foreach($relation as $rel) {
$model = singleton($modelClass);
if ($component = $model->has_one($rel)) {
if(!$this->query->isJoinedTo($component)) {
$foreignKey = $model->getReverseAssociation($component);
$this->query->leftJoin($component, "\"$component\".\"ID\" = \"{$modelClass}\".\"{$foreignKey}ID\"");
/**
* add join clause to the component's ancestry classes so that the search filter could search on its
* ancester fields.
*/
$ancestry = ClassInfo::ancestry($component, true);
if(!empty($ancestry)){
$ancestry = array_reverse($ancestry);
foreach($ancestry as $ancestor){
if($ancestor != $component){
$this->query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\"");
$component=$ancestor;
}
}
}
}
$modelClass = $component;
} elseif ($component = $model->has_many($rel)) {
if(!$this->query->isJoinedTo($component)) {
$ancestry = $model->getClassAncestry();
$foreignKey = $model->getRemoteJoinField($rel);
$this->query->leftJoin($component, "\"$component\".\"{$foreignKey}\" = \"{$ancestry[0]}\".\"ID\"");
/**
* add join clause to the component's ancestry classes so that the search filter could search on its
* ancestor fields.
*/
$ancestry = ClassInfo::ancestry($component, true);
if(!empty($ancestry)){
$ancestry = array_reverse($ancestry);
foreach($ancestry as $ancestor){
if($ancestor != $component){
$this->query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\"");
$component=$ancestor;
}
}
}
}
$modelClass = $component;
} elseif ($component = $model->many_many($rel)) {
list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component;
$parentBaseClass = ClassInfo::baseDataClass($parentClass);
$componentBaseClass = ClassInfo::baseDataClass($componentClass);
$this->query->innerJoin($relationTable, "\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\"");
$this->query->leftJoin($componentBaseClass, "\"$relationTable\".\"$componentField\" = \"$componentBaseClass\".\"ID\"");
if(ClassInfo::hasTable($componentClass)) {
$this->query->leftJoin($componentClass, "\"$relationTable\".\"$componentField\" = \"$componentClass\".\"ID\"");
}
$modelClass = $componentClass;
}
}
return $modelClass;
}
/**
* Select the given fields from the given table
*/
public function selectFromTable($table, $fields) {
$fieldExpressions = array_map(create_function('$item',
"return '\"$table\".\"' . \$item . '\"';"), $fields);
$this->select($fieldExpressions);
}
/**
* Query the given field column from the database and return as an array.
*/
public function column($field = 'ID') {
$query = $this->getFinalisedQuery();
$query->select($this->expressionForField($field, $query));
return $query->execute()->column();
}
protected function expressionForField($field, $query) {
// Special case for ID
if($field == 'ID') {
$baseClass = ClassInfo::baseDataClass($this->dataClass);
return "\"$baseClass\".\"ID\"";
} else {
return $query->expressionForField($field);
}
}
/**
* Clear the selected fields to start over
*/
public function clearSelect() {
$this->query->select = array();
return $this;
}
/**
* Select the given field expressions. You must do your own escaping
*/
protected function select($fieldExpressions) {
$this->query->select = array_merge($this->query->select, $fieldExpressions);
}
//// QUERY PARAMS
/**
* An arbitrary store of query parameters that can be used by decorators.
* @todo This will probably be made obsolete if we have subclasses of DataList and/or DataQuery.
*/
private $queryParams;
/**
* Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query.
* It's expected that the $key will be namespaced, e.g, 'Versioned.stage' instead of just 'stage'.
*/
function setQueryParam($key, $value) {
$this->queryParams[$key] = $value;
}
/**
* Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query.
*/
function getQueryParam($key) {
if(isset($this->queryParams[$key])) return $this->queryParams[$key];
else return null;
}
}
?>

View File

@ -731,10 +731,10 @@ abstract class SS_Database {
$limit = $sqlQuery->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(!array_key_exists('limit',$limit)) throw new InvalidArgumentException('SQLQuery::limit(): Wrong format for $limit: ' . var_export($limit, true));
if(isset($limit['start']) && is_numeric($limit['start']) && isset($limit['limit']) && is_numeric($limit['limit'])) {
$combinedLimit = "$limit[limit] OFFSET $limit[start]";
$combinedLimit = $limit['start'] ? "$limit[limit] OFFSET $limit[start]" : "$limit[limit]";
} elseif(isset($limit['limit']) && is_numeric($limit['limit'])) {
$combinedLimit = (int)$limit['limit'];
} else {
@ -746,7 +746,6 @@ abstract class SS_Database {
$text .= " LIMIT " . $sqlQuery->limit;
}
}
return $text;
}

85
model/HasManyList.php Normal file
View File

@ -0,0 +1,85 @@
<?php
/**
* Subclass of {@link DataList} representing a has_many relation
*/
class HasManyList extends RelationList {
protected $foreignKey;
/**
* Create a new HasManyList object.
* Generation of the appropriate record set is left up to the caller, using the normal
* {@link DataList} methods. Addition arguments are used to support {@@link add()}
* and {@link remove()} methods.
*
* @param $dataClass The class of the DataObjects that this will list.
* @param $relationFilters A map of key => value filters that define which records
* in the $dataClass table actually belong to this relationship.
*/
function __construct($dataClass, $foreignKey) {
parent::__construct($dataClass);
$this->foreignKey = $foreignKey;
}
protected function foreignIDFilter() {
// Apply relation filter
if(is_array($this->foreignID)) {
return "\"$this->foreignKey\" IN ('" .
implode("', '", array_map('Convert::raw2sql', $this->foreignID)) . "')";
} else if($this->foreignID){
return "\"$this->foreignKey\" = '" .
Convert::raw2sql($this->foreignID) . "'";
}
}
/**
* Adds the item to this relation.
* It does so by setting the relationFilters.
* @param $item The DataObject to be added, or its ID
*/
function add($item) {
if(is_numeric($item)) $item = DataObject::get_by_id($this->dataClass, $item);
else if(!($item instanceof $this->dataClass)) user_eror("HasManyList::add() expecting a $this->dataClass object, or ID value", E_USER_ERROR);
// Validate foreignID
if(!$this->foreignID) {
user_error("ManyManyList::add() can't be called until a foreign ID is set", E_USER_WARNING);
return;
}
if(is_array($this->foreignID)) {
user_error("ManyManyList::add() can't be called on a list linked to mulitple foreign IDs", E_USER_WARNING);
return;
}
$fk = $this->foreignKey;
$item->$fk = $this->foreignID;
$item->write();
}
/**
* Remove an item from this relation.
* Doesn't actually remove the item, it just clears the foreign key value.
* @param $itemID The ID of the item to be removed
*/
function removeByID($itemID) {
$item = $this->byID($item);
return $this->remove($item);
}
/**
* Remove an item from this relation.
* Doesn't actually remove the item, it just clears the foreign key value.
* @param $item The DataObject to be removed
* @todo Maybe we should delete the object instead?
*/
function remove($item) {
if(!($item instanceof $this->dataClass)) throw new InvalidArgumentException("HasManyList::remove() expecting a $this->dataClass object, or ID value", E_USER_ERROR);
$fk = $this->foreignKey;
$item->$fk = null;
$item->write();
}
}

View File

@ -400,7 +400,7 @@ class Hierarchy extends DataExtension {
if(!(isset($this->_cache_children) && $this->_cache_children)) {
$result = $this->owner->stageChildren(false);
if(isset($result)) {
$this->_cache_children = new DataObjectSet();
$this->_cache_children = new ArrayList();
foreach($result as $child) {
if($child->canView()) {
$this->_cache_children->push($child);
@ -452,9 +452,10 @@ class Hierarchy extends DataExtension {
// Next, go through the live children. Only some of these will be listed
$liveChildren = $this->owner->liveChildren(true, true);
if($liveChildren) {
foreach($liveChildren as $child) {
$stageChildren->push($child);
}
$merged = new ArrayList();
$merged->merge($stageChildren);
$merged->merge($liveChildren);
$stageChildren = $merged;
}
}
@ -485,10 +486,8 @@ class Hierarchy extends DataExtension {
public function numHistoricalChildren() {
if(!$this->owner->hasExtension('Versioned')) throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
$query = Versioned::get_including_deleted_query(ClassInfo::baseDataClass($this->owner->class),
"\"ParentID\" = " . (int)$this->owner->ID);
return $query->unlimitedRowCount();
return Versioned::get_including_deleted(ClassInfo::baseDataClass($this->owner->class),
"\"ParentID\" = " . (int)$this->owner->ID)->count();
}
/**
@ -500,20 +499,11 @@ class Hierarchy extends DataExtension {
* @return int
*/
public function numChildren($cache = true) {
$baseClass = ClassInfo::baseDataClass($this->owner->class);
// Build the cache for this class if it doesn't exist.
if(!$cache || !is_numeric($this->_cache_numChildren)) {
// We build the query in an extension-friendly way.
$query = new SQLQuery(
"COUNT(*)",
"\"$baseClass\"",
sprintf('"ParentID" = %d', $this->owner->ID)
);
$this->owner->extend('augmentSQL', $query);
$this->owner->extend('augmentNumChildrenCountQuery', $query);
$this->_cache_numChildren = (int)$query->execute()->value();
// Hey, this is efficient now!
// We call stageChildren(), because Children() has canView() filtering
$this->_cache_numChildren = (int)$this->owner->stageChildren(true)->Count();
}
// If theres no value in the cache, it just means that it doesn't have any children.
@ -540,7 +530,6 @@ class Hierarchy extends DataExtension {
. (int)$this->owner->ID . " AND \"{$baseClass}\".\"ID\" != " . (int)$this->owner->ID
. $extraFilter, "");
if(!$staged) $staged = new DataObjectSet();
$this->owner->extend("augmentStageChildren", $staged, $showAll);
return $staged;
}
@ -556,42 +545,30 @@ class Hierarchy extends DataExtension {
public function liveChildren($showAll = false, $onlyDeletedFromStage = false) {
if(!$this->owner->hasExtension('Versioned')) throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied');
if($this->owner->db('ShowInMenus')) {
$extraFilter = ($showAll) ? '' : " AND \"ShowInMenus\"=1";
} else {
$extraFilter = '';
}
$join = "";
$baseClass = ClassInfo::baseDataClass($this->owner->class);
$id = $this->owner->ID;
$filter = "\"{$baseClass}\".\"ParentID\" = " . (int)$this->owner->ID
. " AND \"{$baseClass}\".\"ID\" != " . (int)$this->owner->ID;
$children = DataObject::get($baseClass)->where("\"{$baseClass}\".\"ParentID\" = $id AND \"{$baseClass}\".\"ID\" != $id");
if(!$showAll) $children = $children->where('"ShowInMenus" = 1');
// Query the live site
$children->dataQuery()->setQueryParam('Versioned.mode', 'stage');
$children->dataQuery()->setQueryParam('Versioned.stage', 'Live');
if($onlyDeletedFromStage) {
// Note that the lack of double-quotes around $baseClass are the only thing preventing
// it from being rewritten to {$baseClass}_Live. This is brittle and a little clumsy
$join = "LEFT JOIN {$baseClass} ON {$baseClass}.\"ID\" = \"{$baseClass}\".\"ID\"";
$filter .= " AND {$baseClass}.\"ID\" IS NULL";
// Note that this makes a second query, and could be optimised to be a joi;
$stageChildren = DataObject::get($baseClass)
->where("\"{$baseClass}\".\"ParentID\" = $id AND \"{$baseClass}\".\"ID\" != $id");
$stageChildren->dataQuery()->setQueryParam('Versioned.mode', 'stage');
$stageChildren->dataQuery()->setQueryParam('Versioned.stage', '');
$ids = $stageChildren->column("ID");
if($ids) {
$children->where("\"$baseClass\".\"ID\" NOT IN (" . implode(',',$ids) . ")");
}
}
$oldStage = Versioned::current_stage();
Versioned::reading_stage('Live');
// Singleton is necessary and not $this->owner so as not to muck with Translatable's
// behaviour.
$query = singleton($baseClass)->extendedSQL($filter, null, null, $join);
// Since we didn't include double quotes in the join & filter, we need to add them into the
// SQL now, after Versioned has done is query rewriting
$correctedSQL = str_replace(array("LEFT JOIN {$baseClass}", "{$baseClass}.\"ID\""),
array("LEFT JOIN \"{$baseClass}\"", "\"{$baseClass}\".\"ID\""), $query->sql());
$result = $this->owner->buildDataObjectSet(DB::query($correctedSQL));
Versioned::reading_stage($oldStage);
return $result;
return $children;
}
/**
@ -613,7 +590,7 @@ class Hierarchy extends DataExtension {
* @return DataObjectSet
*/
public function getAncestors() {
$ancestors = new DataObjectSet();
$ancestors = new ArrayList();
$object = $this->owner;
while($object = $object->getParent()) {

106
model/List.php Normal file
View File

@ -0,0 +1,106 @@
<?php
/**
* An interface that a class can implement to be treated as a list container.
*
* @package sapphire
* @subpackage model
*/
interface SS_List extends ArrayAccess, Countable, IteratorAggregate {
/**
* Returns all the items in the list in an array.
*
* @return arary
*/
public function toArray();
/**
* Returns the contents of the list as an array of maps.
*
* @return array
*/
public function toNestedArray();
/**
* Returns a subset of the items within the list.
*
* @param int $offset
* @param int $length
* @return SS_List
*/
public function getRange($offset, $length);
/**
* Adds an item to the list, making no guarantees about where it will
* appear.
*
* @param mixed $item
*/
public function add($item);
/**
* Removes an item from the list.
*
* @param mixed $item
*/
public function remove($item);
/**
* Returns the first item in the list.
*
* @return mixed
*/
public function first();
/**
* Returns the last item in the list.
*
* @return mixed
*/
public function last();
/**
* Returns a map of a key field to a value field of all the items in the
* list.
*
* @param string $keyfield
* @param string $titlefield
* @return array
*/
public function map($keyfield, $titlefield);
/**
* Returns the first item in the list where the key field is equal to the
* value.
*
* @param string $key
* @param mixed $value
* @return mixed
*/
public function find($key, $value);
/**
* Returns an array of a single field value for all items in the list.
*
* @param string $field
* @return array
*/
public function column($field);
/**
* Returns TRUE if the list can be sorted by a field.
*
* @param string $by
* @return bool
*/
public function canSortBy($by);
/**
* Sorts the list in place by a field on the items and direction.
*
* @param string $by The field name to sort by.
* @param string $dir Either "ASC" or "DIR".
*/
public function sort($by, $dir = 'ASC');
}

120
model/ListDecorator.php Normal file
View File

@ -0,0 +1,120 @@
<?php
/**
* A base class for decorators that wrap around a list to provide additional
* functionality. It passes through list methods to the underlying list
* implementation.
*
* @package sapphire
* @subpackage model
*/
abstract class SS_ListDecorator extends ViewableData implements SS_List {
protected $list;
public function __construct(SS_List $list) {
$this->list = $list;
$this->failover = $this->list;
parent::__construct();
}
/**
* Returns the list this decorator wraps around.
*
* @return SS_List
*/
public function getList() {
return $this->list;
}
// PROXIED METHODS ---------------------------------------------------------
public function offsetExists($key) {
return $this->list->offsetExists($key);
}
public function offsetGet($key) {
return $this->list->offsetGet($key);
}
public function offsetSet($key, $value) {
$this->list->offsetSet($key, $value);
}
public function offsetUnset($key) {
$this->list->offsetUnset($key);
}
public function toArray($index = null) {
return $this->list->toArray($index);
}
public function toNestedArray($index = null){
return $this->list->toNestedArray($index);
}
public function add($item) {
$this->list->add($item);
}
public function remove($itemObject) {
$this->list->remove($itemObject);
}
public function getRange($offset, $length) {
return $this->list->getRange($offset, $length);
}
public function getIterator() {
return $this->list->getIterator();
}
public function exists() {
return $this->list->exists();
}
public function First() {
return $this->list->First();
}
public function Last() {
return $this->list->Last();
}
public function TotalItems() {
return $this->list->TotalItems();
}
public function Count() {
return $this->list->Count();
}
public function forTemplate() {
return $this->list->forTemplate();
}
public function map($index = 'ID', $titleField = 'Title', $emptyString = null, $sort = false) {
return $this->list->map($index, $titleField, $emptyString, $sort);
}
public function find($key, $value) {
return $this->list->find($key, $value);
}
public function column($value = 'ID') {
return $this->list->column($value);
}
public function canSortBy($by) {
return $this->list->canSortBy($by);
}
public function sort($fieldname, $direction = "ASC") {
$this->list->sort($fieldname, $direction);
}
public function debug() {
return $this->list->debug();
}
}

175
model/ManyManyList.php Normal file
View File

@ -0,0 +1,175 @@
<?php
/**
* Subclass of {@link DataList} representing a many_many relation
*/
class ManyManyList extends RelationList {
protected $joinTable;
protected $localKey;
protected $foreignKey, $foreignID;
protected $extraFields;
/**
* Create a new ManyManyList object.
*
* A ManyManyList object represents a list of DataObject records that correspond to a many-many
* relationship. In addition to,
*
*
*
* Generation of the appropriate record set is left up to the caller, using the normal
* {@link DataList} methods. Addition arguments are used to support {@@link add()}
* and {@link remove()} methods.
*
* @param $dataClass The class of the DataObjects that this will list.
* @param $joinTable The name of the table whose entries define the content of this
* many_many relation.
* @param $localKey The key in the join table that maps to the dataClass' PK.
* @param $foreignKey The key in the join table that maps to joined class' PK.
* @param $extraFields A map of field => fieldtype of extra fields on the join table.
*/
function __construct($dataClass, $joinTable, $localKey, $foreignKey, $extraFields = array()) {
parent::__construct($dataClass);
$this->joinTable = $joinTable;
$this->localKey = $localKey;
$this->foreignKey = $foreignKey;
$this->extraFields = $extraFields;
$baseClass = ClassInfo::baseDataClass($dataClass);
// Join to the many-many join table
$this->dataQuery->innerJoin($joinTable, "\"$this->localKey\" = \"$baseClass\".\"ID\"");
// Query the extra fields from the join table
if($extraFields) $this->dataQuery->selectFromTable($joinTable, array_keys($extraFields));
}
/**
* Return a filter expression for the foreign ID.
*/
protected function foreignIDFilter() {
// Apply relation filter
if(is_array($this->foreignID)) {
return "\"$this->joinTable\".\"$this->foreignKey\" IN ('" .
implode("', '", array_map('Convert::raw2sql', $this->foreignID)) . "')";
} else if($this->foreignID){
return "\"$this->joinTable\".\"$this->foreignKey\" = '" .
Convert::raw2sql($this->foreignID) . "'";
}
}
/**
* Add an item to this many_many relationship
* Does so by adding an entry to the joinTable.
* @param $extraFields A map of additional columns to insert into the joinTable
*/
function add($item, $extraFields = null) {
if(is_numeric($item)) $itemID = $item;
else if($item instanceof $this->dataClass) $itemID = $item->ID;
else throw new InvalidArgumentException("ManyManyList::add() expecting a $this->dataClass object, or ID value", E_USER_ERROR);
// Validate foreignID
if(!$this->foreignID) {
throw new Exception("ManyManyList::add() can't be called until a foreign ID is set", E_USER_WARNING);
}
if(is_array($this->foreignID)) {
throw new Exception("ManyManyList::add() can't be called on a list linked to mulitple foreign IDs", E_USER_WARNING);
}
// Delete old entries, to prevent duplication
$this->removeById($itemID);
// Insert new entry
$manipulation = array();
$manipulation[$this->joinTable]['command'] = 'insert';
if($extraFields) foreach($extraFields as $k => $v) {
$manipulation[$this->joinTable]['fields'][$k] = "'" . Convert::raw2sql($v) . "'";
}
$manipulation[$this->joinTable]['fields'][$this->localKey] = $itemID;
$manipulation[$this->joinTable]['fields'][$this->foreignKey] = $this->foreignID;
DB::manipulate($manipulation);
}
/**
* Remove the given item from this list.
* Note that for a ManyManyList, the item is never actually deleted, only the join table is affected
* @param $itemID The ID of the item to remove.
*/
function remove($item) {
if(!($item instanceof $this->dataClass)) throw new InvalidArgumentException("ManyManyList::remove() expecting a $this->dataClass object");
return $this->removeByID($item->ID);
}
/**
* Remove the given item from this list.
* Note that for a ManyManyList, the item is never actually deleted, only the join table is affected
* @param $itemID The item it
*/
function removeByID($itemID) {
if(!is_numeric($itemID)) throw new InvalidArgumentException("ManyManyList::removeById() expecting an ID");
$query = new SQLQuery("*", array($this->joinTable));
$query->delete = true;
if($filter = $this->foreignIDFilter()) {
$query->where($filter);
} else {
user_error("Can't call ManyManyList::remove() until a foreign ID is set", E_USER_WARNING);
}
$query->where("\"$this->localKey\" = {$itemID}");
$query->execute();
}
/**
* Remove all items from this many-many join that match the given filter
* @deprecated this is experimental and will change. Don't use it in your projects.
*/
function removeByFilter($filter) {
$query = new SQLQuery("*", array($this->joinTable));
$query->delete = true;
$query->where($filter);
$query->execute();
}
/**
* Find the extra field data for a single row of the relationship
* join table, given the known child ID.
*
* @todo Add tests for this / refactor it / something
*
* @param string $componentName The name of the component
* @param int $childID The ID of the child for the relationship
* @return array Map of fieldName => fieldValue
*/
function getExtraData($componentName, $childID) {
$ownerObj = $this->ownerObj;
$parentField = $this->ownerClass . 'ID';
$childField = ($this->childClass == $this->ownerClass) ? 'ChildID' : ($this->childClass . 'ID');
$result = array();
if(!isset($componentName)) {
user_error('ComponentSet::getExtraData() passed a NULL component name', E_USER_ERROR);
}
if(!is_numeric($childID)) {
user_error('ComponentSet::getExtraData() passed a non-numeric child ID', E_USER_ERROR);
}
// @todo Optimize into a single query instead of one per extra field
if($this->extraFields) {
foreach($this->extraFields as $fieldName => $dbFieldSpec) {
$query = DB::query("SELECT \"$fieldName\" FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" = {$childID}");
$value = $query->value();
$result[$fieldName] = $value;
}
}
return $result;
}
}

View File

@ -830,15 +830,18 @@ class MySQLDatabase extends SS_Database {
// Get records
$records = DB::query($fullQuery);
foreach($records as $record)
$objects = array();
foreach($records as $record) {
$objects[] = new $record['ClassName']($record);
}
$list = new PaginatedList(new ArrayList($objects));
$list->setPageStart($start);
$list->setPageLEngth($pageLength);
$list->setTotalItems($totalCount);
if(isset($objects)) $doSet = new DataObjectSet($objects);
else $doSet = new DataObjectSet();
$doSet->setPageLimits($start, $pageLength, $totalCount);
return $doSet;
return $list;
}
/**

34
model/RelationList.php Normal file
View File

@ -0,0 +1,34 @@
<?php
/**
* A DataList that represents a relation.
* Adds the notion of a foreign ID that can be optionally set.
*
* @todo Is this additional class really necessary?
*/
abstract class RelationList extends DataList {
protected $foreignID;
/**
* Set the ID of the record that this ManyManyList is linking *from*.
* @param $id A single ID, or an array of IDs
*/
function setForeignID($id) {
// Turn a 1-element array into a simple value
if(is_array($id) && sizeof($id) == 1) $id = reset($id);
$this->foreignID = $id;
$this->dataQuery->where($this->foreignIDFilter());
}
/**
* Returns this ManyMany relationship linked to the given foreign ID.
* @param $id An ID or an array of IDs.
*/
function forForeignID($id) {
$this->setForeignID($id);
return $this;
}
abstract protected function foreignIDFilter();
}

View File

@ -68,7 +68,7 @@ class SQLMap extends Object implements IteratorAggregate {
*/
protected function genItems() {
if(!isset($this->items)) {
$this->items = new DataObjectSet();
$this->items = new ArrayList();
$items = $this->query->execute();
foreach($items as $item) {

View File

@ -127,6 +127,31 @@ class SQLQuery {
return $this;
}
/**
* Add addition columns to the select clause
*/
public function selectMore($fields) {
if (func_num_args() > 1) $fields = func_get_args();
if(is_array($fields)) {
foreach($fields as $field) $this->select[] = $field;
} else {
$this->select[] = $fields;
}
}
/**
* Return the SQL expression for the given field
* @todo This should be refactored after $this->select is changed to make that easier
*/
public function expressionForField($field) {
foreach($this->select as $sel) {
if(preg_match('/AS +"?([^"]*)"?/i', $sel, $matches)) $selField = $matches[1];
else if(preg_match('/"([^"]*)"\."([^"]*)"/', $sel, $matches)) $selField = $matches[2];
else if(preg_match('/"?([^"]*)"?/', $sel, $matches)) $selField = $matches[2];
if($selField == $field) return $sel;
}
}
/**
* Specify the target table to select from.
*
@ -156,7 +181,7 @@ class SQLQuery {
if( !$tableAlias ) {
$tableAlias = $table;
}
$this->from[$tableAlias] = "LEFT JOIN \"$table\" AS \"$tableAlias\" ON $onPredicate";
$this->from[$tableAlias] = array('type' => 'LEFT', 'table' => $table, 'filter' => array($onPredicate));
return $this;
}
@ -173,10 +198,25 @@ class SQLQuery {
if( !$tableAlias ) {
$tableAlias = $table;
}
$this->from[$tableAlias] = "INNER JOIN \"$table\" AS \"$tableAlias\" ON $onPredicate";
$this->from[$tableAlias] = array('type' => 'INNER', 'table' => $table, 'filter' => array($onPredicate));
return $this;
}
/**
* Add an additional filter (part of the ON clause) on a join
*/
public function addFilterToJoin($tableAlias, $filter) {
$this->from[$tableAlias]['filter'][] = $filter;
}
/**
* Replace the existing filter (ON clause) on a join
*/
public function setJoinFilter($tableAlias, $filter) {
if(is_string($this->from[$tableAlias])) {Debug::message($tableAlias); Debug::dump($this->from);}
$this->from[$tableAlias]['filter'] = array($filter);
}
/**
* Returns true if we are already joining to the given table alias
*/
@ -184,14 +224,51 @@ class SQLQuery {
return isset($this->from[$tableAlias]);
}
/**
* Return a list of tables that this query is selecting from.
*/
public function queriedTables() {
$tables = array();
foreach($this->from as $key => $tableClause) {
if(is_array($tableClause)) $table = '"'.$tableClause['table'].'"';
else if(is_string($tableClause) && preg_match('/JOIN +("[^"]+") +(AS|ON) +/i', $tableClause, $matches)) $table = $matches[1];
else $table = $tableClause;
// Handle string replacements
if($this->replacementsOld) $table = str_replace($this->replacementsOld, $this->replacementsNew, $table);
$tables[] = preg_replace('/^"|"$/','',$table);
}
return $tables;
}
/**
* Pass LIMIT clause either as SQL snippet or in array format.
* Internally, limit will always be stored as a map containing the keys 'start' and 'limit'
*
* @param string|array $limit
* @return SQLQuery This instance
*/
public function limit($limit) {
if($limit && is_numeric($limit)) {
$this->limit = array(
'start' => 0,
'limit' => $limit,
);
} else if($limit && is_string($limit)) {
if(strpos($limit,',') !== false) list($start, $innerLimit) = explode(',', $limit, 2);
else list($innerLimit, $start) = explode(' OFFSET ', strtoupper($limit), 2);
$this->limit = array(
'start' => trim($start),
'limit' => trim($innerLimit),
);
} else {
$this->limit = $limit;
}
return $this;
}
@ -378,12 +455,27 @@ class SQLQuery {
* @return string
*/
function sql() {
// Don't process empty queries
$select = is_array($this->select) ? $this->select[0] : $this->select;
if($select == '*' && !$this->from) return '';
// TODO: Don't require this internal-state manipulate-and-preserve - let sqlQueryToString() handle the new syntax
$origFrom = $this->from;
// Build from clauses
foreach($this->from as $alias => $join) {
// $join can be something like this array structure
// array('type' => 'inner', 'table' => 'SiteTree', 'filter' => array("SiteTree.ID = 1", "Status = 'approved'"))
if(is_array($join)) {
if(is_string($join['filter'])) $filter = $join['filter'];
else if(sizeof($join['filter']) == 1) $filter = $join['filter'][0];
else $filter = "(" . implode(") AND (", $join['filter']) . ")";
$this->from[$alias] = strtoupper($join['type']) . " JOIN \"{$join['table']}\" AS \"$alias\" ON $filter";
}
}
$sql = DB::getConn()->sqlQueryToString($this);
if($this->replacementsOld) $sql = str_replace($this->replacementsOld, $this->replacementsNew, $sql);
$this->from = $origFrom;
return $sql;
}
@ -393,7 +485,11 @@ class SQLQuery {
* @return string
*/
function __toString() {
try {
return $this->sql();
} catch(Exception $e) {
return "<sql query>";
}
}
/**
@ -491,6 +587,83 @@ class SQLQuery {
return (in_array($SQL_fieldName,$selects) || stripos($sql,"AS {$SQL_fieldName}"));
}
/**
* Return the number of rows in this query if the limit were removed. Useful in paged data sets.
* @return int
*
* TODO Respect HAVING and GROUPBY, which can affect the result-count
*/
function count( $column = null) {
// Choose a default column
if($column == null) {
if($this->groupby) {
$column = 'DISTINCT ' . implode(", ", $this->groupby);
} else {
$column = '*';
}
}
$clone = clone $this;
$clone->select = array("count($column)");
$clone->limit = null;
$clone->orderby = null;
$clone->groupby = null;
$count = $clone->execute()->value();
// If there's a limit set, then that limit is going to heavily affect the count
if($this->limit) {
if($count >= ($this->limit['start'] + $this->limit['limit']))
return $this->limit['limit'];
else
return max(0, $count - $this->limit['start']);
// Otherwise, the count is going to be the output of the SQL query
} else {
return $count;
}
}
/**
* Return a new SQLQuery that calls the given aggregate functions on this data.
* @param $columns An aggregate expression, such as 'MAX("Balance")', or an array of them.
*/
function aggregate($columns) {
if(!is_array($columns)) $columns = array($columns);
if($this->groupby || $this->limit) {
throw new Exception("SQLQuery::aggregate() doesn't work with groupby or limit, yet");
}
$clone = clone $this;
$clone->limit = null;
$clone->orderby = null;
$clone->groupby = null;
$clone->select = $columns;
return $clone;
}
/**
* Returns a query that returns only the first row of this query
*/
function firstRow() {
$query = clone $this;
$offset = $this->limit ? $this->limit['start'] : 0;
$query->limit(array('start' => $offset, 'limit' => 1));
return $query;
}
/**
* Returns a query that returns only the last row of this query
*/
function lastRow() {
$query = clone $this;
$offset = $this->limit ? $this->limit['start'] : 0;
$query->limit(array('start' => $this->count() + $offset - 1, 'limit' => 1));
return $query;
}
}
?>

View File

@ -106,13 +106,39 @@ class Versioned extends DataExtension {
);
}
function augmentSQL(SQLQuery &$query) {
// Get the content at a specific date
if($date = Versioned::current_archived_date()) {
foreach($query->from as $table => $dummy) {
if(!isset($baseTable)) {
$baseTable = $table;
/**
* Amend freshly created DataQuery objects with versioned-specific information
*/
function augmentDataQueryCreation(SQLQuery &$query, DataQuery &$dataQuery) {
$parts = explode('.', Versioned::get_reading_mode());
if($parts[0] == 'Archive') {
$dataQuery->setQueryParam('Versioned.mode', 'archive');
$dataQuery->setQueryParam('Versioned.date', $parts[1]);
} else if($parts[0] == 'Stage' && $parts[1] != $this->defaultStage && array_search($parts[1],$this->stages) !== false) {
$dataQuery->setQueryParam('Versioned.mode', 'stage');
$dataQuery->setQueryParam('Versioned.stage', $parts[1]);
}
}
/**
* Augment the the SQLQuery that is created by the DataQuery
* @todo Should this all go into VersionedDataQuery?
*/
function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery) {
$baseTable = ClassInfo::baseDataClass($dataQuery->dataClass());
switch($dataQuery->getQueryParam('Versioned.mode')) {
// Noop
case '':
break;
// Reading a specific data from the archive
case 'archive':
$date = $dataQuery->getQueryParam('Versioned.date');
foreach($query->from as $table => $dummy) {
$query->renameTable($table, $table . '_versions');
$query->replaceText("\"$table\".\"ID\"", "\"$table\".\"RecordID\"");
@ -132,15 +158,56 @@ class Versioned extends DataExtension {
$query->from[$archiveTable] = "INNER JOIN \"$archiveTable\"
ON \"$archiveTable\".\"ID\" = \"{$baseTable}_versions\".\"RecordID\"
AND \"$archiveTable\".\"Version\" = \"{$baseTable}_versions\".\"Version\"";
break;
// Get a specific stage
} else if(Versioned::current_stage() && Versioned::current_stage() != $this->defaultStage
&& array_search(Versioned::current_stage(), $this->stages) !== false) {
// Reading a specific stage (Stage or Live)
case 'stage':
$stage = $dataQuery->getQueryParam('Versioned.stage');
if($stage && ($stage != $this->defaultStage)) {
foreach($query->from as $table => $dummy) {
$query->renameTable($table, $table . '_' . Versioned::current_stage());
// Only rewrite table names that are actually part of the subclass tree
// This helps prevent rewriting of other tables that get joined in, in
// particular, many_many tables
if(class_exists($table) && ($table == $this->owner->class
|| is_subclass_of($table, $this->owner->class)
|| is_subclass_of($this->owner->class, $table))) {
$query->renameTable($table, $table . '_' . $stage);
}
}
}
break;
// Return all version instances
case 'all_versions':
case 'latest_versions':
foreach($query->from as $alias => $join) {
if($alias != $baseTable) {
$query->setJoinFilter($alias, "\"$alias\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$alias\".\"Version\" = \"{$baseTable}_versions\".\"Version\"");
}
$query->renameTable($alias, $alias . '_versions');
}
// Add all <basetable>_versions columns
foreach(self::$db_for_versions_table as $name => $type) {
$query->selectMore(sprintf('"%s_versions"."%s"', $baseTable, $name));
}
$query->selectMore(sprintf('"%s_versions"."%s" AS "ID"', $baseTable, 'RecordID'));
// latest_version has one more step
// Return latest version instances, regardless of whether they are on a particular stage
// This provides "show all, including deleted" functonality
if($dataQuery->getQueryParam('Versioned.mode') == 'latest_versions') {
$archiveTable = self::requireArchiveTempTable($baseTable);
$query->innerJoin($archiveTable, "\"$archiveTable\".\"ID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$archiveTable\".\"Version\" = \"{$baseTable}_versions\".\"Version\"");
}
break;
default:
throw new InvalidArgumentException("Bad value for query parameter Versioned.mode: " . $dataQuery->getQueryParam('Versioned.mode'));
}
}
/**
* Keep track of the archive tables that have been created
@ -634,7 +701,7 @@ class Versioned extends DataExtension {
$query->orderby = ($sort) ? $sort : "\"{$baseTable}_versions\".\"LastEdited\" DESC, \"{$baseTable}_versions\".\"Version\" DESC";
$records = $query->execute();
$versions = new DataObjectSet();
$versions = new ArrayList();
foreach($records as $record) {
$versions->push(new Versioned_Version($record));
@ -771,16 +838,10 @@ class Versioned extends DataExtension {
* @param string $orderby A sort expression to be inserted into the ORDER BY clause.
* @return DataObject
*/
static function get_one_by_stage($class, $stage, $filter = '', $cache = true, $orderby = '') {
$oldMode = Versioned::get_reading_mode();
Versioned::reading_stage($stage);
singleton($class)->flushCache();
$result = DataObject::get_one($class, $filter, $cache, $orderby);
singleton($class)->flushCache();
Versioned::set_reading_mode($oldMode);
return $result;
static function get_one_by_stage($class, $stage, $filter = '', $cache = true, $sort = '') {
// TODO: No identity cache operating
$items = self::get_by_stage($class, $stage, $filter, $sort, null, 1);
return $items->First();
}
/**
@ -847,11 +908,11 @@ class Versioned extends DataExtension {
* @param string $containerClass The container class for the result set (default is DataObjectSet)
* @return DataObjectSet
*/
static function get_by_stage($class, $stage, $filter = '', $sort = '', $join = '', $limit = '', $containerClass = 'DataObjectSet') {
$oldMode = Versioned::get_reading_mode();
Versioned::reading_stage($stage);
static function get_by_stage($class, $stage, $filter = '', $sort = '', $join = '', $limit = '', $containerClass = 'DataList') {
$result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass);
Versioned::set_reading_mode($oldMode);
$dq = $result->dataQuery();
$dq->setQueryParam('Versioned.mode', 'stage');
$dq->setQueryParam('Versioned.stage', $stage);
return $result;
}
@ -888,69 +949,16 @@ class Versioned extends DataExtension {
$this->owner->writeWithoutVersion();
}
/**
* Build a SQL query to get data from the _version table.
* This function is similar in style to {@link DataObject::buildSQL}
*/
function buildVersionSQL($filter = "", $sort = "") {
$query = $this->owner->extendedSQL($filter,$sort);
foreach($query->from as $table => $join) {
if($join[0] == '"') $baseTable = str_replace('"','',$join);
else $query->from[$table] = "LEFT JOIN \"$table\" ON \"$table\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\"";
$query->renameTable($table, $table . '_versions');
}
// Add all <basetable>_versions columns
foreach(self::$db_for_versions_table as $name => $type) {
$query->select[] = sprintf('"%s_versions"."%s"', $baseTable, $name);
}
$query->select[] = sprintf('"%s_versions"."%s" AS "ID"', $baseTable, 'RecordID');
return $query;
}
static function build_version_sql($className, $filter = "", $sort = "") {
$query = singleton($className)->extendedSQL($filter,$sort);
foreach($query->from as $table => $join) {
if($join[0] == '"') $baseTable = str_replace('"','',$join);
else $query->from[$table] = "LEFT JOIN \"$table\" ON \"$table\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\"";
$query->renameTable($table, $table . '_versions');
}
// Add all <basetable>_versions columns
foreach(self::$db_for_versions_table as $name => $type) {
$query->select[] = sprintf('"%s_versions"."%s"', $baseTable, $name);
}
$query->select[] = sprintf('"%s_versions"."%s" AS "ID"', $baseTable, 'RecordID');
return $query;
}
/**
* Return the latest version of the given page.
*
* @return DataObject
*/
static function get_latest_version($class, $id) {
$oldMode = Versioned::get_reading_mode();
Versioned::set_reading_mode('');
$baseTable = ClassInfo::baseDataClass($class);
$query = singleton($class)->buildVersionSQL("\"{$baseTable}\".\"RecordID\" = $id", "\"{$baseTable}\".\"Version\" DESC");
$query->limit = 1;
$record = $query->execute()->record();
if(!$record) return;
$className = $record['ClassName'];
if(!$className) {
Debug::show($query->sql());
Debug::show($record);
user_error("Versioned::get_version: Couldn't get $class.$id", E_USER_ERROR);
}
Versioned::set_reading_mode($oldMode);
return new $className($record);
$baseClass = ClassInfo::baseDataClass($class);
$list = DataList::create($class)->where("\"$baseClass\".\"RecordID\" = $id");
$list->dataQuery()->setQueryParam("Versioned.mode", "latest_versions");
return $list->First();
}
/**
@ -976,72 +984,31 @@ class Versioned extends DataExtension {
* In particular, this will query deleted records as well as active ones.
*/
static function get_including_deleted($class, $filter = "", $sort = "") {
$query = self::get_including_deleted_query($class, $filter, $sort);
// Process into a DataObjectSet
$SNG = singleton($class);
return $SNG->buildDataObjectSet($query->execute(), 'DataObjectSet', null, $class);
}
/**
* Return the query for the equivalent of a DataObject::get() call, querying the latest
* version of each page stored in the (class)_versions tables.
*
* In particular, this will query deleted records as well as active ones.
*/
static function get_including_deleted_query($class, $filter = "", $sort = "") {
$oldMode = Versioned::get_reading_mode();
Versioned::set_reading_mode('');
$SNG = singleton($class);
// Build query
$query = $SNG->buildVersionSQL($filter, $sort);
$baseTable = ClassInfo::baseDataClass($class);
$archiveTable = self::requireArchiveTempTable($baseTable);
$query->from[$archiveTable] = "INNER JOIN \"$archiveTable\"
ON \"$archiveTable\".\"ID\" = \"{$baseTable}_versions\".\"RecordID\"
AND \"$archiveTable\".\"Version\" = \"{$baseTable}_versions\".\"Version\"";
Versioned::set_reading_mode($oldMode);
return $query;
$list = DataList::create($class)->where($filter)->sort($sort);
$list->dataQuery()->setQueryParam("Versioned.mode", "latest_versions");
return $list;
}
/**
* Return the specific version of the given id
* @return DataObject
*/
static function get_version($class, $id, $version) {
$oldMode = Versioned::get_reading_mode();
Versioned::set_reading_mode('');
$baseTable = ClassInfo::baseDataClass($class);
$query = singleton($class)->buildVersionSQL("\"{$baseTable}\".\"RecordID\" = $id AND \"{$baseTable}\".\"Version\" = $version");
$record = $query->execute()->record();
$className = $record['ClassName'];
if(!$className) {
Debug::show($query->sql());
Debug::show($record);
user_error("Versioned::get_version: Couldn't get $class.$id, version $version", E_USER_ERROR);
}
Versioned::set_reading_mode($oldMode);
return new $className($record);
$baseClass = ClassInfo::baseDataClass($class);
$list = DataList::create($class)->where("\"$baseClass\".\"RecordID\" = $id")->where("\"$baseClass\".\"Version\" = " . (int)$version);
$list->dataQuery()->setQueryParam('Versioned.mode', 'all_versions');
return $list->First();
}
/**
* @return DataObject
* Return a list of all versions for a given id
* @return DataList
*/
static function get_all_versions($class, $id, $version) {
$baseTable = ClassInfo::baseDataClass($class);
$query = singleton($class)->buildVersionSQL("\"{$baseTable}\".\"RecordID\" = $id AND \"{$baseTable}\".\"Version\" = $version");
$record = $query->execute()->record();
$className = $record['ClassName'];
if(!$className) {
Debug::show($query->sql());
Debug::show($record);
user_error("Versioned::get_version: Couldn't get $class.$id, version $version", E_USER_ERROR);
}
return new $className($record);
static function get_all_versions($class, $id) {
$baseClass = ClassInfo::baseDataClass($class);
$list = DataList::create($class)->where("\"$baseClass\".\"RecordID\" = $id");
$list->dataQuery()->setQueryParam('Versioned.mode', 'all_versions');
return $list;
}
function contentcontrollerInit($controller) {
@ -1065,7 +1032,7 @@ class Versioned extends DataExtension {
* Return a piece of text to keep DataObject cache keys appropriately specific
*/
function cacheKeyComponent() {
return 'stage-'.self::current_stage();
return 'versionedmode-'.self::get_reading_mode();
}
}

View File

@ -31,7 +31,7 @@ class Int extends DBField {
}
function Times() {
$output = new DataObjectSet();
$output = new ArrayList();
for( $i = 0; $i < $this->value; $i++ )
$output->push( new ArrayData( array( 'Number' => $i + 1 ) ) );

View File

@ -60,7 +60,7 @@ class BBCodeParser extends TextParser {
static function usable_tags() {
return new DataObjectSet(
return new ArrayList(
new ArrayData(array(
"Title" => _t('BBCodeParser.BOLD', 'Bold Text'),
"Example" => '[b]<b>'._t('BBCodeParser.BOLDEXAMPLE', 'Bold').'</b>[/b]'

View File

@ -63,14 +63,14 @@ class SearchContext extends Object {
*
* @param string $modelClass The base {@link DataObject} class that search properties related to.
* Also used to generate a set of result objects based on this class.
* @param FieldSet $fields Optional. FormFields mapping to {@link DataObject::$db} properties
* @param FieldList $fields Optional. FormFields mapping to {@link DataObject::$db} properties
* which are to be searched. Derived from modelclass using
* {@link DataObject::scaffoldSearchFields()} if left blank.
* @param array $filters Optional. Derived from modelclass if left blank
*/
function __construct($modelClass, $fields = null, $filters = null) {
$this->modelClass = $modelClass;
$this->fields = ($fields) ? $fields : new FieldSet();
$this->fields = ($fields) ? $fields : new FieldList();
$this->filters = ($filters) ? $filters : array();
parent::__construct();
@ -115,19 +115,17 @@ class SearchContext extends Object {
* @return SQLQuery
*/
public function getQuery($searchParams, $sort = false, $limit = false, $existingQuery = null) {
$model = singleton($this->modelClass);
if($existingQuery) {
if(!($existingQuery instanceof DataList)) throw new InvalidArgumentException("existingQuery must be DataList");
if($existingQuery->dataClass() != $this->modelClass) throw new InvalidArgumentException("existingQuery's dataClass is " . $existingQuery->dataClass() . ", $this->modelClass expected.");
$query = $existingQuery;
} else {
$query = $model->extendedSQL();
$query = DataList::create($this->modelClass);
}
$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);
$query->limit($limit);
$query->sort($sort);
// hack to work with $searchParems when it's an Object
$searchParamArray = array();
@ -143,15 +141,12 @@ class SearchContext extends Object {
$filter->setModel($this->modelClass);
$filter->setValue($value);
if(! $filter->isEmpty()) {
$filter->apply($query);
$filter->apply($query->dataQuery());
}
}
}
$query->connective = $this->connective;
$query->distinct = true;
$model->extend('augmentSQL', $query);
if($this->connective != "AND") throw new Exception("SearchContext connective '$this->connective' not supported after ORM-rewrite.");
return $query;
}
@ -169,17 +164,8 @@ class SearchContext extends Object {
public function getResults($searchParams, $sort = false, $limit = false) {
$searchParams = array_filter($searchParams, array($this,'clearEmptySearchFields'));
$query = $this->getQuery($searchParams, $sort, $limit);
// use if a raw SQL query is needed
$results = new DataObjectSet();
foreach($query->execute() as $row) {
$className = $row['RecordClassName'];
$results->push(new $className($row));
}
return $results;
//
//return DataObject::get($this->modelClass, $query->getFilter(), "", "", $limit);
// getQuery actually returns a DataList
return $this->getQuery($searchParams, $sort, $limit);
}
/**
@ -255,7 +241,7 @@ class SearchContext extends Object {
/**
* Apply a list of searchable fields to the current search context.
*
* @param FieldSet $fields
* @param FieldList $fields
*/
public function setFields($fields) {
$this->fields = $fields;

View File

@ -23,8 +23,8 @@ class EndsWithFilter extends SearchFilter {
*
* @return unknown
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$query->where($this->getDbName() . " LIKE '%" . Convert::raw2sql($this->getValue()) . "'");
}

View File

@ -20,8 +20,8 @@ class ExactMatchFilter extends SearchFilter {
*
* @return unknown
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
return $query->where(sprintf(
"%s = '%s'",
$this->getDbName(),

View File

@ -14,9 +14,8 @@
*/
class ExactMatchMultiFilter extends SearchFilter {
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
// hack
// PREVIOUS $values = explode(',',$this->getValue());
$values = array();

View File

@ -27,7 +27,7 @@
*/
class FulltextFilter extends SearchFilter {
public function apply(SQLQuery $query) {
public function apply(DataQuery $query) {
$query->where(sprintf(
"MATCH (%s) AGAINST ('%s')",
$this->getDbName(),

View File

@ -12,8 +12,8 @@ class GreaterThanFilter extends SearchFilter {
/**
* @return $query
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
return $query->where(sprintf(
"%s > '%s'",
$this->getDbName(),

View File

@ -12,8 +12,8 @@ class LessThanFilter extends SearchFilter {
/**
* @return $query
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
return $query->where(sprintf(
"%s < '%s'",
$this->getDbName(),

View File

@ -7,7 +7,7 @@
*/
class NegationFilter extends SearchFilter {
public function apply(SQLQuery $query) {
public function apply(DataQuery $query) {
return $query->where(sprintf(
"%s != '%s'",
$this->getDbName(),

View File

@ -12,8 +12,8 @@
*/
class PartialMatchFilter extends SearchFilter {
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
return $query->where(sprintf(
"%s LIKE '%%%s%%'",
$this->getDbName(),

View File

@ -150,95 +150,6 @@ abstract class SearchFilter extends Object {
return $dbField->RAW();
}
/**
* Traverse the relationship fields, and add the table
* mappings to the query object state. This has to be called
* in any overloaded {@link SearchFilter->apply()} methods manually.
*
* @todo try to make this implicitly triggered so it doesn't have to be manually called in child filters
* @param SQLQuery $query
* @return SQLQuery
*/
function applyRelation($query) {
if (is_array($this->relation)) {
foreach($this->relation as $rel) {
$model = singleton($this->model);
if ($component = $model->has_one($rel)) {
if(!$query->isJoinedTo($component)) {
$foreignKey = $model->getReverseAssociation($component);
$query->leftJoin($component, "\"$component\".\"ID\" = \"{$this->model}\".\"{$foreignKey}ID\"");
/**
* add join clause to the component's ancestry classes so that the search filter could search on its
* ancester fields.
*/
$ancestry = ClassInfo::ancestry($component, true);
if(!empty($ancestry)){
$ancestry = array_reverse($ancestry);
foreach($ancestry as $ancestor){
if($ancestor != $component){
$query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\"");
$component=$ancestor;
}
}
}
}
$this->model = $component;
} elseif ($component = $model->has_many($rel)) {
if(!$query->isJoinedTo($component)) {
$ancestry = $model->getClassAncestry();
$foreignKey = $model->getRemoteJoinField($rel);
$query->leftJoin($component, "\"$component\".\"{$foreignKey}\" = \"{$ancestry[0]}\".\"ID\"");
/**
* add join clause to the component's ancestry classes so that the search filter could search on its
* ancestor fields.
*/
$ancestry = ClassInfo::ancestry($component, true);
if(!empty($ancestry)){
$ancestry = array_reverse($ancestry);
foreach($ancestry as $ancestor){
if($ancestor != $component){
$query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\"");
$component=$ancestor;
}
}
}
}
$this->model = $component;
} elseif ($component = $model->many_many($rel)) {
list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component;
$parentBaseClass = ClassInfo::baseDataClass($parentClass);
$componentBaseClass = ClassInfo::baseDataClass($componentClass);
$query->innerJoin($relationTable, "\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\"");
$query->leftJoin($componentBaseClass, "\"$relationTable\".\"$componentField\" = \"$componentBaseClass\".\"ID\"");
if(ClassInfo::hasTable($componentClass)) {
$query->leftJoin($componentClass, "\"$relationTable\".\"$componentField\" = \"$componentClass\".\"ID\"");
}
$this->model = $componentClass;
// Experimental support for user-defined relationships via a "(relName)Query" method
// This will likely be dropped in 2.4 for a system that makes use of Lazy Data Lists.
} elseif($model->hasMethod($rel.'Query')) {
// Get the query representing the join - it should have "$ID" in the filter
$newQuery = $model->{"{$rel}Query"}();
if($newQuery) {
// Get the table to join to
//DATABASE ABSTRACTION: I don't think we need this line anymore:
$newModel = str_replace('`','',array_shift($newQuery->from));
// Get the filter to use on the join
$ancestry = $model->getClassAncestry();
$newFilter = "(" . str_replace('$ID', "\"{$ancestry[0]}\".\"ID\"" , implode(") AND (", $newQuery->where) ) . ")";
$query->leftJoin($newModel, $newFilter);
$this->model = $newModel;
} else {
$this->name = "NULL";
return;
}
}
}
}
return $query;
}
/**
* Apply filter criteria to a SQL query.
@ -246,7 +157,7 @@ abstract class SearchFilter extends Object {
* @param SQLQuery $query
* @return SQLQuery
*/
abstract public function apply(SQLQuery $query);
abstract public function apply(DataQuery $query);
/**
* Determines if a field has a value,

View File

@ -23,8 +23,8 @@ class StartsWithFilter extends SearchFilter {
*
* @return unknown
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$query->where($this->getDbName() . " LIKE '" . Convert::raw2sql($this->getValue()) . "%'");
}

View File

@ -14,8 +14,8 @@
*/
class StartsWithMultiFilter extends SearchFilter {
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$values = explode(',', $this->getValue());
foreach($values as $value) {

View File

@ -12,7 +12,7 @@
*/
class SubstringFilter extends SearchFilter {
public function apply(SQLQuery $query) {
public function apply(DataQuery $query) {
return $query->where(sprintf(
"LOCATE('%s', %s) != 0",
Convert::raw2sql($this->getValue()),

View File

@ -25,7 +25,7 @@ class WithinRangeFilter extends SearchFilter {
$this->max = $max;
}
function apply(SQLQuery $query) {
function apply(DataQuery $query) {
$query->where(sprintf(
"%s >= %s AND %s <= %s",
$this->getDbName(),

View File

@ -27,7 +27,7 @@ class ChangePasswordForm extends Form {
}
if(!$fields) {
$fields = new FieldSet();
$fields = new FieldList();
// Security/changepassword?h=XXX redirects to Security/changepassword
// without GET parameter to avoid potential HTTP referer leakage.
@ -40,7 +40,7 @@ class ChangePasswordForm extends Form {
$fields->push(new PasswordField("NewPassword2", _t('Member.CONFIRMNEWPASSWORD', "Confirm New Password")));
}
if(!$actions) {
$actions = new FieldSet(
$actions = new FieldList(
new FormAction("doChangePassword", _t('Member.BUTTONCHANGEPASSWORD', "Change Password"))
);
}

View File

@ -42,7 +42,7 @@ class Group extends DataObject {
}
function getAllChildren() {
$doSet = new DataObjectSet();
$doSet = new ArrayList();
if ($children = DataObject::get('Group', '"ParentID" = '.$this->ID)) {
foreach($children as $child) {
@ -62,7 +62,7 @@ class Group extends DataObject {
public function getCMSFields() {
Requirements::javascript(SAPPHIRE_DIR . '/javascript/PermissionCheckboxSetField.js');
$fields = new FieldSet(
$fields = new FieldList(
new TabSet("Root",
new Tab('Members', _t('SecurityAdmin.MEMBERS', 'Members'),
new TextField("Title", $this->fieldLabel('Title')),
@ -137,7 +137,7 @@ class Group extends DataObject {
// Add roles (and disable all checkboxes for inherited roles)
$allRoles = Permission::check('ADMIN') ? DataObject::get('PermissionRole') : DataObject::get('PermissionRole', 'OnlyAdminCanApply = 0');
$groupRoles = $this->Roles();
$inheritedRoles = new DataObjectSet();
$inheritedRoles = new ArrayList();
$ancestors = $this->getAncestors();
foreach($ancestors as $ancestor) {
$ancestorRoles = $ancestor->Roles();
@ -153,9 +153,7 @@ class Group extends DataObject {
}
$memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd'));
$memberList->setParentClass('Group');
$memberList->setPopupCaption(_t('SecurityAdmin.VIEWUSER', 'View User'));
$memberList->setRelationAutoSetting(false);
$fields->push($idField = new HiddenField("ID"));
@ -207,38 +205,20 @@ class Group extends DataObject {
* @param $join string SQL
* @return ComponentSet
*/
public function Members($limit = "", $offset = "", $filter = "", $sort = "", $join = "") {
$table = "Group_Members";
if($filter) $filter = is_array($filter) ? $filter : array($filter);
public function Members($filter = "", $sort = "", $join = "", $limit = "") {
// Get a DataList of the relevant groups
$groups = DataList::create("Group")->byIDs($this->collateFamilyIDs());
if( is_numeric( $limit ) ) {
if( is_numeric( $offset ) )
$limit = "$limit OFFSET $offset";
else
$limit = "$limit OFFSET 0";
} else {
$limit = "";
// Call the relation method on the DataList to get the members from all the groups
return $groups->relation('DirectMembers')
->where($filter)->sort($sort)->join($join)->limit($limit);
}
// Get all of groups that this group contains
$groupFamily = implode(", ", $this->collateFamilyIDs());
$filter[] = "\"$table\".\"GroupID\" IN ($groupFamily)";
$join .= " INNER JOIN \"$table\" ON \"$table\".\"MemberID\" = \"Member\".\"ID\"" . Convert::raw2sql($join);
$result = singleton("Member")->instance_get(
$filter,
$sort,
$join,
$limit,
"ComponentSet" // datatype
);
if(!$result) $result = new ComponentSet();
$result->setComponentInfo("many-to-many", $this, "Group", $table, "Member");
foreach($result as $item) $item->GroupID = $this->ID;
return $result;
/**
* Return only the members directly added to this group
*/
public function DirectMembers() {
return $this->getManyManyComponents('Members');
}
public function map($filter = "", $sort = "", $blank="") {
@ -419,7 +399,7 @@ class Group extends DataObject {
$children = $extInstance->AllChildrenIncludingDeleted();
$extInstance->clearOwner();
$filteredChildren = new DataObjectSet();
$filteredChildren = new ArrayList();
if($children) foreach($children as $child) {
if($child->canView()) $filteredChildren->push($child);
@ -461,7 +441,7 @@ class Group extends DataObject {
// Add default author group if no other group exists
$allGroups = DataObject::get('Group');
if(!$allGroups) {
if(!$allGroups->count()) {
$authorGroup = new Group();
$authorGroup->Code = 'content-authors';
$authorGroup->Title = _t('Group.DefaultGroupTitleContentAuthors', 'Content Authors');
@ -476,7 +456,7 @@ class Group extends DataObject {
// Add default admin group if none with permission code ADMIN exists
$adminGroups = Permission::get_groups_by_permission('ADMIN');
if(!$adminGroups) {
if(!$adminGroups->count()) {
$adminGroup = new Group();
$adminGroup->Code = 'administrators';
$adminGroup->Title = _t('Group.DefaultGroupTitleAdministrators', 'Administrators');

View File

@ -139,16 +139,15 @@ class Member extends DataObject {
// Default groups should've been built by Group->requireDefaultRecords() already
// Find or create ADMIN group
$adminGroups = Permission::get_groups_by_permission('ADMIN');
if(!$adminGroups) {
$adminGroup = Permission::get_groups_by_permission('ADMIN')->First();
if(!$adminGroup) {
singleton('Group')->requireDefaultRecords();
$adminGroups = Permission::get_groups_by_permission('ADMIN');
$adminGroup = Permission::get_groups_by_permission('ADMIN')->First();
}
$adminGroup = $adminGroups->First();
// Add a default administrator to the first ADMIN group found (most likely the default
// group created through Group->requireDefaultRecords()).
$admins = Permission::get_members_by_permission('ADMIN');
$admins = Permission::get_members_by_permission('ADMIN')->First();
if(!$admins) {
// Leave 'Email' and 'Password' are not set to avoid creating
// persistent logins in the database. See Security::setDefaultAdmin().
@ -942,40 +941,22 @@ class Member extends DataObject {
* Get a "many-to-many" map that holds for all members their group
* memberships
*
* @return Member_GroupSet Returns a map holding for all members their
* group memberships.
* @todo Push all this logic into Member_GroupSet's getIterator()?
*/
public function Groups() {
$groups = $this->getManyManyComponents("Groups");
$groupIDs = $groups->column();
$collatedGroups = array();
$groups = new Member_GroupSet('Group', 'Group_Members', 'GroupID', 'MemberID');
if($this->ID) $groups->setForeignID($this->ID);
if($groups) {
foreach($groups as $group) {
$collatedGroups = array_merge((array)$collatedGroups, $group->collateAncestorIDs());
}
}
$table = "Group_Members";
if(count($collatedGroups) > 0) {
$collatedGroups = implode(", ", array_unique($collatedGroups));
$unfilteredGroups = singleton('Group')->instance_get("\"Group\".\"ID\" IN ($collatedGroups)", "\"Group\".\"ID\"", "", "", "Member_GroupSet");
$result = new ComponentSet();
// Only include groups where allowedIPAddress() returns true
// Filter out groups that aren't allowed from this IP
$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
foreach($unfilteredGroups as $group) {
if($group->allowedIPAddress($ip)) $result->push($group);
}
} else {
$result = new Member_GroupSet();
$disallowedGroups = array();
foreach($groups as $group) {
if(!$group->allowedIPAddress($ip)) $disallowedGroups[] = $groupID;
}
if($disallowedGroups) $group->where("\"Group\".\"ID\" NOT IN (" .
implode(',',$disallowedGroups) . ")");
$result->setComponentInfo("many-to-many", $this, "Member", $table, "Group");
return $result;
return $groups;
}
@ -1405,38 +1386,52 @@ class Member extends DataObject {
}
/**
* Special kind of {@link ComponentSet} that has special methods for
* manipulating a user's membership
* Represents a set of Groups attached to a member.
* Handles the hierarchy logic.
* @package sapphire
* @subpackage security
*/
class Member_GroupSet extends ComponentSet {
class Member_GroupSet extends ManyManyList {
function __construct($dataClass, $joinTable, $localKey, $foreignKey, $extraFields = array()) {
// Bypass the many-many constructor
DataList::__construct($dataClass);
$this->joinTable = $joinTable;
$this->localKey = $localKey;
$this->foreignKey = $foreignKey;
$this->extraFields = $extraFields;
}
/**
* Control group membership with a number of checkboxes.
* - If the checkbox fields are present in $data, then the member will be
* added to the group with the same codename.
* - If the checkbox fields are *NOT* present in $data, then the member
* will be removed from the group with the same codename.
*
* @param array $checkboxes An array list of the checkbox fieldnames (only
* values are used). E.g. array(0, 1, 2)
* @param array $data The form data. Uually in the format array(0 => 2)
* (just pass the checkbox data from your form)
* Link this group set to a specific member.
*/
public function setForeignID($id) {
// Turn a 1-element array into a simple value
if(is_array($id) && sizeof($id) == 1) $id = reset($id);
$this->foreignID = $id;
// Find directly applied groups
$manymanyFilter = $this->foreignIDFilter();
$groupIDs = DB::query('SELECT "GroupID" FROM Group_Members WHERE ' . $manymanyFilter)->column();
// Get all ancestors
$allGroupIDs = array();
while($groupIDs) {
$allGroupIDs = array_merge($allGroupIDs, $groupIDs);
$groupIDs = DataObject::get("Group")->byIDs($groupIDs)->column("ParentID");
$groupIDs = array_filter($groupIDs);
}
// Add a filter to this DataList
if($allGroupIDs) $this->byIDs($allGroupIDs);
else $this->byIDs(array(0));
}
/**
* @deprecated Use setByIdList() and/or a CheckboxSetField
*/
function setByCheckboxes(array $checkboxes, array $data) {
foreach($checkboxes as $checkbox) {
if($data[$checkbox]) {
$add[] = $checkbox;
} else {
$remove[] = $checkbox;
}
}
if($add)
$this->addManyByCodename($add);
if($remove)
$this->removeManyByCodename($remove);
user_error("Member_GroupSet is deprecated and no longer works", E_USER_WARNING);
}
@ -1506,108 +1501,45 @@ class Member_GroupSet extends ComponentSet {
/**
* Adds this member to the groups based on the group IDs
*
* @param array $ids Group identifiers.
* @deprecated Use DataList::addMany
*/
function addManyByGroupID($groupIds){
$groups = $this->getGroupsFromIDs($groupIds);
if($groups) {
foreach($groups as $group) {
$this->add($group);
}
}
function addManyByGroupID($ids){
user_error('addManyByGroupID is deprecated, use addMany', E_USER_NOTICE);
return $this->addMany($ids);
}
/**
* Removes the member from many groups based on the group IDs
*
* @param array $ids Group identifiers.
* @deprecated Use DataList::removeMany
*/
function removeManyByGroupID($groupIds) {
$groups = $this->getGroupsFromIDs($groupIds);
if($groups) {
foreach($groups as $group) {
$this->remove($group);
}
}
user_error('removeManyByGroupID is deprecated, use removeMany', E_USER_NOTICE);
return $this->removeMany($ids);
}
/**
* Returns the groups from an array of group IDs
*
* @param array $ids Group identifiers.
* @return mixed Returns the groups from the array of Group IDs.
* @deprecated Use DataObject::get("Group")->byIds()
*/
function getGroupsFromIDs($ids){
if($ids && count($ids) > 1) {
return DataObject::get("Group", "\"ID\" IN (" . implode(",", $ids) . ")");
} else {
return DataObject::get_by_id("Group", $ids[0]);
}
function getGroupsFromIDs($ids) {
user_error('getGroupsFromIDs is deprecated, use DataObject::get("Group")->byIds()', E_USER_NOTICE);
return DataObject::get("Group")->byIDs($ids);
}
/**
* Adds this member to the groups based on the group codenames
*
* @param array $codenames Group codenames
* @deprecated Group.Code is deprecated
*/
function addManyByCodename($codenames) {
$groups = $this->codenamesToGroups($codenames);
if($groups) {
foreach($groups as $group){
$this->add($group);
}
}
user_error("addManyByCodename is deprecated and no longer works", E_USER_WARNING);
}
/**
* Removes this member from the groups based on the group codenames
*
* @param array $codenames Group codenames
* @deprecated Group.Code is deprecated
*/
function removeManyByCodename($codenames) {
$groups = $this->codenamesToGroups($codenames);
if($groups) {
foreach($groups as $group) {
$this->remove($group);
}
}
}
/**
* Helper function to return the appropriate groups via a codenames
*
* @param array $codenames Group codenames
* @return array Returns the the appropriate groups.
*/
protected function codenamesToGroups($codenames) {
$list = "'" . implode("', '", $codenames) . "'";
$output = DataObject::get("Group", "\"Code\" IN ($list)");
// Some are missing - throw warnings
if(!$output || ($output->Count() != sizeof($list))) {
foreach($codenames as $codename)
$missing[$codename] = $codename;
if($output) {
foreach($output as $record)
unset($missing[$record->Code]);
}
if($missing)
user_error("The following group-codes aren't matched to any groups: " .
implode(", ", $missing) .
". You probably need to link up the correct group codes in phpMyAdmin",
E_USER_WARNING);
}
return $output;
user_error("removeManyByCodename is deprecated and no longer works", E_USER_WARNING);
}
}
@ -1626,7 +1558,7 @@ class Member_ProfileForm extends Form {
$fields = $member->getCMSFields();
$fields->push(new HiddenField('ID','ID',$member->ID));
$actions = new FieldSet(
$actions = new FieldList(
new FormAction('dosave',_t('CMSMain.SAVE', 'Save'))
);

View File

@ -50,16 +50,16 @@ class MemberLoginForm extends LoginForm {
}
if($checkCurrentUser && Member::currentUser() && Member::logged_in_session_exists()) {
$fields = new FieldSet(
$fields = new FieldList(
new HiddenField("AuthenticationMethod", null, $this->authenticator_class, $this)
);
$actions = new FieldSet(
$actions = new FieldList(
new FormAction("logout", _t('Member.BUTTONLOGINOTHER', "Log in as someone else"))
);
} else {
if(!$fields) {
$label=singleton('Member')->fieldLabel(Member::get_unique_identifier_field());
$fields = new FieldSet(
$fields = new FieldList(
new HiddenField("AuthenticationMethod", null, $this->authenticator_class, $this),
//Regardless of what the unique identifer field is (usually 'Email'), it will be held in the 'Email' value, below:
new TextField("Email", $label, Session::get('SessionForms.MemberLoginForm.Email'), null, $this),
@ -73,7 +73,7 @@ class MemberLoginForm extends LoginForm {
}
}
if(!$actions) {
$actions = new FieldSet(
$actions = new FieldList(
new FormAction('dologin', _t('Member.BUTTONLOGIN', "Log in")),
new LiteralField(
'forgotPassword',

View File

@ -379,7 +379,7 @@ class Permission extends DataObject {
*/
public static function get_members_by_permission($code) {
$toplevelGroups = self::get_groups_by_permission($code);
if (!$toplevelGroups) return false;
if (!$toplevelGroups) return new ArrayList();
$groupIDs = array();
foreach($toplevelGroups as $group) {
@ -389,8 +389,7 @@ class Permission extends DataObject {
}
}
if(!count($groupIDs))
return false;
if(!count($groupIDs)) return new ArrayList();
$members = DataObject::get(
Object::getCustomClass('Member'),

View File

@ -42,10 +42,10 @@ class PermissionCheckboxSetField extends FormField {
$this->filterField = $filterField;
$this->managedClass = $managedClass;
if(is_a($records, 'DataObjectSet')) {
if($records instanceof SS_List) {
$this->records = $records;
} elseif(is_a($records, 'DataObject')) {
$this->records = new DataObjectSet($records);
} elseif($records instanceof Group) {
$this->records = new ArrayList(array($records));
} elseif($records) {
throw new InvalidArgumentException('$record should be either a Group record, or a DataObjectSet of Group records');
}
@ -76,7 +76,7 @@ class PermissionCheckboxSetField extends FormField {
$uninheritedCodes = array();
$inheritedCodes = array();
$records = ($this->records) ? $this->records : new DataObjectSet();
$records = ($this->records) ? $this->records : new ArrayList();
// Get existing values from the form record (assuming the formfield name is a join field on the record)
if(is_object($this->form)) {
@ -173,6 +173,7 @@ class PermissionCheckboxSetField extends FormField {
$options .= "<li><h5>$categoryName</h5></li>";
foreach($permissions as $code => $permission) {
if(in_array($code, $this->hiddenPermissions)) continue;
if(in_array($code, Permission::$hidden_permissions)) continue;
$value = $permission['name'];

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 {
@ -469,10 +470,10 @@ class Security extends Controller {
return Object::create('MemberLoginForm',
$this,
'LostPasswordForm',
new FieldSet(
new FieldList(
new EmailField('Email', _t('Member.EMAIL', 'Email'))
),
new FieldSet(
new FieldList(
new FormAction(
'forgotPassword',
_t('Security.BUTTONSEND', 'Send me the password reset link')
@ -668,32 +669,30 @@ class Security extends Controller {
Subsite::changeSubsite(0);
}
$member = null;
// find a group with ADMIN permission
$adminGroup = DataObject::get('Group',
"\"Permission\".\"Code\" = 'ADMIN'",
"\"Group\".\"ID\"",
"JOIN \"Permission\" ON \"Group\".\"ID\"=\"Permission\".\"GroupID\"",
'1');
'1')->First();
if(is_callable('Subsite::changeSubsite')) {
Subsite::changeSubsite($origSubsite);
}
if ($adminGroup) {
$adminGroup = $adminGroup->First();
if($adminGroup->Members()->First()) {
if ($adminGroup) {
$member = $adminGroup->Members()->First();
}
}
if(!$adminGroup) {
singleton('Group')->requireDefaultRecords();
}
if(!isset($member)) {
if(!$member) {
singleton('Member')->requireDefaultRecords();
$members = Permission::get_members_by_permission('ADMIN');
$member = $members->First();
$member = Permission::get_members_by_permission('ADMIN')->First();
}
return $member;

View File

@ -164,7 +164,7 @@ class SecurityToken extends Object {
* on the returned {@link HiddenField}, you'll need to take
* care of this yourself.
*
* @param FieldSet $fieldset
* @param FieldList $fieldset
* @return HiddenField|false
*/
function updateFieldSet(&$fieldset) {
@ -234,7 +234,7 @@ class NullSecurityToken extends SecurityToken {
}
/**
* @param FieldSet $fieldset
* @param FieldList $fieldset
* @return false
*/
function updateFieldSet(&$fieldset) {

View File

@ -8,7 +8,7 @@ class RSSFeedTest extends SapphireTest {
protected static $original_host;
function testRSSFeed() {
$list = new DataObjectSet();
$list = new ArrayList();
$list->push(new RSSFeedTest_ItemA());
$list->push(new RSSFeedTest_ItemB());
$list->push(new RSSFeedTest_ItemC());

View File

@ -121,7 +121,7 @@ class RestfulServerTest extends SapphireTest {
$url = "/api/v1/RestfulServerTest_Author/" . $author4->ID . '/RelatedAuthors';
$response = Director::test($url, null, null, 'GET');
$this->assertEquals($response->getStatusCode(), 200);
$this->assertEquals(200, $response->getStatusCode());
$arr = Convert::xml2array($response->getBody());
$authorsArr = $arr['RestfulServerTest_Author'];

View File

@ -298,10 +298,10 @@ class RequestHandlingTest_Controller extends Controller implements TestOnly {
}
function TestForm() {
return new RequestHandlingTest_Form($this, "TestForm", new FieldSet(
return new RequestHandlingTest_Form($this, "TestForm", new FieldList(
new RequestHandlingTest_FormField("MyField"),
new RequestHandlingTest_SubclassedFormField("SubclassedField")
), new FieldSet(
), new FieldList(
new FormAction("myAction")
));
}
@ -350,10 +350,10 @@ class RequestHandlingTest_FormActionController extends Controller {
return new Form(
$this,
"Form",
new FieldSet(
new FieldList(
new TextField("MyField")
),
new FieldSet(
new FieldList(
new FormAction("formaction"),
new FormAction('formactionInAllowedActions')
)
@ -472,8 +472,8 @@ class RequestHandlingTest_ControllerFormWithAllowedActions extends Controller im
return new RequestHandlingTest_FormWithAllowedActions(
$this,
'Form',
new FieldSet(),
new FieldSet(
new FieldList(),
new FieldList(
new FormAction('allowedformaction'),
new FormAction('disallowedformaction') // disallowed through $allowed_actions in form
)

View File

@ -117,8 +117,8 @@ class CheckboxSetFieldTest extends SapphireTest {
$form = new Form(
new Controller(),
'Form',
new FieldSet($field),
new FieldSet()
new FieldList($field),
new FieldList()
);
$form->loadDataFrom($articleWithTags);
$this->assertEquals(

View File

@ -100,24 +100,23 @@ class ComplexTableFieldTest_Controller extends Controller {
$playersField = new ComplexTableField(
$this,
'Players',
'ComplexTableFieldTest_Player',
$team->Players(),
ComplexTableFieldTest_Player::$summary_fields,
'getCMSFields'
);
$playersField->setParentClass('ComplexTableFieldTest_Team');
$form = new Form(
$this,
'ManyManyForm',
new FieldSet(
new FieldList(
new HiddenField('ID', '', $team->ID),
$playersField
),
new FieldSet(
new FieldList(
new FormAction('doSubmit', 'Submit')
)
);
$form->loadDataFrom($team);
$form->disableSecurityToken();
@ -130,24 +129,23 @@ class ComplexTableFieldTest_Controller extends Controller {
$sponsorsField = new ComplexTableField(
$this,
'Sponsors',
'ComplexTableFieldTest_Sponsor',
$team->Sponsors(),
ComplexTableFieldTest_Sponsor::$summary_fields,
'getCMSFields'
);
$sponsorsField->setParentClass('ComplexTableFieldTest_Team');
$form = new Form(
$this,
'HasManyForm',
new FieldSet(
new FieldList(
new HiddenField('ID', '', $team->ID),
$sponsorsField
),
new FieldSet(
new FieldList(
new FormAction('doSubmit', 'Submit')
)
);
$form->loadDataFrom($team);
$form->disableSecurityToken();

View File

@ -6,6 +6,7 @@ ComplexTableFieldTest_Player:
ComplexTableFieldTest_Team:
t1:
Name: The Awesome People
Players: =>ComplexTableFieldTest_Player.p1,=>ComplexTableFieldTest_Player.p2
t2:
Name: Incredible Four
ComplexTableFieldTest_Sponsor:

View File

@ -22,10 +22,10 @@ class DatetimeFieldTest extends SapphireTest {
$form = new Form(
new Controller(),
'Form',
new FieldSet(
new FieldList(
$f = new DatetimeField('MyDatetime', null)
),
new FieldSet(
new FieldList(
new FormAction('doSubmit')
)
);

View File

@ -1,34 +1,34 @@
<?php
/**
* Tests for FieldSet
* Tests for FieldList
*
* @package sapphire
* @subpackage tests
*
* @todo test for {@link FieldSet->setValues()}. Need to check
* @todo test for {@link FieldList->setValues()}. Need to check
* that the values that were set are the correct ones given back.
* @todo test for {@link FieldSet->transform()} and {@link FieldSet->makeReadonly()}.
* Need to ensure that it correctly transforms the FieldSet object.
* @todo test for {@link FieldSet->HiddenFields()}. Need to check
* @todo test for {@link FieldList->transform()} and {@link FieldList->makeReadonly()}.
* Need to ensure that it correctly transforms the FieldList object.
* @todo test for {@link FieldList->HiddenFields()}. Need to check
* the fields returned are the correct HiddenField objects for a
* given FieldSet instance.
* @todo test for {@link FieldSet->dataFields()}.
* @todo test for {@link FieldSet->findOrMakeTab()}.
* given FieldList instance.
* @todo test for {@link FieldList->dataFields()}.
* @todo test for {@link FieldList->findOrMakeTab()}.
* @todo the same as above with insertBefore() and insertAfter()
*
*/
class FieldSetTest extends SapphireTest {
class FieldListTest extends SapphireTest {
/**
* Test adding a field to a tab in a set.
*/
function testAddFieldToTab() {
$fields = new FieldSet();
$fields = new FieldList();
$tab = new Tab('Root');
$fields->push($tab);
/* We add field objects to the FieldSet, using two different methods */
/* We add field objects to the FieldList, using two different methods */
$fields->addFieldToTab('Root', new TextField('Country'));
$fields->addFieldsToTab('Root', array(
new EmailField('Email'),
@ -53,7 +53,7 @@ class FieldSetTest extends SapphireTest {
* Test removing a single field from a tab in a set.
*/
function testRemoveSingleFieldFromTab() {
$fields = new FieldSet();
$fields = new FieldList();
$tab = new Tab('Root');
$fields->push($tab);
@ -71,7 +71,7 @@ class FieldSetTest extends SapphireTest {
}
function testRemoveTab() {
$fields = new FieldSet(new TabSet(
$fields = new FieldList(new TabSet(
'Root',
$tab1 = new Tab('Tab1'),
$tab2 = new Tab('Tab2'),
@ -85,12 +85,12 @@ class FieldSetTest extends SapphireTest {
}
function testHasTabSet() {
$untabbedFields = new FieldSet(
$untabbedFields = new FieldList(
new TextField('Field1')
);
$this->assertFalse($untabbedFields->hasTabSet());
$tabbedFields = new FieldSet(
$tabbedFields = new FieldList(
new TabSet('Root',
new Tab('Tab1')
)
@ -102,7 +102,7 @@ class FieldSetTest extends SapphireTest {
* Test removing an array of fields from a tab in a set.
*/
function testRemoveMultipleFieldsFromTab() {
$fields = new FieldSet();
$fields = new FieldList();
$tab = new Tab('Root');
$fields->push($tab);
@ -131,9 +131,9 @@ class FieldSetTest extends SapphireTest {
* Test removing a field from a set by it's name.
*/
function testRemoveFieldByName() {
$fields = new FieldSet();
$fields = new FieldList();
/* First of all, we add a field into our FieldSet object */
/* First of all, we add a field into our FieldList object */
$fields->push(new TextField('Name', 'Your name'));
/* We have 1 field in our set now */
@ -150,7 +150,7 @@ class FieldSetTest extends SapphireTest {
* Test replacing a field with another one.
*/
function testReplaceField() {
$fields = new FieldSet();
$fields = new FieldList();
$tab = new Tab('Root');
$fields->push($tab);
@ -168,7 +168,7 @@ class FieldSetTest extends SapphireTest {
}
function testRenameField() {
$fields = new FieldSet();
$fields = new FieldList();
$nameField = new TextField('Name', 'Before title');
$fields->push($nameField);
@ -186,8 +186,8 @@ class FieldSetTest extends SapphireTest {
}
function testReplaceAFieldInADifferentTab() {
/* A FieldSet gets created with a TabSet and some field objects */
$fieldSet = new FieldSet(
/* A FieldList gets created with a TabSet and some field objects */
$FieldList = new FieldList(
new TabSet('Root', $main = new Tab('Main',
new TextField('A'),
new TextField('B')
@ -197,8 +197,8 @@ class FieldSetTest extends SapphireTest {
))
);
/* The field "A" gets added to the FieldSet we just created created */
$fieldSet->addFieldToTab('Root.Other', $newA = new TextField('A', 'New Title'));
/* The field "A" gets added to the FieldList we just created created */
$FieldList->addFieldToTab('Root.Other', $newA = new TextField('A', 'New Title'));
/* The field named "A" has been removed from the Main tab to make way for our new field named "A" in Other tab. */
$this->assertEquals(1, $main->Fields()->Count());
@ -209,7 +209,7 @@ class FieldSetTest extends SapphireTest {
* Test finding a field that's inside a tabset, within another tab.
*/
function testNestedTabsFindingFieldByName() {
$fields = new FieldSet();
$fields = new FieldList();
/* 2 tabs get created within a TabSet inside our set */
$tab = new TabSet('Root',
@ -241,7 +241,7 @@ class FieldSetTest extends SapphireTest {
}
function testTabTitles() {
$set = new FieldSet(
$set = new FieldList(
$rootTabSet = new TabSet('Root',
$tabSetWithoutTitle = new TabSet('TabSetWithoutTitle'),
$tabSetWithTitle = new TabSet('TabSetWithTitle', 'My TabSet Title',
@ -281,10 +281,10 @@ class FieldSetTest extends SapphireTest {
/**
* Test pushing a field to a set.
*
* This tests {@link FieldSet->push()}.
* This tests {@link FieldList->push()}.
*/
function testPushFieldToSet() {
$fields = new FieldSet();
$fields = new FieldList();
/* A field named Country is added to the set */
$fields->push(new TextField('Country'));
@ -310,10 +310,10 @@ class FieldSetTest extends SapphireTest {
/**
* Test inserting a field before another in a set.
*
* This tests {@link FieldSet->insertBefore()}.
* This tests {@link FieldList->insertBefore()}.
*/
function testInsertBeforeFieldToSet() {
$fields = new FieldSet();
$fields = new FieldList();
/* 3 fields are added to the set */
$fields->push(new TextField('Country'));
@ -333,11 +333,11 @@ class FieldSetTest extends SapphireTest {
$this->assertEquals(4, $fields->Count());
/* The position of the Title field is at number 3 */
$this->assertEquals(3, $fields->fieldByName('Title')->Pos());
$this->assertEquals('Title', $fields[2]->Name());
}
function testInsertBeforeMultipleFields() {
$fields = new FieldSet(
$fields = new FieldList(
$root = new TabSet("Root",
$main = new Tab("Main",
$a = new TextField("A"),
@ -363,7 +363,7 @@ class FieldSetTest extends SapphireTest {
* Test inserting a field after another in a set.
*/
function testInsertAfterFieldToSet() {
$fields = new FieldSet();
$fields = new FieldList();
/* 3 fields are added to the set */
$fields->push(new TextField('Country'));
@ -379,16 +379,16 @@ class FieldSetTest extends SapphireTest {
/* The field we just added actually exists in the set */
$this->assertNotNull($fields->dataFieldByName('Title'));
/* We now have 4 fields in the FieldSet */
/* We now have 4 fields in the FieldList */
$this->assertEquals(4, $fields->Count());
/* The position of the Title field should be at number 2 */
$this->assertEquals(2, $fields->fieldByName('Title')->Pos());
$this->assertEquals('Title', $fields[1]->Name());
}
function testRootFieldSet() {
/* Given a nested set of FormField, CompositeField, and FieldSet objects */
$fieldSet = new FieldSet(
function testrootFieldSet() {
/* Given a nested set of FormField, CompositeField, and FieldList objects */
$FieldList = new FieldList(
$root = new TabSet("Root",
$main = new Tab("Main",
$a = new TextField("A"),
@ -397,27 +397,27 @@ class FieldSetTest extends SapphireTest {
)
);
/* rootFieldSet() should always evaluate to the same object: the topmost fieldset */
$this->assertSame($fieldSet, $fieldSet->rootFieldSet());
$this->assertSame($fieldSet, $root->rootFieldSet());
$this->assertSame($fieldSet, $main->rootFieldSet());
$this->assertSame($fieldSet, $a->rootFieldSet());
$this->assertSame($fieldSet, $b->rootFieldSet());
/* rootFieldSet() should always evaluate to the same object: the topmost FieldList */
$this->assertSame($FieldList, $FieldList->rootFieldSet());
$this->assertSame($FieldList, $root->rootFieldSet());
$this->assertSame($FieldList, $main->rootFieldSet());
$this->assertSame($FieldList, $a->rootFieldSet());
$this->assertSame($FieldList, $b->rootFieldSet());
/* If we push additional fields, they should also have the same rootFieldSet() */
$root->push($other = new Tab("Other"));
$other->push($c = new TextField("C"));
$root->push($third = new Tab("Third", $d = new TextField("D")));
$this->assertSame($fieldSet, $other->rootFieldSet());
$this->assertSame($fieldSet, $third->rootFieldSet());
$this->assertSame($fieldSet, $c->rootFieldSet());
$this->assertSame($fieldSet, $d->rootFieldSet());
$this->assertSame($FieldList, $other->rootFieldSet());
$this->assertSame($FieldList, $third->rootFieldSet());
$this->assertSame($FieldList, $c->rootFieldSet());
$this->assertSame($FieldList, $d->rootFieldSet());
}
function testAddingDuplicateReplacesOldField() {
/* Given a nested set of FormField, CompositeField, and FieldSet objects */
$fieldSet = new FieldSet(
/* Given a nested set of FormField, CompositeField, and FieldList objects */
$FieldList = new FieldList(
$root = new TabSet("Root",
$main = new Tab("Main",
$a = new TextField("A"),
@ -430,27 +430,27 @@ class FieldSetTest extends SapphireTest {
$newA = new TextField("A", "New A");
$newB = new TextField("B", "New B");
$fieldSet->addFieldToTab("Root.Main", $newA);
$fieldSet->addFieldToTab("Root.Other", $newB);
$FieldList->addFieldToTab("Root.Main", $newA);
$FieldList->addFieldToTab("Root.Other", $newB);
$this->assertSame($newA, $fieldSet->dataFieldByName("A"));
$this->assertSame($newB, $fieldSet->dataFieldByName("B"));
$this->assertSame($newA, $FieldList->dataFieldByName("A"));
$this->assertSame($newB, $FieldList->dataFieldByName("B"));
$this->assertEquals(1, $main->Fields()->Count());
/* Pushing fields on the end of the field set should remove them from the tab */
$thirdA = new TextField("A", "Third A");
$thirdB = new TextField("B", "Third B");
$fieldSet->push($thirdA);
$fieldSet->push($thirdB);
$FieldList->push($thirdA);
$FieldList->push($thirdB);
$this->assertSame($thirdA, $fieldSet->fieldByName("A"));
$this->assertSame($thirdB, $fieldSet->fieldByName("B"));
$this->assertSame($thirdA, $FieldList->fieldByName("A"));
$this->assertSame($thirdB, $FieldList->fieldByName("B"));
$this->assertEquals(0, $main->Fields()->Count());
}
function testAddingFieldToNonExistentTabCreatesThatTab() {
$fieldSet = new FieldSet(
$FieldList = new FieldList(
$root = new TabSet("Root",
$main = new Tab("Main",
$a = new TextField("A")
@ -459,13 +459,13 @@ class FieldSetTest extends SapphireTest {
);
/* Add a field to a non-existent tab, and it will be created */
$fieldSet->addFieldToTab("Root.Other", $b = new TextField("B"));
$this->assertNotNull($fieldSet->fieldByName('Root')->fieldByName('Other'));
$this->assertSame($b, $fieldSet->fieldByName('Root')->fieldByName('Other')->Fields()->First());
$FieldList->addFieldToTab("Root.Other", $b = new TextField("B"));
$this->assertNotNull($FieldList->fieldByName('Root')->fieldByName('Other'));
$this->assertSame($b, $FieldList->fieldByName('Root')->fieldByName('Other')->Fields()->First());
}
function testAddingFieldToATabWithTheSameNameAsTheField() {
$fieldSet = new FieldSet(
$FieldList = new FieldList(
$root = new TabSet("Root",
$main = new Tab("Main",
$a = new TextField("A")
@ -475,13 +475,13 @@ class FieldSetTest extends SapphireTest {
/* If you have a tab with the same name as the field, then technically it's a duplicate. However, it's allowed because
tab isn't a data field. Only duplicate data fields are problematic */
$fieldSet->addFieldToTab("Root.MyName", $myName = new TextField("MyName"));
$this->assertNotNull($fieldSet->fieldByName('Root')->fieldByName('MyName'));
$this->assertSame($myName, $fieldSet->fieldByName('Root')->fieldByName('MyName')->Fields()->First());
$FieldList->addFieldToTab("Root.MyName", $myName = new TextField("MyName"));
$this->assertNotNull($FieldList->fieldByName('Root')->fieldByName('MyName'));
$this->assertSame($myName, $FieldList->fieldByName('Root')->fieldByName('MyName')->Fields()->First());
}
function testInsertBeforeWithNestedCompositeFields() {
$fieldSet = new FieldSet(
$FieldList = new FieldList(
new TextField('A_pre'),
new TextField('A'),
new TextField('A_post'),
@ -497,34 +497,34 @@ class FieldSetTest extends SapphireTest {
)
);
$fieldSet->insertBefore(
$FieldList->insertBefore(
$A_insertbefore = new TextField('A_insertbefore'),
'A'
);
$this->assertSame(
$A_insertbefore,
$fieldSet->dataFieldByName('A_insertbefore'),
'Field on toplevel fieldset can be inserted'
$FieldList->dataFieldByName('A_insertbefore'),
'Field on toplevel FieldList can be inserted'
);
$fieldSet->insertBefore(
$FieldList->insertBefore(
$B_insertbefore = new TextField('B_insertbefore'),
'B'
);
$this->assertSame(
$fieldSet->dataFieldByName('B_insertbefore'),
$FieldList->dataFieldByName('B_insertbefore'),
$B_insertbefore,
'Field on one nesting level fieldset can be inserted'
'Field on one nesting level FieldList can be inserted'
);
$fieldSet->insertBefore(
$FieldList->insertBefore(
$C_insertbefore = new TextField('C_insertbefore'),
'C'
);
$this->assertSame(
$fieldSet->dataFieldByName('C_insertbefore'),
$FieldList->dataFieldByName('C_insertbefore'),
$C_insertbefore,
'Field on two nesting levels fieldset can be inserted'
'Field on two nesting levels FieldList can be inserted'
);
}
@ -532,7 +532,7 @@ class FieldSetTest extends SapphireTest {
* @todo check actual placement of fields
*/
function testInsertBeforeWithNestedTabsets() {
$fieldSetA = new FieldSet(
$FieldListA = new FieldList(
$tabSetA = new TabSet('TabSet_A',
$tabA1 = new Tab('Tab_A1',
new TextField('A_pre'),
@ -549,7 +549,7 @@ class FieldSetTest extends SapphireTest {
'A'
);
$this->assertEquals(
$fieldSetA->dataFieldByName('A_insertbefore'),
$FieldListA->dataFieldByName('A_insertbefore'),
$A_insertbefore,
'Field on toplevel tab can be inserted'
);
@ -559,7 +559,7 @@ class FieldSetTest extends SapphireTest {
$this->assertEquals(2, $tabA1->fieldPosition('A'));
$this->assertEquals(3, $tabA1->fieldPosition('A_post'));
$fieldSetB = new FieldSet(
$FieldListB = new FieldList(
new TabSet('TabSet_A',
$tabsetB = new TabSet('TabSet_B',
$tabB1 = new Tab('Tab_B1',
@ -573,12 +573,12 @@ class FieldSetTest extends SapphireTest {
)
)
);
$fieldSetB->insertBefore(
$FieldListB->insertBefore(
$B_insertbefore = new TextField('B_insertbefore'),
'B'
);
$this->assertSame(
$fieldSetB->dataFieldByName('B_insertbefore'),
$FieldListB->dataFieldByName('B_insertbefore'),
$B_insertbefore,
'Field on nested tab can be inserted'
);
@ -589,7 +589,7 @@ class FieldSetTest extends SapphireTest {
}
function testInsertAfterWithNestedCompositeFields() {
$fieldSet = new FieldSet(
$FieldList = new FieldList(
new TextField('A_pre'),
new TextField('A'),
new TextField('A_post'),
@ -605,34 +605,34 @@ class FieldSetTest extends SapphireTest {
)
);
$fieldSet->insertAfter(
$FieldList->insertAfter(
$A_insertafter = new TextField('A_insertafter'),
'A'
);
$this->assertSame(
$A_insertafter,
$fieldSet->dataFieldByName('A_insertafter'),
'Field on toplevel fieldset can be inserted after'
$FieldList->dataFieldByName('A_insertafter'),
'Field on toplevel FieldList can be inserted after'
);
$fieldSet->insertAfter(
$FieldList->insertAfter(
$B_insertafter = new TextField('B_insertafter'),
'B'
);
$this->assertSame(
$fieldSet->dataFieldByName('B_insertafter'),
$FieldList->dataFieldByName('B_insertafter'),
$B_insertafter,
'Field on one nesting level fieldset can be inserted after'
'Field on one nesting level FieldList can be inserted after'
);
$fieldSet->insertAfter(
$FieldList->insertAfter(
$C_insertafter = new TextField('C_insertafter'),
'C'
);
$this->assertSame(
$fieldSet->dataFieldByName('C_insertafter'),
$FieldList->dataFieldByName('C_insertafter'),
$C_insertafter,
'Field on two nesting levels fieldset can be inserted after'
'Field on two nesting levels FieldList can be inserted after'
);
}
@ -640,7 +640,7 @@ class FieldSetTest extends SapphireTest {
* @todo check actual placement of fields
*/
function testInsertAfterWithNestedTabsets() {
$fieldSetA = new FieldSet(
$FieldListA = new FieldList(
$tabSetA = new TabSet('TabSet_A',
$tabA1 = new Tab('Tab_A1',
new TextField('A_pre'),
@ -657,7 +657,7 @@ class FieldSetTest extends SapphireTest {
'A'
);
$this->assertEquals(
$fieldSetA->dataFieldByName('A_insertafter'),
$FieldListA->dataFieldByName('A_insertafter'),
$A_insertafter,
'Field on toplevel tab can be inserted after'
);
@ -666,7 +666,7 @@ class FieldSetTest extends SapphireTest {
$this->assertEquals(2, $tabA1->fieldPosition('A_insertafter'));
$this->assertEquals(3, $tabA1->fieldPosition('A_post'));
$fieldSetB = new FieldSet(
$FieldListB = new FieldList(
new TabSet('TabSet_A',
$tabsetB = new TabSet('TabSet_B',
$tabB1 = new Tab('Tab_B1',
@ -680,12 +680,12 @@ class FieldSetTest extends SapphireTest {
)
)
);
$fieldSetB->insertAfter(
$FieldListB->insertAfter(
$B_insertafter = new TextField('B_insertafter'),
'B'
);
$this->assertSame(
$fieldSetB->dataFieldByName('B_insertafter'),
$FieldListB->dataFieldByName('B_insertafter'),
$B_insertafter,
'Field on nested tab can be inserted after'
);
@ -696,7 +696,7 @@ class FieldSetTest extends SapphireTest {
}
function testFieldPosition() {
$set = new FieldSet(
$set = new FieldList(
new TextField('A'),
new TextField('B'),
new TextField('C')
@ -716,17 +716,17 @@ class FieldSetTest extends SapphireTest {
}
function testMakeFieldReadonly() {
$fieldSet = new FieldSet(
$FieldList = new FieldList(
new TabSet('Root', new Tab('Main',
new TextField('A'),
new TextField('B')
)
));
$fieldSet->makeFieldReadonly('A');
$FieldList->makeFieldReadonly('A');
$this->assertTrue(
$fieldSet->dataFieldByName('A')->isReadonly(),
'Field nested inside a TabSet and FieldSet can be marked readonly by FieldSet->makeFieldReadonly()'
$FieldList->dataFieldByName('A')->isReadonly(),
'Field nested inside a TabSet and FieldList can be marked readonly by FieldList->makeFieldReadonly()'
);
}
}

View File

@ -12,10 +12,10 @@ class FileFieldTest extends FunctionalTest {
$form = new Form(
new Controller(),
'Form',
new FieldSet(
new FieldList(
$fileField = new FileField('cv', 'Upload your CV')
),
new FieldSet()
new FieldList()
);
$fileFieldValue = array(
'name' => 'aCV.txt',
@ -38,10 +38,10 @@ class FileFieldTest extends FunctionalTest {
$form = new Form(
new Controller(),
'Form',
new FieldSet(
new FieldList(
$fileField = new FileField('cv', 'Upload your CV')
),
new FieldSet(),
new FieldList(),
new RequiredFields('cv')
);
// All fields are filled but for some reason an error occured when uploading the file => fails

View File

@ -20,6 +20,9 @@ class FormScaffolderTest extends SapphireTest {
function testGetCMSFieldsSingleton() {
$fields = singleton('FormScaffolderTest_Article')->getCMSFields();
$form = new Form(new Controller(), 'TestForm', $fields, new FieldList());
$form->loadDataFrom(singleton('FormScaffolderTest_Article'));
$this->assertTrue($fields->hasTabSet(), 'getCMSFields() produces a TabSet');
$this->assertNotNull($fields->dataFieldByName('Title'), 'getCMSFields() includes db fields');
$this->assertNotNull($fields->dataFieldByName('Content'), 'getCMSFields() includes db fields');
@ -29,14 +32,22 @@ class FormScaffolderTest extends SapphireTest {
function testGetCMSFieldsInstance() {
$article1 = $this->objFromFixture('FormScaffolderTest_Article', 'article1');
$fields = $article1->getCMSFields();
$form = new Form(new Controller(), 'TestForm', $fields, new FieldList());
$form->loadDataFrom($article1);
$this->assertNotNull($fields->dataFieldByName('AuthorID'), 'getCMSFields() includes has_one fields on instances');
$this->assertNotNull($fields->dataFieldByName('Tags'), 'getCMSFields() includes many_many fields if ID is present on instances');
}
function testUpdateCMSFields() {
$article1 = $this->objFromFixture('FormScaffolderTest_Article', 'article1');
$fields = $article1->getCMSFields();
$form = new Form(new Controller(), 'TestForm', $fields, new FieldList());
$form->loadDataFrom($article1);
$this->assertNotNull(
$fields->dataFieldByName('AddedExtensionField'),
'getCMSFields() includes extended fields'
@ -45,18 +56,26 @@ class FormScaffolderTest extends SapphireTest {
function testRestrictCMSFields() {
$article1 = $this->objFromFixture('FormScaffolderTest_Article', 'article1');
$fields = $article1->scaffoldFormFields(array(
'restrictFields' => array('Title')
));
$form = new Form(new Controller(), 'TestForm', $fields, new FieldList());
$form->loadDataFrom($article1);
$this->assertNotNull($fields->dataFieldByName('Title'), 'scaffoldCMSFields() includes explitly defined "restrictFields"');
$this->assertNull($fields->dataFieldByName('Content'), 'getCMSFields() doesnt include fields left out in a "restrictFields" definition');
}
function testFieldClassesOnGetCMSFields() {
$article1 = $this->objFromFixture('FormScaffolderTest_Article', 'article1');
$fields = $article1->scaffoldFormFields(array(
'fieldClasses' => array('Title' => 'HtmlEditorField')
));
$form = new Form(new Controller(), 'TestForm', $fields, new FieldList());
$form->loadDataFrom($article1);
$this->assertNotNull(
$fields->dataFieldByName('Title')
);
@ -69,6 +88,9 @@ class FormScaffolderTest extends SapphireTest {
function testGetFormFields() {
$fields = singleton('FormScaffolderTest_Article')->getFrontEndFields();
$form = new Form(new Controller(), 'TestForm', $fields, new FieldList());
$form->loadDataFrom(singleton('FormScaffolderTest_Article'));
$this->assertFalse($fields->hasTabSet(), 'getFrontEndFields() doesnt produce a TabSet by default');
}
}

View File

@ -16,13 +16,13 @@ class FormTest extends FunctionalTest {
$form = new Form(
new Controller(),
'Form',
new FieldSet(
new FieldList(
new TextField('key1'),
new TextField('namespace[key2]'),
new TextField('namespace[key3][key4]'),
new TextField('othernamespace[key5][key6][key7]')
),
new FieldSet()
new FieldList()
);
// url would be ?key1=val1&namespace[key2]=val2&namespace[key3][key4]=val4&othernamespace[key5][key6][key7]=val7
@ -56,11 +56,11 @@ class FormTest extends FunctionalTest {
$form = new Form(
new Controller(),
'Form',
new FieldSet(
new FieldList(
new TextField('key1'),
new TextField('key2')
),
new FieldSet()
new FieldList()
);
$form->loadDataFrom(array(
'key1' => 'save',
@ -81,14 +81,14 @@ class FormTest extends FunctionalTest {
$form = new Form(
new Controller(),
'Form',
new FieldSet(
new FieldList(
new HeaderField('MyPlayerHeader','My Player'),
new TextField('Name'), // appears in both Player and Team
new TextareaField('Biography'),
new DateField('Birthday'),
new NumericField('BirthdayYear') // dynamic property
),
new FieldSet()
new FieldList()
);
$captainWithDetails = $this->objFromFixture('FormTest_Player', 'captainWithDetails');
@ -122,7 +122,7 @@ class FormTest extends FunctionalTest {
$form = new Form(
new Controller(),
'Form',
new FieldSet(
new FieldList(
new HeaderField('MyPlayerHeader','My Player'),
new TextField('Name'), // appears in both Player and Team
new TextareaField('Biography'),
@ -131,7 +131,7 @@ class FormTest extends FunctionalTest {
$unrelatedField = new TextField('UnrelatedFormField')
//new CheckboxSetField('Teams') // relation editing
),
new FieldSet()
new FieldList()
);
$unrelatedField->setValue("random value");
@ -329,8 +329,8 @@ class FormTest extends FunctionalTest {
return new Form(
new Controller(),
'Form',
new FieldSet(new TextField('key1')),
new FieldSet()
new FieldList(new TextField('key1')),
new FieldList()
);
}
@ -383,12 +383,12 @@ class FormTest_Controller extends Controller implements TestOnly {
$form = new Form(
$this,
'Form',
new FieldSet(
new FieldList(
new EmailField('Email'),
new TextField('SomeRequiredField'),
new CheckboxSetField('Boxes', null, array('1'=>'one','2'=>'two'))
),
new FieldSet(
new FieldList(
new FormAction('doSubmit')
),
new RequiredFields(
@ -407,10 +407,10 @@ class FormTest_Controller extends Controller implements TestOnly {
$form = new Form(
$this,
'FormWithSecurityToken',
new FieldSet(
new FieldList(
new EmailField('Email')
),
new FieldSet(
new FieldList(
new FormAction('doSubmit')
)
);
@ -444,10 +444,10 @@ class FormTest_ControllerWithSecurityToken extends Controller implements TestOnl
$form = new Form(
$this,
'Form',
new FieldSet(
new FieldList(
new EmailField('Email')
),
new FieldSet(
new FieldList(
new FormAction('doSubmit')
)
);

View File

@ -45,7 +45,7 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest {
function testDateFormatDefaultCheckedInFormField() {
$field = $this->createDateFormatFieldForMember($this->objFromFixture('Member', 'noformatmember'));
$field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldSet(), new FieldSet())); // fake form
$field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(), new FieldList())); // fake form
$parser = new CSSContentParser($field->Field());
$xmlArr = $parser->getBySelector('#Form_Form_DateFormat_MMM_d__y');
$this->assertEquals('checked', (string) $xmlArr[0]['checked']);
@ -53,7 +53,7 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest {
function testTimeFormatDefaultCheckedInFormField() {
$field = $this->createTimeFormatFieldForMember($this->objFromFixture('Member', 'noformatmember'));
$field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldSet(), new FieldSet())); // fake form
$field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(), new FieldList())); // fake form
$parser = new CSSContentParser($field->Field());
$xmlArr = $parser->getBySelector('#Form_Form_TimeFormat_h_mm_ss_a');
$this->assertEquals('checked', (string) $xmlArr[0]['checked']);
@ -63,7 +63,7 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest {
$member = $this->objFromFixture('Member', 'noformatmember');
$member->setField('DateFormat', 'MM/dd/yyyy');
$field = $this->createDateFormatFieldForMember($member);
$field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldSet(), new FieldSet())); // fake form
$field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(), new FieldList())); // fake form
$parser = new CSSContentParser($field->Field());
$xmlArr = $parser->getBySelector('#Form_Form_DateFormat_MM_dd_yyyy');
$this->assertEquals('checked', (string) $xmlArr[0]['checked']);
@ -73,7 +73,7 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest {
$member = $this->objFromFixture('Member', 'noformatmember');
$member->setField('DateFormat', 'dd MM yy');
$field = $this->createDateFormatFieldForMember($member);
$field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldSet(), new FieldSet())); // fake form
$field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(), new FieldList())); // fake form
$parser = new CSSContentParser($field->Field());
$xmlInputArr = $parser->getBySelector('.valCustom input');
$xmlPreview = $parser->getBySelector('.preview');

View File

@ -28,8 +28,8 @@ class TableFieldTest extends SapphireTest {
$form = new Form(
new TableFieldTest_Controller(),
"Form",
new FieldSet($tableField),
new FieldSet()
new FieldList($tableField),
new FieldList()
);
// Test Insert
@ -51,7 +51,7 @@ class TableFieldTest extends SapphireTest {
$tableField->saveInto($group);
// Let's check that the 2 permissions entries have been saved
$permissions = $group->Permissions()->toDropdownMap('Arg', 'Code');
$permissions = $group->Permissions()->map('Arg', 'Code');
$this->assertEquals(array(
1 => 'CustomPerm1',
2 => 'CustomPerm2',
@ -75,7 +75,7 @@ class TableFieldTest extends SapphireTest {
$tableField->saveInto($group);
// Let's check that the 2 existing permissions entries, and the 1 new one, have been saved
$permissions = $group->Permissions()->toDropdownMap('Arg', 'Code');
$permissions = $group->Permissions()->map('Arg', 'Code');
$this->assertEquals(array(
1 => 'CustomPerm1',
2 => 'CustomPerm2',
@ -106,11 +106,11 @@ class TableFieldTest extends SapphireTest {
$form = new Form(
new TableFieldTest_Controller(),
"Form",
new FieldSet($tableField),
new FieldSet()
new FieldList($tableField),
new FieldList()
);
$this->assertEquals($tableField->sourceItems()->Count(), 2);
$this->assertEquals(2, $tableField->sourceItems()->Count());
// We have replicated the array structure that the specific layout of the form generates.
$tableField->setValue(array(
@ -126,7 +126,7 @@ class TableFieldTest extends SapphireTest {
$tableField->saveInto($group);
// Let's check that the 2 permissions entries have been saved
$permissions = $group->Permissions()->toDropdownMap('Arg', 'Code');
$permissions = $group->Permissions()->map('Arg', 'Code');
$this->assertEquals(array(
101 => 'Perm1 Modified',
102 => 'Perm2 Modified',
@ -155,8 +155,8 @@ class TableFieldTest extends SapphireTest {
$form = new Form(
new TableFieldTest_Controller(),
"Form",
new FieldSet($tableField),
new FieldSet()
new FieldList($tableField),
new FieldList()
);
$this->assertContains($perm1->ID, $tableField->sourceItems()->column('ID'));
@ -166,7 +166,13 @@ class TableFieldTest extends SapphireTest {
$this->assertNotContains($perm1->ID, $tableField->sourceItems()->column('ID'));
}
/**
* Relation auto-setting is now the only option
*/
function testAutoRelationSettingOn() {
$o = new TableFieldTest_Object();
$o->write();
$tf = new TableField(
'HasManyRelations',
'TableFieldTest_HasManyRelation',
@ -179,70 +185,17 @@ class TableFieldTest extends SapphireTest {
);
// Test with auto relation setting
$form = new Form(new TableFieldTest_Controller(), "Form", new FieldSet($tf), new FieldSet());
$form = new Form(new TableFieldTest_Controller(), "Form", new FieldList($tf), new FieldList());
$form->loadDataFrom($o);
$tf->setValue(array(
'new' => array(
'Value' => array('one','two',)
)
));
$tf->setRelationAutoSetting(true);
$o = new TableFieldTest_Object();
$o->write();
$form->saveInto($o);
$this->assertEquals($o->HasManyRelations()->Count(), 2);
}
function testAutoRelationSettingOff() {
$tf = new TableField(
'HasManyRelations',
'TableFieldTest_HasManyRelation',
array(
'Value' => 'Value'
),
array(
'Value' => 'TextField'
)
);
// Test with auto relation setting
$form = new Form(new TableFieldTest_Controller(), "Form", new FieldSet($tf), new FieldSet());
$tf->setValue(array(
'new' => array(
'Value' => array('one','two',)
)
));
$tf->setRelationAutoSetting(false);
$o = new TableFieldTest_Object();
$o->write();
$form->saveInto($o);
$this->assertEquals($o->HasManyRelations()->Count(), 0);
}
function testDataValue() {
$tf = new TableField(
'TestTableField',
'TestTableField',
array(
'Currency' => 'Currency'
),
array(
'Currency' => 'CurrencyField'
)
);
$form = new Form(new TableFieldTest_Controller(), "Form", new FieldSet($tf), new FieldSet());
$tf->setValue(array(
'new' => array(
'Currency' => array(
'$1,234.56',
'1234.57',
)
)
));
$data = $form->getData();
// @todo Fix getData()
//$this->assertEquals($data['TestTableField']['new']['Currency'][0], 1234.56);
//$this->assertEquals($data['TestTableField']['new']['Currency'][1], 1234.57);
$this->assertEquals(2, $o->HasManyRelations()->Count());
}
function testHasItemsWhenSetAsArray() {

View File

@ -17,9 +17,9 @@ class TableListFieldTest extends SapphireTest {
"E" => "Col E",
));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet(
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
$table
), new FieldSet());
), new FieldList());
$result = $table->FieldHolder();
@ -45,14 +45,14 @@ class TableListFieldTest extends SapphireTest {
"E" => "Col E",
));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet(
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
$table
), new FieldSet());
), new FieldList());
$items = $table->sourceItems();
$this->assertNotNull($items);
$itemMap = $items->toDropdownMap("ID", "A") ;
$itemMap = $items->map("ID", "A") ;
$this->assertEquals(array(
$item1->ID => "a1",
$item2->ID => "a2",
@ -78,9 +78,9 @@ class TableListFieldTest extends SapphireTest {
"E" => "Col E",
));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet(
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
$table
), new FieldSet());
), new FieldList());
$table->ShowPagination = true;
$table->PageSize = 2;
@ -88,7 +88,7 @@ class TableListFieldTest extends SapphireTest {
$items = $table->sourceItems();
$this->assertNotNull($items);
$itemMap = $items->toDropdownMap("ID", "A") ;
$itemMap = $items->map("ID", "A") ;
$this->assertEquals(array(
$item1->ID => "a1",
$item2->ID => "a2"
@ -111,9 +111,9 @@ class TableListFieldTest extends SapphireTest {
"E" => "Col E",
));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet(
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
$table
), new FieldSet());
), new FieldList());
$table->ShowPagination = true;
$table->PageSize = 2;
@ -122,7 +122,7 @@ class TableListFieldTest extends SapphireTest {
$items = $table->sourceItems();
$this->assertNotNull($items);
$itemMap = $items->toDropdownMap("ID", "A") ;
$itemMap = $items->map("ID", "A") ;
$this->assertEquals(array($item3->ID => "a3", $item4->ID => "a4"), $itemMap);
}
@ -182,9 +182,9 @@ class TableListFieldTest extends SapphireTest {
"B" => "Col B"
));
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet(
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
$table
), new FieldSet());
), new FieldList());
$csvResponse = $table->export();
@ -219,7 +219,7 @@ class TableListFieldTest extends SapphireTest {
function testLink() {
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet(
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
new TableListField("Tester", "TableListFieldTest_Obj", array(
"A" => "Col A",
"B" => "Col B",
@ -227,7 +227,7 @@ class TableListFieldTest extends SapphireTest {
"D" => "Col D",
"E" => "Col E",
))
), new FieldSet());
), new FieldList());
$table = $form->dataFieldByName('Tester');
$this->assertEquals(
@ -252,9 +252,9 @@ class TableListFieldTest extends SapphireTest {
"E" => "Col E",
));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet(
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
$table
), new FieldSet());
), new FieldList());
$table->ShowPagination = true;
$table->PageSize = 2;
@ -292,6 +292,38 @@ class TableListFieldTest extends SapphireTest {
unset($_REQUEST['ctf']);
}
/**
* Check that a DataObjectSet can be passed to TableListField
*/
function testDataObjectSet() {
$one = new TableListFieldTest_Obj;
$one->A = "A-one";
$two = new TableListFieldTest_Obj;
$two->A = "A-two";
$three = new TableListFieldTest_Obj;
$three->A = "A-three";
$list = new ArrayList(array($one, $two, $three));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
new TableListField("Tester", $list, array(
"A" => "Col A",
"B" => "Col B",
"C" => "Col C",
"D" => "Col D",
"E" => "Col E",
))
), new FieldList());
$table = $form->dataFieldByName('Tester');
$rendered = $table->FieldHolder();
$this->assertContains('A-one', $rendered);
$this->assertContains('A-two', $rendered);
$this->assertContains('A-three', $rendered);
}
}
class TableListFieldTest_Obj extends DataObject implements TestOnly {
@ -335,8 +367,8 @@ class TableListFieldTest_TestController extends Controller {
$table->disableSorting();
// A TableListField must be inside a form for its links to be generated
return new Form($this, "TestForm", new FieldSet(
return new Form($this, "TestForm", new FieldList(
$table
), new FieldSet());
), new FieldList());
}
}

View File

@ -0,0 +1,278 @@
<?php
/**
* @package sapphire
* @subpackage tests
*/
class ArrayListTest extends SapphireTest {
public function testArrayAccessExists() {
$list = new ArrayList(array(
$one = new DataObject(array('Title' => 'one')),
$two = new DataObject(array('Title' => 'two')),
$three = new DataObject(array('Title' => 'three'))
));
$this->assertEquals(count($list), 3);
$this->assertTrue(isset($list[0]), 'First item in the set is set');
$this->assertEquals($one, $list[0], 'First item in the set is accessible by array notation');
}
public function testArrayAccessUnset() {
$list = new ArrayList(array(
$one = new DataObject(array('Title' => 'one')),
$two = new DataObject(array('Title' => 'two')),
$three = new DataObject(array('Title' => 'three'))
));
unset($list[0]);
$this->assertEquals(count($list), 2);
}
public function testArrayAccessSet() {
$list = new ArrayList();
$this->assertEquals(0, count($list));
$list['testing!'] = $test = new DataObject(array('Title' => 'I\'m testing!'));
$this->assertEquals($test, $list['testing!'], 'Set item is accessible by the key we set it as');
}
public function testCount() {
$list = new ArrayList();
$this->assertEquals(0, $list->count());
$list = new ArrayList(array(1, 2, 3));
$this->assertEquals(3, $list->count());
}
public function testExists() {
$list = new ArrayList();
$this->assertFalse($list->exists());
$list = new ArrayList(array(1, 2, 3));
$this->assertTrue($list->exists());
}
public function testToNestedArray() {
$list = new ArrayList(array(
array('First' => 'FirstFirst', 'Second' => 'FirstSecond'),
(object) array('First' => 'SecondFirst', 'Second' => 'SecondSecond'),
new ArrayListTest_Object('ThirdFirst', 'ThirdSecond')
));
$this->assertEquals($list->toNestedArray(), array(
array('First' => 'FirstFirst', 'Second' => 'FirstSecond'),
array('First' => 'SecondFirst', 'Second' => 'SecondSecond'),
array('First' => 'ThirdFirst', 'Second' => 'ThirdSecond')
));
}
public function testGetRange() {
$list = new ArrayList(array(
array('Key' => 1), array('Key' => 2), array('Key' => 3)
));
$this->assertEquals($list->getRange(1, 2)->toArray(), array(
array('Key' => 2), array('Key' => 3)
));
}
public function testAddRemove() {
$list = new ArrayList(array(
array('Key' => 1), array('Key' => 2)
));
$list->add(array('Key' => 3));
$this->assertEquals($list->toArray(), array(
array('Key' => 1), array('Key' => 2), array('Key' => 3)
));
$list->remove(array('Key' => 2));
$this->assertEquals(array_values($list->toArray()), array(
array('Key' => 1), array('Key' => 3)
));
}
public function testReplace() {
$list = new ArrayList(array(
array('Key' => 1),
$two = (object) array('Key' => 2),
(object) array('Key' => 3)
));
$this->assertEquals(array('Key' => 1), $list[0]);
$list->replace(array('Key' => 1), array('Replaced' => 1));
$this->assertEquals(3, count($list));
$this->assertEquals(array('Replaced' => 1), $list[0]);
$this->assertEquals($two, $list[1]);
$list->replace($two, array('Replaced' => 2));
$this->assertEquals(3, count($list));
$this->assertEquals(array('Replaced' => 2), $list[1]);
}
public function testMerge() {
$list = new ArrayList(array(
array('Num' => 1), array('Num' => 2)
));
$list->merge(array(
array('Num' => 3), array('Num' => 4)
));
$this->assertEquals(4, count($list));
$this->assertEquals($list->toArray(), array(
array('Num' => 1), array('Num' => 2), array('Num' => 3), array('Num' => 4)
));
}
public function testRemoveDuplicates() {
$list = new ArrayList(array(
array('ID' => 1, 'Field' => 1),
array('ID' => 2, 'Field' => 2),
array('ID' => 3, 'Field' => 3),
array('ID' => 4, 'Field' => 1),
(object) array('ID' => 5, 'Field' => 2)
));
$this->assertEquals(5, count($list));
$list->removeDuplicates();
$this->assertEquals(5, count($list));
$list->removeDuplicates('Field');
$this->assertEquals(3, count($list));
$this->assertEquals(array(1, 2, 3), $list->column('Field'));
$this->assertEquals(array(1, 2, 3), $list->column('ID'));
}
public function testPushPop() {
$list = new ArrayList(array('Num' => 1));
$this->assertEquals(1, count($list));
$list->push(array('Num' => 2));
$this->assertEquals(2, count($list));
$this->assertEquals(array('Num' => 2), $list->last());
$list->push(array('Num' => 3));
$this->assertEquals(3, count($list));
$this->assertEquals(array('Num' => 3), $list->last());
$this->assertEquals(array('Num' => 3), $list->pop());
$this->assertEquals(2, count($list));
$this->assertEquals(array('Num' => 2), $list->last());
}
public function testShiftUnshift() {
$list = new ArrayList(array('Num' => 1));
$this->assertEquals(1, count($list));
$list->unshift(array('Num' => 2));
$this->assertEquals(2, count($list));
$this->assertEquals(array('Num' => 2), $list->first());
$list->unshift(array('Num' => 3));
$this->assertEquals(3, count($list));
$this->assertEquals(array('Num' => 3), $list->first());
$this->assertEquals(array('Num' => 3), $list->shift());
$this->assertEquals(2, count($list));
$this->assertEquals(array('Num' => 2), $list->first());
}
public function testFirstLast() {
$list = new ArrayList(array(
array('Key' => 1), array('Key' => 2), array('Key' => 3)
));
$this->assertEquals($list->first(), array('Key' => 1));
$this->assertEquals($list->last(), array('Key' => 3));
}
public function testMap() {
$list = new ArrayList(array(
array('ID' => 1, 'Name' => 'Steve',),
(object) array('ID' => 3, 'Name' => 'Bob'),
array('ID' => 5, 'Name' => 'John')
));
$this->assertEquals($list->map('ID', 'Name'), array(
1 => 'Steve',
3 => 'Bob',
5 => 'John'
));
}
public function testFind() {
$list = new ArrayList(array(
array('Name' => 'Steve'),
(object) array('Name' => 'Bob'),
array('Name' => 'John')
));
$this->assertEquals($list->find('Name', 'Bob'), (object) array(
'Name' => 'Bob'
));
}
public function testColumn() {
$list = new ArrayList(array(
array('Name' => 'Steve'),
(object) array('Name' => 'Bob'),
array('Name' => 'John')
));
$this->assertEquals($list->column('Name'), array(
'Steve', 'Bob', 'John'
));
}
public function testSort() {
$list = new ArrayList(array(
array('Name' => 'Steve'),
(object) array('Name' => 'Bob'),
array('Name' => 'John')
));
$list->sort('Name');
$this->assertEquals($list->toArray(), array(
(object) array('Name' => 'Bob'),
array('Name' => 'John'),
array('Name' => 'Steve')
));
$list->sort('Name', 'DESC');
$this->assertEquals($list->toArray(), array(
array('Name' => 'Steve'),
array('Name' => 'John'),
(object) array('Name' => 'Bob')
));
}
public function testMultiSort() {
$list = new ArrayList(array(
(object) array('Name'=>'Object1', 'F1'=>1, 'F2'=>2, 'F3'=>3),
(object) array('Name'=>'Object2', 'F1'=>2, 'F2'=>1, 'F3'=>4),
(object) array('Name'=>'Object3', 'F1'=>5, 'F2'=>2, 'F3'=>2),
));
$list->sort('F3', 'ASC');
$this->assertEquals($list->first()->Name, 'Object3', 'Object3 should be first in the list');
$list->sort('F3', 'DESC');
$this->assertEquals($list->first()->Name, 'Object2', 'Object2 should be first in the list');
$list->sort(array('F2'=>'ASC', 'F1'=>'ASC'));
$this->assertEquals($list->last()->Name, 'Object3', 'Object3 should be last in the list');
$list->sort(array('F2'=>'ASC', 'F1'=>'DESC'));
$this->assertEquals($list->last()->Name, 'Object1', 'Object1 should be last in the list');
}
}
/**
* @ignore
*/
class ArrayListTest_Object {
public $First;
public $Second;
public function __construct($first, $second) {
$this->First = $first;
$this->Second = $second;
}
public function toMap() {
return array('First' => $this->First, 'Second' => $this->Second);
}
}

View File

@ -66,7 +66,7 @@ class DataExtensionTest extends SapphireTest {
$parent->Faves()->add($obj2->ID);
$this->assertEquals(2, $parent->Faves()->Count());
$parent->Faves()->remove($obj2->ID);
$parent->Faves()->removeByID($obj2->ID);
$this->assertEquals(1, $parent->Faves()->Count());
}

View File

@ -0,0 +1,107 @@
<?php
class DataListTest extends SapphireTest {
// Borrow the model from DataObjectTest
static $fixture_file = 'DataObjectTest.yml';
protected $extraDataObjects = array(
'DataObjectTest_Team',
'DataObjectTest_Fixture',
'DataObjectTest_SubTeam',
'OtherSubclassWithSameField',
'DataObjectTest_FieldlessTable',
'DataObjectTest_FieldlessSubTable',
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment'
);
function testListCreationSortAndLimit() {
// By default, a DataList will contain all items of that class
$list = DataList::create('DataObjectTest_TeamComment')->sort('ID');
// We can iterate on the DataList
$names = array();
foreach($list as $item) {
$names[] = $item->Name;
}
$this->assertEquals(array('Joe', 'Bob', 'Phil'), $names);
// If we don't want to iterate, we can extract a single column from the list with column()
$this->assertEquals(array('Joe', 'Bob', 'Phil'), $list->column('Name'));
// We can sort a list
$list = $list->sort('Name');
$this->assertEquals(array('Bob', 'Joe', 'Phil'), $list->column('Name'));
// We can also restrict the output to a range
$this->assertEquals(array('Joe', 'Phil'), $list->getRange(1,2)->column('Name'));
}
function testFilter() {
// coming soon!
}
function testWhere() {
// We can use raw SQL queries with where. This is only recommended for advanced uses;
// if you can, you should use filter().
$list = DataList::create('DataObjectTest_TeamComment');
// where() returns a new DataList, like all the other modifiers, so it can be chained.
$list2 = $list->where('"Name" = \'Joe\'');
$this->assertEquals(array('This is a team comment by Joe'), $list2->column('Comment'));
// The where() clauses are chained together with AND
$list3 = $list2->where('"Name" = \'Bob\'');
$this->assertEquals(array(), $list3->column('Comment'));
}
/**
* Test DataList->byID()
*/
function testByID() {
// We can get a single item by ID.
$id = $this->idFromFixture('DataObjectTest_Team','team2');
$team = DataList::create("DataObjectTest_Team")->byID($id);
// byID() returns a DataObject, rather than a DataList
$this->assertInstanceOf('DataObjectTest_Team', $team);
$this->assertEquals('Team 2', $team->Title);
}
/**
* Test DataList->removeByID()
*/
function testRemoveByID() {
$list = DataList::create("DataObjectTest_Team");
$id = $this->idFromFixture('DataObjectTest_Team','team2');
$this->assertNotNull($list->byID($id));
$list->removeByID($id);
$this->assertNull($list->byID($id));
}
/**
* Test DataList->canSortBy()
*/
function testCanSortBy() {
// Basic check
$team = DataList::create("DataObjectTest_Team");
$this->assertTrue($team->canSortBy("Title"));
$this->assertFalse($team->canSortBy("SomethingElse"));
// Subclasses
$subteam = DataList::create("DataObjectTest_SubTeam");
$this->assertTrue($subteam->canSortBy("Title"));
$this->assertTrue($subteam->canSortBy("SubclassDatabaseField"));
}
function testDataListArrayAccess() {
$list = DataList::create("DataObjectTest_Team")->sort("Title");
// We can use array access to refer to single items in the DataList, as if it were an array
$this->assertEquals("Subteam 1", $list[0]->Title);
$this->assertEquals("Subteam 3", $list[2]->Title);
$this->assertEquals("Team 2", $list[4]->Title);
}
}

View File

@ -1,436 +0,0 @@
<?php
/**
* Test the {@link DataObjectSet} class.
*
* @package sapphire
* @subpackage tests
*/
class DataObjectSetTest extends SapphireTest {
static $fixture_file = 'DataObjectSetTest.yml';
protected $extraDataObjects = array(
'DataObjectTest_Team',
'DataObjectTest_SubTeam',
'DataObjectTest_Player',
'DataObjectSetTest_TeamComment',
'DataObjectSetTest_Base',
'DataObjectSetTest_ChildClass',
);
function testArrayAccessExists() {
$set = new DataObjectSet(array(
$one = new DataObject(array('Title' => 'one')),
$two = new DataObject(array('Title' => 'two')),
$three = new DataObject(array('Title' => 'three'))
));
$this->assertEquals(count($set), 3);
$this->assertTrue(isset($set[0]), 'First item in the set is set');
$this->assertEquals($one, $set[0], 'First item in the set is accessible by array notation');
}
function testArrayAccessUnset() {
$set = new DataObjectSet(array(
$one = new DataObject(array('Title' => 'one')),
$two = new DataObject(array('Title' => 'two')),
$three = new DataObject(array('Title' => 'three'))
));
unset($set[0]);
$this->assertEquals(count($set), 2);
}
function testArrayAccessSet() {
$set = new DataObjectSet();
$this->assertEquals(0, count($set));
$set['testing!'] = $test = new DataObject(array('Title' => 'I\'m testing!'));
$this->assertEquals($test, $set['testing!'], 'Set item is accessible by the key we set it as');
}
function testIterator() {
$set = new DataObjectSet(array(
$one = new DataObject(array('Title'=>'one')),
$two = new DataObject(array('Title'=>'two')),
$three = new DataObject(array('Title'=>'three')),
$four = new DataObject(array('Title'=>'four'))
));
// test Pos() with foreach()
$i = 0;
foreach($set as $item) {
$i++;
$this->assertEquals($i, $item->Pos(), "Iterator position is set correctly on ViewableData when iterated with foreach()");
}
// test Pos() manually
$this->assertEquals(1, $one->Pos());
$this->assertEquals(2, $two->Pos());
$this->assertEquals(3, $three->Pos());
$this->assertEquals(4, $four->Pos());
// test DataObjectSet->Count()
$this->assertEquals(4, $set->Count());
// test DataObjectSet->First()
$this->assertSame($one, $set->First());
// test DataObjectSet->Last()
$this->assertSame($four, $set->Last());
// test ViewableData->First()
$this->assertTrue($one->First());
$this->assertFalse($two->First());
$this->assertFalse($three->First());
$this->assertFalse($four->First());
// test ViewableData->Last()
$this->assertFalse($one->Last());
$this->assertFalse($two->Last());
$this->assertFalse($three->Last());
$this->assertTrue($four->Last());
// test ViewableData->Middle()
$this->assertFalse($one->Middle());
$this->assertTrue($two->Middle());
$this->assertTrue($three->Middle());
$this->assertFalse($four->Middle());
// test ViewableData->Even()
$this->assertFalse($one->Even());
$this->assertTrue($two->Even());
$this->assertFalse($three->Even());
$this->assertTrue($four->Even());
// test ViewableData->Odd()
$this->assertTrue($one->Odd());
$this->assertFalse($two->Odd());
$this->assertTrue($three->Odd());
$this->assertFalse($four->Odd());
}
public function testMultipleOf() {
$comments = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC");
$commArr = $comments->toArray();
$multiplesOf3 = 0;
foreach($comments as $comment) {
if($comment->MultipleOf(3)) {
$comment->IsMultipleOf3 = true;
$multiplesOf3++;
} else {
$comment->IsMultipleOf3 = false;
}
}
$this->assertEquals(1, $multiplesOf3);
$this->assertFalse($commArr[0]->IsMultipleOf3);
$this->assertFalse($commArr[1]->IsMultipleOf3);
$this->assertTrue($commArr[2]->IsMultipleOf3);
foreach($comments as $comment) {
if($comment->MultipleOf(3, 1)) {
$comment->IsMultipleOf3 = true;
} else {
$comment->IsMultipleOf3 = false;
}
}
$this->assertFalse($commArr[0]->IsMultipleOf3);
$this->assertFalse($commArr[1]->IsMultipleOf3);
$this->assertTrue($commArr[2]->IsMultipleOf3);
}
/**
* Test {@link DataObjectSet->Count()}
*/
function testCount() {
$comments = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC");
/* There are a total of 8 items in the set */
$this->assertEquals($comments->Count(), 3, 'There are a total of 3 items in the set');
}
/**
* Test {@link DataObjectSet->First()}
*/
function testFirst() {
$comments = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC");
/* The first object is Joe's comment */
$this->assertEquals($comments->First()->Name, 'Joe', 'The first object has a Name field value of "Joe"');
}
/**
* Test {@link DataObjectSet->Last()}
*/
function testLast() {
$comments = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC");
/* The last object is Dean's comment */
$this->assertEquals($comments->Last()->Name, 'Phil', 'The last object has a Name field value of "Phil"');
}
/**
* Test {@link DataObjectSet->map()}
*/
function testMap() {
$comments = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC");
/* Now we get a map of all the PageComment records */
$map = $comments->map('ID', 'Title', '(Select one)');
$expectedMap = array(
'' => '(Select one)',
1 => 'Joe',
2 => 'Bob',
3 => 'Phil'
);
/* There are 9 items in the map. 3 are records. 1 is the empty value */
$this->assertEquals(count($map), 4, 'There are 4 items in the map. 3 are records. 1 is the empty value');
/* We have the same map as our expected map, asserted above */
/* toDropDownMap() is an alias of map() - let's make a map from that */
$map2 = $comments->toDropDownMap('ID', 'Title', '(Select one)');
/* There are 4 items in the map. 3 are records. 1 is the empty value */
$this->assertEquals(count($map), 4, 'There are 4 items in the map. 3 are records. 1 is the empty value.');
}
function testRemoveDuplicates() {
// Note that PageComment and DataObjectSetTest_TeamComment are both descendants of DataObject, and don't
// share an inheritance relationship below that.
$pageComments = DataObject::get('DataObjectSetTest_TeamComment');
$teamComments = DataObject::get('DataObjectSetTest_TeamComment');
/* Test default functionality (remove by ID). We'd expect to loose all our
* team comments as they have the same IDs as the first three page comments */
$allComments = new DataObjectSet();
$allComments->merge($pageComments);
$allComments->merge($teamComments);
$this->assertEquals($allComments->Count(), 6);
$allComments->removeDuplicates();
$this->assertEquals($allComments->Count(), 3, 'Standard functionality is to remove duplicate base class/IDs');
/* Now test removing duplicates based on a common field. In this case we shall
* use 'Name', so we can get all the unique commentators */
$comment = new DataObjectSetTest_TeamComment();
$comment->Name = "Bob";
$allComments->push($comment);
$this->assertEquals($allComments->Count(), 4);
$allComments->removeDuplicates('Name');
$this->assertEquals($allComments->Count(), 3, 'There are 3 uniquely named commentators');
// Ensure that duplicates are removed where the base data class is the same.
$mixedSet = new DataObjectSet();
$mixedSet->push(new DataObjectSetTest_Base(array('ID' => 1)));
$mixedSet->push(new DataObjectSetTest_ChildClass(array('ID' => 1))); // dup: same base class and ID
$mixedSet->push(new DataObjectSetTest_ChildClass(array('ID' => 1))); // dup: more than one dup of the same object
$mixedSet->push(new DataObjectSetTest_ChildClass(array('ID' => 2))); // not dup: same type again, but different
$mixedSet->push(new DataObjectSetTest_Base(array('ID' => 1))); // dup: another dup, not consequetive.
$mixedSet->removeDuplicates('ID');
$this->assertEquals($mixedSet->Count(), 2, 'There are 3 unique data objects in a very mixed set');
}
/**
* Test {@link DataObjectSet->parseQueryLimit()}
*/
function testParseQueryLimit() {
// Create empty objects, because they don't need to have contents
$sql = new SQLQuery('*', '"Member"');
$max = $sql->unlimitedRowCount();
$set = new DataObjectSet();
// Test handling an array
$set->parseQueryLimit($sql->limit(array('limit'=>5, 'start'=>2)));
$expected = array(
'pageStart' => 2,
'pageLength' => 5,
'totalSize' => $max,
);
$this->assertEquals($expected, $set->getPageLimits(), 'The page limits match expected values.');
// Test handling OFFSET string
// uppercase
$set->parseQueryLimit($sql->limit('3 OFFSET 1'));
$expected = array(
'pageStart' => 1,
'pageLength' => 3,
'totalSize' => $max,
);
$this->assertEquals($expected, $set->getPageLimits(), 'The page limits match expected values.');
// and lowercase
$set->parseQueryLimit($sql->limit('32 offset 3'));
$expected = array(
'pageStart' => 3,
'pageLength' => 32,
'totalSize' => $max,
);
$this->assertEquals($expected, $set->getPageLimits(), 'The page limits match expected values.');
// Finally check MySQL LIMIT syntax
$set->parseQueryLimit($sql->limit('7, 7'));
$expected = array(
'pageStart' => 7,
'pageLength' => 7,
'totalSize' => $max,
);
$this->assertEquals($expected, $set->getPageLimits(), 'The page limits match expected values.');
}
/**
* Test {@link DataObjectSet->insertFirst()}
*/
function testInsertFirst() {
// Get one comment
$comment = DataObject::get_one('DataObjectSetTest_TeamComment', "\"Name\" = 'Joe'");
// Get all other comments
$set = DataObject::get('DataObjectSetTest_TeamComment', '"Name" != \'Joe\'');
// Duplicate so we can use it later without another lookup
$otherSet = clone $set;
// insert without a key
$otherSet->insertFirst($comment);
$this->assertEquals($comment, $otherSet->First(), 'Comment should be first');
// Give us another copy
$otherSet = clone $set;
// insert with a numeric key
$otherSet->insertFirst($comment, 2);
$this->assertEquals($comment, $otherSet->First(), 'Comment should be first');
// insert with a non-numeric key
$set->insertFirst($comment, 'SomeRandomKey');
$this->assertEquals($comment, $set->First(), 'Comment should be first');
}
/**
* Test {@link DataObjectSet->getRange()}
*/
function testGetRange() {
$comments = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC");
// Make sure we got all 8 comments
$this->assertEquals($comments->Count(), 3, 'Three comments in the database.');
// Grab a range
$range = $comments->getRange(1, 2);
$this->assertEquals($range->Count(), 2, 'Two comment in the range.');
// And now grab a range that shouldn't be full. Remember counting starts at 0.
$range = $comments->getRange(2, 1);
$this->assertEquals($range->Count(), 1, 'One comment in the range.');
// Make sure it's the last one
$this->assertEquals($range->First(), $comments->Last(), 'The only item in the range should be the last one.');
}
/**
* Test {@link DataObjectSet->exists()}
*/
function testExists() {
// Test an empty set
$set = new DataObjectSet();
$this->assertFalse($set->exists(), 'Empty set doesn\'t exist.');
// Test a non-empty set
$set = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC");
$this->assertTrue($set->exists(), 'Non-empty set does exist.');
}
/**
* Test {@link DataObjectSet->shift()}
*/
function testShift() {
$set = new DataObjectSet();
$set->push(new ArrayData(array('Name' => 'Joe')));
$set->push(new ArrayData(array('Name' => 'Bob')));
$set->push(new ArrayData(array('Name' => 'Ted')));
$this->assertEquals('Joe', $set->shift()->Name);
}
/**
* Test {@link DataObjectSet->unshift()}
*/
function testUnshift() {
$set = new DataObjectSet();
$set->push(new ArrayData(array('Name' => 'Joe')));
$set->push(new ArrayData(array('Name' => 'Bob')));
$set->push(new ArrayData(array('Name' => 'Ted')));
$set->unshift(new ArrayData(array('Name' => 'Steve')));
$this->assertEquals('Steve', $set->First()->Name);
}
/**
* Test {@link DataObjectSet->pop()}
*/
function testPop() {
$set = new DataObjectSet();
$set->push(new ArrayData(array('Name' => 'Joe')));
$set->push(new ArrayData(array('Name' => 'Bob')));
$set->push(new ArrayData(array('Name' => 'Ted')));
$this->assertEquals('Ted', $set->pop()->Name);
}
/**
* Test {@link DataObjectSet->sort()}
*/
function testSort() {
$set = new DataObjectSet(array(
array('Name'=>'Object1', 'F1'=>1, 'F2'=>2, 'F3'=>3),
array('Name'=>'Object2', 'F1'=>2, 'F2'=>1, 'F3'=>4),
array('Name'=>'Object3', 'F1'=>5, 'F2'=>2, 'F3'=>2),
));
// test a single sort ASC
$set->sort('F3', 'ASC');
$this->assertEquals($set->First()->Name, 'Object3', 'Object3 should be first in the set');
// test a single sort DESC
$set->sort('F3', 'DESC');
$this->assertEquals($set->First()->Name, 'Object2', 'Object2 should be first in the set');
// test a multi sort
$set->sort(array('F2'=>'ASC', 'F1'=>'ASC'));
$this->assertEquals($set->Last()->Name, 'Object3', 'Object3 should be last in the set');
// test a multi sort
$set->sort(array('F2'=>'ASC', 'F1'=>'DESC'));
$this->assertEquals($set->Last()->Name, 'Object1', 'Object1 should be last in the set');
}
}
/**
* @package sapphire
* @subpackage tests
*/
class DataObjectSetTest_TeamComment extends DataObject implements TestOnly {
static $db = array(
'Name' => 'Varchar',
'Comment' => 'Text',
);
static $has_one = array(
'Team' => 'DataObjectTest_Team',
);
}
class DataObjectSetTest_Base extends DataObject implements TestOnly {
static $db = array(
'Name' => 'Varchar'
);
}
class DataObjectSetTest_ChildClass extends DataObjectSetTest_Base implements TestOnly {
}

View File

@ -1,45 +0,0 @@
DataObjectTest_Team:
team1:
Title: Team 1
team2:
Title: Team 2
DataObjectTest_Player:
captain1:
FirstName: Captain 1
FavouriteTeam: =>DataObjectTest_Team.team1
Teams: =>DataObjectTest_Team.team1
captain2:
FirstName: Captain 2
Teams: =>DataObjectTest_Team.team2
player1:
FirstName: Player 1
player2:
FirstName: Player 2
Teams: =>DataObjectTest_Team.team1,=>DataObjectTest_Team.team2
DataObjectTest_SubTeam:
subteam1:
Title: Subteam 1
SubclassDatabaseField: Subclassed 1
ExtendedDatabaseField: Extended 1
subteam2_with_player_relation:
Title: Subteam 2
SubclassDatabaseField: Subclassed 2
ExtendeHasOneRelationship: =>DataObjectTest_Player.player1
subteam3_with_empty_fields:
Title: Subteam 3
DataObjectSetTest_TeamComment:
comment1:
Name: Joe
Comment: This is a team comment by Joe
Team: =>DataObjectTest_Team.team1
comment2:
Name: Bob
Comment: This is a team comment by Bob
Team: =>DataObjectTest_Team.team1
comment3:
Name: Phil
Comment: Phil is a unique guy, and comments on team2
Team: =>DataObjectTest_Team.team2

View File

@ -137,14 +137,6 @@ class DataObjectTest extends SapphireTest {
$this->assertEquals('Joe', $comments->First()->Name);
$this->assertEquals('Phil', $comments->Last()->Name);
// Test container class
$comments = DataObject::get('DataObjectTest_TeamComment', '', '', '', '', 'DataObjectSet');
$this->assertEquals('DataObjectSet', get_class($comments));
$comments = DataObject::get('DataObjectTest_TeamComment', '', '', '', '', 'ComponentSet');
$this->assertEquals('ComponentSet', get_class($comments));
// Test get_by_id()
$captain1ID = $this->idFromFixture('DataObjectTest_Player', 'captain1');
$captain1 = DataObject::get_by_id('DataObjectTest_Player', $captain1ID);
@ -178,6 +170,45 @@ class DataObjectTest extends SapphireTest {
$this->assertEquals('Phil', $comment->Name);
}
function testGetSubclassFields() {
/* Test that fields / has_one relations from the parent table and the subclass tables are extracted */
$captain1 = $this->objFromFixture("DataObjectTest_Player", "captain1");
// Base field
$this->assertEquals('Captain', $captain1->FirstName);
// Subclass field
$this->assertEquals('007', $captain1->ShirtNumber);
// Subclass has_one relation
$this->assertEquals($this->idFromFixture('DataObjectTest_Team', 'team1'), $captain1->FavouriteTeamID);
}
function testGetHasOneRelations() {
$captain1 = $this->objFromFixture("DataObjectTest_Player", "captain1");
/* There will be a field called (relname)ID that contains the ID of the object linked to via the has_one relation */
$this->assertEquals($this->idFromFixture('DataObjectTest_Team', 'team1'), $captain1->FavouriteTeamID);
/* There will be a method called $obj->relname() that returns the object itself */
$this->assertEquals($this->idFromFixture('DataObjectTest_Team', 'team1'), $captain1->FavouriteTeam()->ID);
}
function testLimitAndCount() {
$players = DataObject::get("DataObjectTest_Player");
// There's 4 records in total
$this->assertEquals(4, $players->count());
// Testing "## offset ##" syntax
$this->assertEquals(4, $players->limit("20 OFFSET 0")->count());
$this->assertEquals(0, $players->limit("20 OFFSET 20")->count());
$this->assertEquals(2, $players->limit("2 OFFSET 0")->count());
$this->assertEquals(1, $players->limit("5 OFFSET 3")->count());
// Testing "##, ##" syntax
$this->assertEquals(4, $players->limit("20")->count());
$this->assertEquals(4, $players->limit("0, 20")->count());
$this->assertEquals(0, $players->limit("20, 20")->count());
$this->assertEquals(2, $players->limit("0, 2")->count());
$this->assertEquals(1, $players->limit("3, 5")->count());
}
/**
* Test writing of database columns which don't correlate to a DBField,
* e.g. all relation fields on has_one/has_many like "ParentID".
@ -201,12 +232,27 @@ class DataObjectTest extends SapphireTest {
$team = $this->objFromFixture('DataObjectTest_Team', 'team1');
// Test getComponents() gets the ComponentSet of the other side of the relation
$this->assertTrue($team->getComponents('Comments')->Count() == 2);
$this->assertTrue($team->Comments()->Count() == 2);
// Test the IDs on the DataObjects are set correctly
foreach($team->getComponents('Comments') as $comment) {
$this->assertTrue($comment->TeamID == $team->ID);
foreach($team->Comments() as $comment) {
$this->assertEquals($team->ID, $comment->TeamID);
}
// Test that we can add and remove items that already exist in the database
$newComment = new DataObjectTest_TeamComment();
$newComment->Name = "Automated commenter";
$newComment->Comment = "This is a new comment";
$newComment->write();
$team->Comments()->add($newComment);
$this->assertEquals($team->ID, $newComment->TeamID);
$comment1 = $this->fixture->objFromFixture('DataObjectTest_TeamComment', 'comment1');
$comment2 = $this->fixture->objFromFixture('DataObjectTest_TeamComment', 'comment2');
$team->Comments()->remove($comment2);
$commentIDs = $team->Comments()->column('ID');
$this->assertEquals(array($comment1->ID, $newComment->ID), $commentIDs);
}
function testHasOneRelationship() {
@ -236,7 +282,7 @@ class DataObjectTest extends SapphireTest {
// Test adding single DataObject by reference
$player1->Teams()->add($team1);
$player1->flushCache();
$compareTeams = new ComponentSet($team1);
$compareTeams = new ManyManyList($team1);
$this->assertEquals(
$player1->Teams()->column('ID'),
$compareTeams->column('ID'),
@ -246,7 +292,7 @@ class DataObjectTest extends SapphireTest {
// test removing single DataObject by reference
$player1->Teams()->remove($team1);
$player1->flushCache();
$compareTeams = new ComponentSet();
$compareTeams = new ManyManyList();
$this->assertEquals(
$player1->Teams()->column('ID'),
$compareTeams->column('ID'),
@ -256,7 +302,7 @@ class DataObjectTest extends SapphireTest {
// test adding single DataObject by ID
$player1->Teams()->add($team1->ID);
$player1->flushCache();
$compareTeams = new ComponentSet($team1);
$compareTeams = new ManyManyList($team1);
$this->assertEquals(
$player1->Teams()->column('ID'),
$compareTeams->column('ID'),
@ -264,14 +310,22 @@ class DataObjectTest extends SapphireTest {
);
// test removing single DataObject by ID
$player1->Teams()->remove($team1->ID);
$player1->Teams()->removeByID($team1->ID);
$player1->flushCache();
$compareTeams = new ComponentSet();
$compareTeams = new ManyManyList();
$this->assertEquals(
$player1->Teams()->column('ID'),
$compareTeams->column('ID'),
"Removing single record as ID from many_many"
);
// Set a many-many relationship by and idList
$player1->Teams()->setByIdList(array($team1->ID, $team2->ID));
$this->assertEquals(array($team1->ID, $team2->ID), $player1->Teams()->column());
$player1->Teams()->setByIdList(array($team1->ID));
$this->assertEquals(array($team1->ID), $player1->Teams()->column());
$player1->Teams()->setByIdList(array($team2->ID));
$this->assertEquals(array($team2->ID), $player1->Teams()->column());
}
/**
@ -380,6 +434,20 @@ class DataObjectTest extends SapphireTest {
$obj->write();
$this->assertFalse($obj->isChanged());
/* If we perform the same random query twice, it shouldn't return the same results */
$itemsA = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random());
$itemsB = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random());
$itemsC = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random());
$itemsD = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random());
foreach($itemsA as $item) $keysA[] = $item->ID;
foreach($itemsB as $item) $keysB[] = $item->ID;
foreach($itemsC as $item) $keysC[] = $item->ID;
foreach($itemsD as $item) $keysD[] = $item->ID;
// These shouldn't all be the same (run it 4 times to minimise chance of an accidental collision)
// There's about a 1 in a billion chance of an accidental collision
$this->assertTrue($keysA != $keysB || $keysB != $keysC || $keysC != $keysD);
}
function testWriteSavesToHasOneRelations() {
@ -764,8 +832,8 @@ class DataObjectTest extends SapphireTest {
*/
function testManyManyUnlimitedRowCount() {
$player = $this->objFromFixture('DataObjectTest_Player', 'player2');
$query = $player->getManyManyComponentsQuery('Teams');
$this->assertEquals(2, $query->unlimitedRowCount());
// TODO: What's going on here?
$this->assertEquals(2, $player->Teams()->dataQuery()->query()->unlimitedRowCount());
}
/**
@ -1000,11 +1068,14 @@ class DataObjectTest extends SapphireTest {
$objEmpty->Title = '0'; //
$this->assertFalse($objEmpty->isEmpty(), 'Zero value in attribute considered non-empty');
}
}
class DataObjectTest_Player extends Member implements TestOnly {
static $db = array(
'IsRetired' => 'Boolean'
'IsRetired' => 'Boolean',
'ShirtNumber' => 'Varchar',
);
static $has_one = array(

View File

@ -7,6 +7,7 @@ DataObjectTest_Team:
DataObjectTest_Player:
captain1:
FirstName: Captain
ShirtNumber: 007
FavouriteTeam: =>DataObjectTest_Team.team1
Teams: =>DataObjectTest_Team.team1
IsRetired: 1

View File

@ -0,0 +1,14 @@
<?php
class DataQueryTest extends SapphireTest {
/**
* Test the join() method of the DataQuery object
*/
function testJoin() {
$dq = new DataQuery('Member');
$dq->join("INNER JOIN \"Group_Members\" ON \"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
$this->assertContains("INNER JOIN \"Group_Members\" ON \"Group_Members\".\"MemberID\" = \"Member\".\"ID\"", $dq->sql());
}
}
?>

View File

@ -0,0 +1,247 @@
<?php
/**
* Tests for the {@link PaginatedList} class.
*
* @package sapphire
* @subpackage tests
*/
class PaginatedListTest extends SapphireTest {
public function testPageStart() {
$list = new PaginatedList(new ArrayList());
$this->assertEquals(0, $list->getPageStart(), 'The start defaults to 0.');
$list->setPageStart(10);
$this->assertEquals(10, $list->getPageStart(), 'You can set the page start.');
$list = new PaginatedList(new ArrayList(), array('start' => 50));
$this->assertEquals(50, $list->getPageStart(), 'The page start can be read from the request.');
}
public function testGetTotalItems() {
$list = new PaginatedList(new ArrayList());
$this->assertEquals(0, $list->getTotalItems());
$list->setTotalItems(10);
$this->assertEquals(10, $list->getTotalItems());
$list = new PaginatedList(new ArrayList(array(
new ArrayData(array()),
new ArrayData(array())
)));
$this->assertEquals(2, $list->getTotalItems());
}
public function testSetPaginationFromQuery() {
$query = $this->getMock('SQLQuery');
$query->limit = array('limit' => 15, 'start' => 30);
$query->expects($this->once())
->method('unlimitedRowCount')
->will($this->returnValue(100));
$list = new PaginatedList(new ArrayList());
$list->setPaginationFromQuery($query);
$this->assertEquals(15, $list->getPageLength());
$this->assertEquals(30, $list->getPageStart());
$this->assertEquals(100, $list->getTotalItems());
}
public function testSetCurrentPage() {
$list = new PaginatedList(new ArrayList());
$list->setPageLength(10);
$list->setCurrentPage(10);
$this->assertEquals(10, $list->CurrentPage());
$this->assertEquals(90, $list->getPageStart());
}
public function testGetIterator() {
$list = new PaginatedList(new ArrayList(array(
new DataObject(array('Num' => 1)),
new DataObject(array('Num' => 2)),
new DataObject(array('Num' => 3)),
new DataObject(array('Num' => 4)),
new DataObject(array('Num' => 5)),
)));
$list->setPageLength(2);
$this->assertDOSEquals(
array(array('Num' => 1), array('Num' => 2)), $list->getIterator()
);
$list->setCurrentPage(2);
$this->assertDOSEquals(
array(array('Num' => 3), array('Num' => 4)), $list->getIterator()
);
$list->setCurrentPage(3);
$this->assertDOSEquals(
array(array('Num' => 5)), $list->getIterator()
);
$list->setCurrentPage(999);
$this->assertDOSEquals(array(), $list->getIterator());
}
public function testPages() {
$list = new PaginatedList(new ArrayList());
$list->setPageLength(10);
$list->setTotalItems(50);
$this->assertEquals(5, count($list->Pages()));
$this->assertEquals(3, count($list->Pages(3)));
$this->assertEquals(5, count($list->Pages(15)));
$list->setCurrentPage(3);
$expectAll = array(
array('PageNum' => 1),
array('PageNum' => 2),
array('PageNum' => 3, 'CurrentBool' => true),
array('PageNum' => 4),
array('PageNum' => 5),
);
$this->assertDOSEquals($expectAll, $list->Pages());
$expectLimited = array(
array('PageNum' => 2),
array('PageNum' => 3, 'CurrentBool' => true),
array('PageNum' => 4),
);
$this->assertDOSEquals($expectLimited, $list->Pages(3));
}
public function testPaginationSummary() {
$list = new PaginatedList(new ArrayList());
$list->setPageLength(10);
$list->setTotalItems(250);
$list->setCurrentPage(6);
$expect = array(
array('PageNum' => 1),
array('PageNum' => null),
array('PageNum' => 4),
array('PageNum' => 5),
array('PageNum' => 6, 'CurrentBool' => true),
array('PageNum' => 7),
array('PageNum' => 8),
array('PageNum' => null),
array('PageNum' => 25),
);
$this->assertDOSEquals($expect, $list->PaginationSummary(4));
}
public function testCurrentPage() {
$list = new PaginatedList(new ArrayList());
$list->setTotalItems(50);
$this->assertEquals(1, $list->CurrentPage());
$list->setPageStart(10);
$this->assertEquals(2, $list->CurrentPage());
$list->setPageStart(40);
$this->assertEquals(5, $list->CurrentPage());
}
public function testTotalPages() {
$list = new PaginatedList(new ArrayList());
$list->setPageLength(1);
$this->assertEquals(0, $list->TotalPages());
$list->setTotalItems(1);
$this->assertEquals(1, $list->TotalPages());
$list->setTotalItems(5);
$this->assertEquals(5, $list->TotalPages());
}
public function testMoreThanOnePage() {
$list = new PaginatedList(new ArrayList());
$list->setPageLength(1);
$list->setTotalItems(1);
$this->assertFalse($list->MoreThanOnePage());
$list->setTotalItems(2);
$this->assertTrue($list->MoreThanOnePage());
}
public function testNotFirstPage() {
$list = new PaginatedList(new ArrayList());
$this->assertFalse($list->NotFirstPage());
$list->setCurrentPage(2);
$this->assertTrue($list->NotFirstPage());
}
public function testNotLastPage() {
$list = new PaginatedList(new ArrayList());
$list->setTotalItems(50);
$this->assertTrue($list->NotLastPage());
$list->setCurrentPage(5);
$this->assertFalse($list->NotLastPage());
}
public function testFirstItem() {
$list = new PaginatedList(new ArrayList());
$this->assertEquals(1, $list->FirstItem());
$list->setPageStart(10);
$this->assertEquals(11, $list->FirstItem());
}
public function testLastItem() {
$list = new PaginatedList(new ArrayList());
$list->setPageLength(10);
$list->setTotalItems(25);
$list->setCurrentPage(1);
$this->assertEquals(10, $list->LastItem());
$list->setCurrentPage(2);
$this->assertEquals(20, $list->LastItem());
$list->setCurrentPage(3);
$this->assertEquals(25, $list->LastItem());
}
public function testFirstLink() {
$list = new PaginatedList(new ArrayList());
$this->assertContains('start=0', $list->FirstLink());
}
public function testLastLink() {
$list = new PaginatedList(new ArrayList());
$list->setPageLength(10);
$list->setTotalItems(100);
$this->assertContains('start=90', $list->LastLink());
}
public function testNextLink() {
$list = new PaginatedList(new ArrayList());
$list->setTotalItems(50);
$this->assertContains('start=10', $list->NextLink());
$list->setCurrentPage(2);
$this->assertContains('start=20', $list->NextLink());
$list->setCurrentPage(3);
$this->assertContains('start=30', $list->NextLink());
$list->setCurrentPage(4);
$this->assertContains('start=40', $list->NextLink());
$list->setCurrentPage(5);
$this->assertNull($list->NextLink());
}
public function testPrevLink() {
$list = new PaginatedList(new ArrayList());
$list->setTotalItems(50);
$this->assertNull($list->PrevLink());
$list->setCurrentPage(2);
$this->assertContains('start=0', $list->PrevLink());
$list->setCurrentPage(3);
$this->assertContains('start=10', $list->PrevLink());
$list->setCurrentPage(5);
$this->assertContains('start=30', $list->PrevLink());
}
}

View File

@ -65,6 +65,7 @@ class SQLQueryTest extends SapphireTest {
}
function testSelectWithPredicateFilters() {
/* this is no longer part of this
$query = new SQLQuery();
$query->select(array("Name"))->from("SQLQueryTest_DO");
@ -77,6 +78,7 @@ class SQLQueryTest extends SapphireTest {
$match->apply($query);
$this->assertEquals("SELECT Name FROM SQLQueryTest_DO WHERE (\"SQLQueryTest_DO\".\"Name\" = 'Value') AND (\"SQLQueryTest_DO\".\"Meta\" LIKE '%Value%')", $query->sql());
*/
}
function testSelectWithLimitClause() {

View File

@ -171,10 +171,10 @@ class VersionedTest extends SapphireTest {
$page->write();
$live = Versioned::get_by_stage('VersionedTest_DataObject', 'Live', "\"VersionedTest_DataObject_Live\".\"ID\"='$page->ID'");
$this->assertNull($live);
$this->assertEquals(0, $live->count());
$stage = Versioned::get_by_stage('VersionedTest_DataObject', 'Stage', "\"VersionedTest_DataObject\".\"ID\"='$page->ID'");
$this->assertNotNull($stage);
$this->assertEquals(1, $stage->count());
$this->assertEquals($stage->First()->Title, 'testWritingNewToStage');
Versioned::reading_stage($origStage);
@ -195,11 +195,11 @@ class VersionedTest extends SapphireTest {
$page->write();
$live = Versioned::get_by_stage('VersionedTest_DataObject', 'Live', "\"VersionedTest_DataObject_Live\".\"ID\"='$page->ID'");
$this->assertNotNull($live->First());
$this->assertEquals(1, $live->count());
$this->assertEquals($live->First()->Title, 'testWritingNewToLive');
$stage = Versioned::get_by_stage('VersionedTest_DataObject', 'Stage', "\"VersionedTest_DataObject\".\"ID\"='$page->ID'");
$this->assertNull($stage);
$this->assertEquals(0, $stage->count());
Versioned::reading_stage($origStage);
}
@ -230,6 +230,18 @@ class VersionedTest extends SapphireTest {
'Models w/o Versioned can have their own Version field.'
);
}
/**
* Test that SQLQuery::queriedTables() applies the version-suffixes properly.
*/
public function testQueriedTables() {
Versioned::reading_stage('Live');
$this->assertEquals(array(
'VersionedTest_DataObject_Live',
'VersionedTest_Subclass_Live',
), DataObject::get('VersionedTest_DataObject')->dataQuery()->query()->queriedTables());
}
}
class VersionedTest_DataObject extends DataObject implements TestOnly {

View File

@ -85,7 +85,7 @@ class SearchContextTest extends SapphireTest {
$context = $company->getDefaultSearchContext();
$fields = $context->getFields();
$this->assertEquals(
new FieldSet(
new FieldList(
new TextField("Name", 'Name'),
new TextareaField("Industry", 'Industry'),
new NumericField("AnnualProfit", 'The Almighty Annual Profit')

View File

@ -62,13 +62,9 @@ class GroupTest extends FunctionalTest {
$form->saveInto($member);
$updatedGroups = $member->Groups();
$controlGroups = new Member_GroupSet(
$adminGroup,
$parentGroup
);
$this->assertEquals(
$updatedGroups->Map('ID','ID'),
$controlGroups->Map('ID','ID'),
array($adminGroup->ID, $parentGroup->ID),
$updatedGroups->column(),
"Adding a toplevel group works"
);
@ -82,12 +78,9 @@ class GroupTest extends FunctionalTest {
$form->saveInto($member);
$member->flushCache();
$updatedGroups = $member->Groups();
$controlGroups = new Member_GroupSet(
$adminGroup
);
$this->assertEquals(
$updatedGroups->Map('ID','ID'),
$controlGroups->Map('ID','ID'),
array($adminGroup->ID),
$updatedGroups->column(),
"Removing a previously added toplevel group works"
);
@ -100,8 +93,8 @@ class GroupTest extends FunctionalTest {
$adminGroup->delete();
$this->assertNull(DataObject::get('Group', "\"ID\"={$adminGroup->ID}"), 'Group is removed');
$this->assertNull(DataObject::get('Permission',"\"GroupID\"={$adminGroup->ID}"), 'Permissions removed along with the group');
$this->assertEquals(0, DataObject::get('Group', "\"ID\"={$adminGroup->ID}")->count(), 'Group is removed');
$this->assertEquals(0, DataObject::get('Permission',"\"GroupID\"={$adminGroup->ID}")->count(), 'Permissions removed along with the group');
}
function testCollateAncestorIDs() {
@ -135,8 +128,8 @@ class GroupTest_Member extends Member implements TestOnly {
function getCMSFields() {
$groups = DataObject::get('Group');
$groupsMap = ($groups) ? $groups->toDropDownMap() : false;
$fields = new FieldSet(
$groupsMap = ($groups) ? $groups->map() : false;
$fields = new FieldList(
new HiddenField('ID', 'ID'),
new CheckboxSetField(
'Groups',
@ -154,7 +147,7 @@ class GroupTest_MemberForm extends Form {
function __construct($controller, $name) {
$fields = singleton('GroupTest_Member')->getCMSFields();
$actions = new FieldSet(
$actions = new FieldList(
new FormAction('doSave','save')
);

View File

@ -140,18 +140,18 @@ class MemberTest extends FunctionalTest {
$passwords = DataObject::get("MemberPassword", "\"MemberID\" = $member->ID", "\"Created\" DESC, \"ID\" DESC")->getIterator();
$this->assertNotNull($passwords);
$record = $passwords->rewind();
$this->assertTrue($record->checkPassword('test3'), "Password test3 not found in MemberRecord");
$passwords->rewind();
$this->assertTrue($passwords->current()->checkPassword('test3'), "Password test3 not found in MemberRecord");
$record = $passwords->next();
$this->assertTrue($record->checkPassword('test2'), "Password test2 not found in MemberRecord");
$passwords->next();
$this->assertTrue($passwords->current()->checkPassword('test2'), "Password test2 not found in MemberRecord");
$record = $passwords->next();
$this->assertTrue($record->checkPassword('test1'), "Password test1 not found in MemberRecord");
$passwords->next();
$this->assertTrue($passwords->current()->checkPassword('test1'), "Password test1 not found in MemberRecord");
$record = $passwords->next();
$this->assertType('DataObject', $record);
$this->assertTrue($record->checkPassword('1nitialPassword'), "Password 1nitialPassword not found in MemberRecord");
$passwords->next();
$this->assertType('DataObject', $passwords->current());
$this->assertTrue($passwords->current()->checkPassword('1nitialPassword'), "Password 1nitialPassword not found in MemberRecord");
}
/**

View File

@ -11,7 +11,7 @@ class PermissionRoleTest extends FunctionalTest {
$role->delete();
$this->assertNull(DataObject::get('PermissionRole', "\"ID\"={$role->ID}"), 'Role is removed');
$this->assertNull(DataObject::get('PermissionRoleCode',"\"RoleID\"={$role->ID}"), 'Permissions removed along with the role');
$this->assertEquals(0, DataObject::get('PermissionRole', "\"ID\"={$role->ID}")->count(), 'Role is removed');
$this->assertEquals(0, DataObject::get('PermissionRoleCode',"\"RoleID\"={$role->ID}")->count(), 'Permissions removed along with the role');
}
}

View File

@ -64,4 +64,20 @@ class PermissionTest extends SapphireTest {
'Member is found via a permission attached to a role');
$this->assertNotContains($accessAuthor->ID, $resultIDs);
}
function testHiddenPermissions(){
$permissionCheckboxSet = new PermissionCheckboxSetField('Permissions','Permissions','Permission','GroupID');
$this->assertContains('CMS_ACCESS_CMSMain', $permissionCheckboxSet->Field());
$this->assertContains('CMS_ACCESS_AssetAdmin', $permissionCheckboxSet->Field());
Permission::add_to_hidden_permissions('CMS_ACCESS_CMSMain');
Permission::add_to_hidden_permissions('CMS_ACCESS_AssetAdmin');
$this->assertNotContains('CMS_ACCESS_CMSMain', $permissionCheckboxSet->Field());
$this->assertNotContains('CMS_ACCESS_AssetAdmin', $permissionCheckboxSet->Field());
Permission::remove_from_hidden_permissions('CMS_ACCESS_AssetAdmin');
$this->assertContains('CMS_ACCESS_AssetAdmin', $permissionCheckboxSet->Field());
Permission::remove_from_hidden_permissions('CMS_ACCESS_CMSMain');
}
}

View File

@ -36,7 +36,7 @@ class SecurityDefaultAdminTest extends SapphireTest {
function testFindAnAdministratorCreatesNewUser() {
$adminMembers = Permission::get_members_by_permission('ADMIN');
$this->assertFalse($adminMembers);
$this->assertEquals(0, $adminMembers->count());
$admin = Security::findAnAdministrator();

View File

@ -106,7 +106,7 @@ class SecurityTokenTest extends SapphireTest {
}
function testUpdateFieldSet() {
$fs = new FieldSet();
$fs = new FieldList();
$t = new SecurityToken();
$t->updateFieldSet($fs);
$f = $fs->dataFieldByName($t->getName());
@ -117,7 +117,7 @@ class SecurityTokenTest extends SapphireTest {
}
function testUpdateFieldSetDoesntAddTwice() {
$fs = new FieldSet();
$fs = new FieldList();
$t = new SecurityToken();
$t->updateFieldSet($fs); // first
$t->updateFieldSet($fs); // second

View File

@ -332,10 +332,10 @@ after')
$data = new ArrayData(array(
'Title' => 'A',
'Children' => new DataObjectSet(array(
'Children' => new ArrayList(array(
new ArrayData(array(
'Title' => 'A1',
'Children' => new DataObjectSet(array(
'Children' => new ArrayList(array(
new ArrayData(array( 'Title' => 'A1 i', )),
new ArrayData(array( 'Title' => 'A1 ii', )),
)),
@ -415,7 +415,7 @@ after')
// Data to run the loop tests on - one sequence of three items, each with a subitem
$data = new ArrayData(array(
'Name' => 'Top',
'Foo' => new DataObjectSet(array(
'Foo' => new ArrayList(array(
new ArrayData(array(
'Name' => '1',
'Sub' => new ArrayData(array(
@ -538,7 +538,7 @@ class SSViewerTestFixture extends ViewableData {
// Special field name Loop### to create a list
if(preg_match('/^Loop([0-9]+)$/', $fieldName, $matches)) {
$output = new DataObjectSet();
$output = new ArrayList();
for($i=0;$i<$matches[1];$i++) $output->push(new SSViewerTestFixture($childName));
return $output;

View File

@ -43,7 +43,7 @@ class ArrayData extends ViewableData {
*
* @return array
*/
public function getArray() {
public function toMap() {
return $this->array;
}
@ -107,4 +107,12 @@ class ArrayData extends ViewableData {
function forTemplate() {
return var_export($this->array, true);
}
/**
* @deprecated 3.0 Use {@link ArrayData::toMap()}.
*/
public function getArray() {
return $this->toMap();
}
}