7 * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
8 * @copyright Copyright (c) 2007-2011 Metaways Infosystems GmbH (http://www.metaways.de)
9 * @author Cornelius Weiss <c.weiss@metaways.de>
13 * class to hold a list of records
15 * records are held as a unsorted set with a autoasigned numeric index.
16 * NOTE: the index of an record is _not_ related to the record and/or its identifier!
22 class Tinebase_Record_RecordSet implements IteratorAggregate, Countable, ArrayAccess
25 * class name of records this instance can hold
28 protected $_recordClass;
34 protected $_listOfRecords = array();
37 * holds mapping id -> offset in $_listOfRecords
40 protected $_idMap = array();
43 * holds offsets of idless (new) records in $_listOfRecords
46 protected $_idLess = array();
49 * Holds validation errors
52 protected $_validationErrors = array();
55 * creates new Tinebase_Record_RecordSet
57 * @param string $_className the required classType
58 * @param array|Tinebase_Record_RecordSet $_records array of record objects
59 * @param bool $_bypassFilters {@see Tinebase_Record_Interface::__construct}
60 * @param bool $_convertDates {@see Tinebase_Record_Interface::__construct}
62 * @throws Tinebase_Exception_InvalidArgument
64 public function __construct($_className, $_records = array(), $_bypassFilters = false, $_convertDates = true)
66 if (! class_exists($_className)) {
67 throw new Tinebase_Exception_InvalidArgument('Class ' . $_className . ' does not exist');
69 $this->_recordClass = $_className;
71 foreach ($_records as $record) {
72 $toAdd = $record instanceof Tinebase_Record_Abstract ? $record : new $this->_recordClass($record, $_bypassFilters, $_convertDates);
73 $this->addRecord($toAdd);
80 public function __clone()
82 foreach ($this->_listOfRecords as $key => $record) {
83 $this->_listOfRecords[$key] = clone $record;
88 * returns name of record class this recordSet contains
92 public function getRecordClassName()
94 return $this->_recordClass;
98 * add Tinebase_Record_Interface like object to internal list
100 * @param Tinebase_Record_Interface $_record
101 * @param integer $_index
102 * @return int index in set of inserted record
104 public function addRecord(Tinebase_Record_Interface $_record, $_index = NULL)
106 if (! $_record instanceof $this->_recordClass) {
107 throw new Tinebase_Exception_Record_NotAllowed('Attempt to add/set record of wrong record class. Should be ' . $this->_recordClass);
109 $this->_listOfRecords[] = $_record;
110 end($this->_listOfRecords);
111 $index = ($_index !== NULL) ? $_index : key($this->_listOfRecords);
114 $recordId = $_record->getId();
116 $this->_idMap[$recordId] = $index;
118 $this->_idLess[] = $index;
125 * removes all records from this set
127 public function removeAll()
129 foreach($this->_listOfRecords as $record) {
130 $this->removeRecord($record);
135 * remove record from set
137 * @param Tinebase_Record_Interface $_record
139 public function removeRecord(Tinebase_Record_Interface $_record)
141 $idx = $this->indexOf($_record);
142 if ($idx !== false) {
143 $this->offsetUnset($idx);
148 * remove records from set
150 * @param Tinebase_Record_RecordSet $_records
152 public function removeRecords(Tinebase_Record_RecordSet $_records)
154 foreach ($_records as $record) {
155 $this->removeRecord($record);
160 * get index of given record
162 * @param Tinebase_Record_Interface $_record
163 * @return (int) index of record of false if not found
165 public function indexOf(Tinebase_Record_Interface $_record)
167 return array_search($_record, $this->_listOfRecords);
171 * checks if each member record of this set is valid
175 public function isValid()
177 foreach ($this->_listOfRecords as $index => $record) {
178 if (!$record->isValid()) {
179 $this->_validationErrors[$index] = $record->getValidationErrors();
182 return !(bool)count($this->_validationErrors);
186 * returns array of array of fields with validation errors
188 * @return array index => validationErrors
190 public function getValidationErrors()
192 return $this->_validationErrors;
196 * converts RecordSet to array
197 * NOTE: keys of the array are numeric and have _noting_ to do with internal indexes or identifiers
201 public function toArray()
203 $resultArray = array();
204 foreach($this->_listOfRecords as $index => $record) {
205 $resultArray[$index] = $record->toArray();
208 return array_values($resultArray);
212 * returns index of record identified by its id
214 * @param string $_id id of record
215 * @return int|bool index of record or false if not in set
217 public function getIndexById($_id)
219 return (isset($this->_idMap[$_id]) || array_key_exists($_id, $this->_idMap)) ? $this->_idMap[$_id] : false;
223 * returns record identified by its id
225 * @param string $_id id of record
226 * @return Tinebase_Record_Abstract::|bool record or false if not in set
228 public function getById($_id)
230 $idx = $this->getIndexById($_id);
232 return $idx !== false ? $this[$idx] : false;
236 * returns record identified by its id
238 * @param integer $index of record
239 * @return Tinebase_Record_Abstract::|bool record or false if not in set
241 public function getByIndex($index)
243 return (isset($this->_listOfRecords[$index])) ? $this->_listOfRecords[$index] : false;
247 * returns array of ids
249 public function getArrayOfIds()
251 return array_keys($this->_idMap);
255 * returns array of ids
257 public function getArrayOfIdsAsString()
259 $ids = array_keys($this->_idMap);
260 foreach($ids as $key => $id) {
261 $ids[$key] = (string) $id;
267 * returns array with idless (new) records in this set
271 public function getIdLessIndexes()
273 return array_values($this->_idLess);
277 * sets given property in all records with data from given values identified by their indices
279 * @param string $_name property name
280 * @param array $_values index => property value
281 * @param boolean $skipMissing
282 * @throws Tinebase_Exception_Record_NotDefined
284 public function setByIndices($_name, array $_values, $skipMissing = false)
286 foreach ($_values as $index => $value) {
287 if (! (isset($this->_listOfRecords[$index]) || array_key_exists($index, $this->_listOfRecords))) {
289 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
290 . ' Skip missing record ' . $index . ' => ' . $value . ' property: ' . $_name);
293 throw new Tinebase_Exception_Record_NotDefined('Could not find record with index ' . $index);
296 $this->_listOfRecords[$index]->$_name = $value;
301 * Sets timezone of $this->_datetimeFields
303 * @see Tinebase_DateTime::setTimezone()
304 * @param string $_timezone
305 * @param bool $_recursive
307 * @throws Tinebase_Exception_Record_Validation
309 public function setTimezone($_timezone, $_recursive = TRUE)
311 $returnValues = array();
312 foreach ($this->_listOfRecords as $index => $record) {
313 $returnValues[$index] = $record->setTimezone($_timezone, $_recursive);
316 return $returnValues;
320 * sets given property in all member records of this set
322 * @param string $_name
323 * @param mixed $_value
326 public function __set($_name, $_value)
328 foreach ($this->_listOfRecords as $record) {
329 $record->$_name = $_value;
334 * returns an array with the properties of all records in this set
336 * @param string $_name property
337 * @return array index => property
339 public function __get($_name)
341 $propertiesArray = array();
343 foreach ($this->_listOfRecords as $index => $record) {
344 $propertiesArray[$index] = $record->$_name;
347 return $propertiesArray;
351 * executes given function in all records
353 * @param string $_fname
354 * @param array $_arguments
355 * @return array array index => return value
357 public function __call($_fname, $_arguments)
359 $returnValues = array();
360 foreach ($this->_listOfRecords as $index => $record) {
361 $returnValues[$index] = call_user_func_array(array($record, $_fname), $_arguments);
364 return $returnValues;
367 /** convert this to string
371 public function __toString()
373 return print_r($this->toArray(), TRUE);
377 * Returns the number of elements in the recordSet.
378 * required by interface Countable
382 public function count()
384 return count($this->_listOfRecords);
388 * required by IteratorAggregate interface
392 public function getIterator()
394 return new ArrayIterator($this->_listOfRecords);
398 * required by ArrayAccess interface
400 public function offsetExists($_offset)
402 return isset($this->_listOfRecords[$_offset]);
406 * required by ArrayAccess interface
408 public function offsetGet($_offset)
410 if (! is_int($_offset)) {
411 throw new Tinebase_Exception_UnexpectedValue("index must be of type integer (". gettype($_offset) .") " . $_offset . ' given');
413 if (! (isset($this->_listOfRecords[$_offset]) || array_key_exists($_offset, $this->_listOfRecords))) {
414 throw new Tinebase_Exception_NotFound("No such entry with index $_offset in this record set");
417 return $this->_listOfRecords[$_offset];
421 * required by ArrayAccess interface
423 public function offsetSet($_offset, $_value)
425 if (! $_value instanceof $this->_recordClass) {
426 throw new Tinebase_Exception_Record_NotAllowed('Attempt to add/set record of wrong record class. Should be ' . $this->_recordClass);
429 if (!is_int($_offset)) {
430 $this->addRecord($_value);
432 if (!(isset($this->_listOfRecords[$_offset]) || array_key_exists($_offset, $this->_listOfRecords))) {
433 throw new Tinebase_Exception_Record_NotAllowed('adding a record is only allowd via the addRecord method');
435 $this->_listOfRecords[$_offset] = $_value;
436 $id = $_value->getId();
438 if(! (isset($this->_idMap[$id]) || array_key_exists($id, $this->_idMap))) {
439 $this->_idMap[$id] = $_offset;
440 $idLessIdx = array_search($_offset, $this->_idLess);
441 unset($this->_idLess[$idLessIdx]);
444 if (array_search($_offset, $this->_idLess) === false) {
445 $this->_idLess[] = $_offset;
446 $idMapIdx = array_search($_offset, $this->_idMap);
447 unset($this->_idMap[$idMapIdx]);
454 * required by ArrayAccess interface
456 public function offsetUnset($_offset)
458 $id = $this->_listOfRecords[$_offset]->getId();
460 unset($this->_idMap[$id]);
462 $idLessIdx = array_search($_offset, $this->_idLess);
463 unset($this->_idLess[$idLessIdx]);
466 unset($this->_listOfRecords[$_offset]);
470 * Returns an array with ids of records to delete, to create or to update
472 * @param array $_toCompareWithRecordsIds Array to compare this record sets ids with
473 * @return array An array with sub array indices 'toDeleteIds', 'toCreateIds' and 'toUpdateIds'
475 * @deprecated please use diff() as this returns wrong result when idless records have been added
476 * @see 0007492: replace getMigration() with diff() when comparing Tinebase_Record_RecordSets
478 public function getMigration(array $_toCompareWithRecordsIds)
480 $existingRecordsIds = $this->getArrayOfIds();
484 $result['toDeleteIds'] = array_diff($existingRecordsIds, $_toCompareWithRecordsIds);
485 $result['toCreateIds'] = array_diff($_toCompareWithRecordsIds, $existingRecordsIds);
486 $result['toUpdateIds'] = array_intersect($existingRecordsIds, $_toCompareWithRecordsIds);
492 * adds indices to this record set
494 * @param array $_properties
497 public function addIndices(array $_properties)
503 * filter recordset and return subset
505 * @param string $_field
506 * @param string $_value
507 * @return Tinebase_Record_RecordSet
509 public function filter($_field, $_value = NULL, $_valueIsRegExp = FALSE)
511 $matchingRecords = $this->_getMatchingRecords($_field, $_value, $_valueIsRegExp);
513 $result = new Tinebase_Record_RecordSet($this->_recordClass, $matchingRecords);
519 * returns new set with records of this set
521 * @param bool $recordsByRef
522 * @return Tinebase_Record_RecordSet
524 public function getClone($recordsByRef=false)
527 $result = new Tinebase_Record_RecordSet($this->_recordClass, $this->_listOfRecords);
529 $result = clone $this;
536 * Finds the first matching record in this store by a specific property/value.
538 * @param string $_field
539 * @param string $_value
540 * @return Tinebase_Record_Abstract
542 public function find($_field, $_value, $_valueIsRegExp = FALSE)
544 $matchingRecords = array_values($this->_getMatchingRecords($_field, $_value, $_valueIsRegExp));
545 return count($matchingRecords) > 0 ? $matchingRecords[0] : NULL;
549 * filter recordset and return matching records
551 * @param string|function $_field
552 * @param string $_value
553 * @param boolean $_valueIsRegExp
556 protected function _getMatchingRecords($_field, $_value, $_valueIsRegExp = FALSE)
558 if (!is_string($_field) && is_callable($_field)) {
559 $matchingRecords = array_filter($this->_listOfRecords, $_field);
561 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " Filtering field '$_field' of '{$this->_recordClass}' without indices, expecting slow results");
562 $valueMap = $this->$_field;
564 if ($_valueIsRegExp) {
565 $matchingMap = preg_grep($_value, $valueMap);
567 $matchingMap = array_flip((array)array_keys($valueMap, $_value));
570 $matchingRecords = array_intersect_key($this->_listOfRecords, $matchingMap);
573 return $matchingRecords;
577 * returns first record of this set
579 * @return Tinebase_Record_Abstract|NULL
581 public function getFirstRecord()
583 if (count($this->_listOfRecords) > 0) {
584 foreach ($this->_listOfRecords as $idx => $record) {
593 * compares two recordsets / only compares the ids / returns all records that are different in an array:
594 * - removed -> all records that are in $this but not in $_recordSet
595 * - added -> all records that are in $_recordSet but not in $this
596 * - modified -> array of diffs for all different records that are in both record sets
598 * @param Tinebase_Record_RecordSet $recordSet
599 * @return Tinebase_Record_RecordSetDiff
601 public function diff($recordSet)
603 if (! $recordSet instanceof Tinebase_Record_RecordSet) {
604 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
605 . ' Did not get Tinebase_Record_RecordSet, skipping diff(' . $this->_recordClass . ')');
606 return new Tinebase_Record_RecordSetDiff(array(
607 'model' => $this->getRecordClassName()
611 if ($this->getRecordClassName() !== $recordSet->getRecordClassName()) {
612 throw new Tinebase_Exception_InvalidArgument('can only compare recordsets with the same type of records');
615 $existingRecordsIds = $this->getArrayOfIds();
616 $toCompareWithRecordsIds = $recordSet->getArrayOfIds();
618 $removedIds = array_diff($existingRecordsIds, $toCompareWithRecordsIds);
619 $addedIds = array_diff($toCompareWithRecordsIds, $existingRecordsIds);
620 $modifiedIds = array_intersect($existingRecordsIds, $toCompareWithRecordsIds);
622 $removed = new Tinebase_Record_RecordSet($this->getRecordClassName());
623 $added = new Tinebase_Record_RecordSet($this->getRecordClassName());
624 $modified = new Tinebase_Record_RecordSet('Tinebase_Record_Diff');
626 foreach ($addedIds as $id) {
627 $added->addRecord($recordSet->getById($id));
629 // consider records without id, too
630 foreach ($recordSet->getIdLessIndexes() as $index) {
631 $added->addRecord($recordSet->getByIndex($index));
633 foreach ($removedIds as $id) {
634 $removed->addRecord($this->getById($id));
636 // consider records without id, too
637 foreach ($this->getIdLessIndexes() as $index) {
638 $removed->addRecord($this->getByIndex($index));
640 foreach ($modifiedIds as $id) {
641 $diff = $this->getById($id)->diff($recordSet->getById($id));
642 if (! $diff->isEmpty()) {
643 $modified->addRecord($diff);
647 $result = new Tinebase_Record_RecordSetDiff(array(
648 'model' => $this->getRecordClassName(),
650 'removed' => $removed,
651 'modified' => $modified,
658 * merges records from given record set
660 * @param Tinebase_Record_RecordSet $_recordSet
663 public function merge(Tinebase_Record_RecordSet $_recordSet)
665 foreach ($_recordSet as $record) {
666 if (! in_array($record, $this->_listOfRecords, true)) {
667 $this->addRecord($record);
675 * sorts this recordset
677 * @param string $_field
678 * @param string $_direction
679 * @param string $_sortFunction
680 * @param int $_flags sort flags for asort/arsort
683 public function sort($_field, $_direction = 'ASC', $_sortFunction = 'asort', $_flags = SORT_REGULAR)
685 if (! is_string($_field) && is_callable($_field)) {
686 $_sortFunction = 'function';
688 $offsetToSortFieldMap = $this->__get($_field);
691 switch ($_sortFunction) {
693 $fn = $_direction == 'ASC' ? 'asort' : 'arsort';
694 $fn($offsetToSortFieldMap, $_flags);
697 natcasesort($offsetToSortFieldMap);
698 if ($_direction == 'DESC') {
699 // @todo check if this is working
700 $offsetToSortFieldMap = array_reverse($offsetToSortFieldMap);
704 uasort ($this->_listOfRecords , $_field);
705 $offsetToSortFieldMap = $this->_listOfRecords;
708 throw new Tinebase_Exception_InvalidArgument('Sort function unknown.');
712 $oldListOfRecords = $this->_listOfRecords;
714 // reset indexes and records
715 $this->_idLess = array();
716 $this->_idMap = array();
717 $this->_listOfRecords = array();
719 foreach (array_keys($offsetToSortFieldMap) as $oldOffset) {
720 $this->addRecord($oldListOfRecords[$oldOffset]);
727 * sorts this recordset by pagination sort info
729 * @param Tinebase_Model_Pagination $_pagination
732 public function sortByPagination($_pagination)
734 if ($_pagination !== NULL && $_pagination->sort) {
735 $sortField = is_array($_pagination->sort) ? $_pagination->sort[0] : $_pagination->sort;
736 $this->sort($sortField, ($_pagination->dir) ? $_pagination->dir : 'ASC');
743 * limits this recordset by pagination
744 * sorting should always be applied before to get the desired sequence
745 * @param Tinebase_Model_Pagination $_pagination
748 public function limitByPagination($_pagination)
750 if ($_pagination !== NULL && $_pagination->limit) {
751 $indices = range($_pagination->start, $_pagination->start + $_pagination->limit - 1);
752 foreach($this as $index => &$record) {
753 if(! in_array($index, $indices)) {
754 $this->offsetUnset($index);
762 * translate all member records of this set
765 public function translate()
767 foreach ($this->_listOfRecords as $record) {
768 $record->translate();