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 (it is not inserted if record is already in set)
100 * @param Tinebase_Record_Interface $_record
101 * @param integer $_index
102 * @return int index in set of inserted record or index of existing 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 ('
108 . get_class($_record) . ') Should be ' . $this->_recordClass);
111 $recordId = $_record->getId();
113 if ($recordId && isset($this->_idMap[$recordId]) && isset($this->_listOfRecords[$this->_idMap[$recordId]])) {
114 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
115 . ' Record (id ' . $recordId . ') already in set - we don\'t want duplicates)');
116 return $this->_idMap[$recordId];
119 $this->_listOfRecords[] = $_record;
120 end($this->_listOfRecords);
121 $index = ($_index !== NULL) ? $_index : key($this->_listOfRecords);
125 $this->_idMap[$recordId] = $index;
127 $this->_idLess[] = $index;
134 * removes all records from this set
136 public function removeAll()
138 foreach ($this->_listOfRecords as $record) {
139 $this->removeRecord($record);
144 * remove record from set
146 * @param Tinebase_Record_Interface $_record
148 public function removeRecord(Tinebase_Record_Interface $_record)
150 $idx = $this->indexOf($_record);
151 if ($idx !== false) {
152 $this->offsetUnset($idx);
157 * remove records from set
159 * @param Tinebase_Record_RecordSet $_records
161 public function removeRecords(Tinebase_Record_RecordSet $_records)
163 foreach ($_records as $record) {
164 $this->removeRecord($record);
169 * get index of given record
171 * @param Tinebase_Record_Interface $_record
172 * @return (int) index of record of false if not found
174 public function indexOf(Tinebase_Record_Interface $_record)
176 return array_search($_record, $this->_listOfRecords);
180 * checks if each member record of this set is valid
184 public function isValid()
186 foreach ($this->_listOfRecords as $index => $record) {
187 if (!$record->isValid()) {
188 $this->_validationErrors[$index] = $record->getValidationErrors();
191 return !(bool)count($this->_validationErrors);
195 * returns array of array of fields with validation errors
197 * @return array index => validationErrors
199 public function getValidationErrors()
201 return $this->_validationErrors;
205 * converts RecordSet to array
206 * NOTE: keys of the array are numeric and have _noting_ to do with internal indexes or identifiers
210 public function toArray()
212 $resultArray = array();
213 foreach($this->_listOfRecords as $index => $record) {
214 $resultArray[$index] = $record->toArray();
217 return array_values($resultArray);
221 * returns index of record identified by its id
223 * @param string $_id id of record
224 * @return int|bool index of record or false if not in set
226 public function getIndexById($_id)
228 return (isset($this->_idMap[$_id]) || array_key_exists($_id, $this->_idMap)) ? $this->_idMap[$_id] : false;
232 * returns record identified by its id
234 * @param string $_id id of record
235 * @return Tinebase_Record_Abstract::|bool record or false if not in set
237 public function getById($_id)
239 $idx = $this->getIndexById($_id);
241 return $idx !== false ? $this[$idx] : false;
245 * returns record identified by its id
247 * @param integer $index of record
248 * @return Tinebase_Record_Abstract::|bool record or false if not in set
250 public function getByIndex($index)
252 return (isset($this->_listOfRecords[$index])) ? $this->_listOfRecords[$index] : false;
256 * returns array of ids
258 public function getArrayOfIds()
260 return array_keys($this->_idMap);
264 * returns array of ids
266 public function getArrayOfIdsAsString()
268 $ids = array_keys($this->_idMap);
269 foreach($ids as $key => $id) {
270 $ids[$key] = (string) $id;
276 * returns array with idless (new) records in this set
280 public function getIdLessIndexes()
282 return array_values($this->_idLess);
286 * sets given property in all records with data from given values identified by their indices
288 * @param string $_name property name
289 * @param array $_values index => property value
290 * @param boolean $skipMissing
291 * @throws Tinebase_Exception_Record_NotDefined
293 public function setByIndices($_name, array $_values, $skipMissing = false)
295 foreach ($_values as $index => $value) {
296 if (! (isset($this->_listOfRecords[$index]) || array_key_exists($index, $this->_listOfRecords))) {
298 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
299 . ' Skip missing record ' . $index . ' => ' . $value . ' property: ' . $_name);
302 throw new Tinebase_Exception_Record_NotDefined('Could not find record with index ' . $index);
305 $this->_listOfRecords[$index]->$_name = $value;
310 * Sets timezone of $this->_datetimeFields
312 * @see Tinebase_DateTime::setTimezone()
313 * @param string $_timezone
314 * @param bool $_recursive
316 * @throws Tinebase_Exception_Record_Validation
318 public function setTimezone($_timezone, $_recursive = TRUE)
320 $returnValues = array();
321 foreach ($this->_listOfRecords as $index => $record) {
322 $returnValues[$index] = $record->setTimezone($_timezone, $_recursive);
325 return $returnValues;
329 * sets given property in all member records of this set
331 * @param string $_name
332 * @param mixed $_value
335 public function __set($_name, $_value)
337 foreach ($this->_listOfRecords as $record) {
338 $record->$_name = $_value;
343 * returns an array with the properties of all records in this set
345 * @param string $_name property
346 * @return array index => property
348 public function __get($_name)
350 $propertiesArray = array();
352 foreach ($this->_listOfRecords as $index => $record) {
353 $propertiesArray[$index] = $record->$_name;
356 return $propertiesArray;
360 * executes given function in all records
362 * @param string $_fname
363 * @param array $_arguments
364 * @return array array index => return value
366 public function __call($_fname, $_arguments)
368 $returnValues = array();
369 foreach ($this->_listOfRecords as $index => $record) {
370 $returnValues[$index] = call_user_func_array(array($record, $_fname), $_arguments);
373 return $returnValues;
376 /** convert this to string
380 public function __toString()
382 return print_r($this->toArray(), TRUE);
386 * Returns the number of elements in the recordSet.
387 * required by interface Countable
391 public function count()
393 return count($this->_listOfRecords);
397 * required by IteratorAggregate interface
401 public function getIterator()
403 return new ArrayIterator($this->_listOfRecords);
407 * required by ArrayAccess interface
409 public function offsetExists($_offset)
411 return isset($this->_listOfRecords[$_offset]);
415 * required by ArrayAccess interface
417 public function offsetGet($_offset)
419 if (! is_int($_offset)) {
420 throw new Tinebase_Exception_UnexpectedValue("index must be of type integer (". gettype($_offset) .") " . $_offset . ' given');
422 if (! (isset($this->_listOfRecords[$_offset]) || array_key_exists($_offset, $this->_listOfRecords))) {
423 throw new Tinebase_Exception_NotFound("No such entry with index $_offset in this record set");
426 return $this->_listOfRecords[$_offset];
430 * required by ArrayAccess interface
432 public function offsetSet($_offset, $_value)
434 if (! $_value instanceof $this->_recordClass) {
435 throw new Tinebase_Exception_Record_NotAllowed('Attempt to add/set record of wrong record class. Should be ' . $this->_recordClass);
438 if (!is_int($_offset)) {
439 $this->addRecord($_value);
441 if (!(isset($this->_listOfRecords[$_offset]) || array_key_exists($_offset, $this->_listOfRecords))) {
442 throw new Tinebase_Exception_Record_NotAllowed('adding a record is only allowd via the addRecord method');
444 $this->_listOfRecords[$_offset] = $_value;
445 $id = $_value->getId();
447 if(! (isset($this->_idMap[$id]) || array_key_exists($id, $this->_idMap))) {
448 $this->_idMap[$id] = $_offset;
449 $idLessIdx = array_search($_offset, $this->_idLess);
450 unset($this->_idLess[$idLessIdx]);
453 if (array_search($_offset, $this->_idLess) === false) {
454 $this->_idLess[] = $_offset;
455 $idMapIdx = array_search($_offset, $this->_idMap);
456 unset($this->_idMap[$idMapIdx]);
463 * required by ArrayAccess interface
465 public function offsetUnset($_offset)
467 $id = $this->_listOfRecords[$_offset]->getId();
469 unset($this->_idMap[$id]);
471 $idLessIdx = array_search($_offset, $this->_idLess);
472 unset($this->_idLess[$idLessIdx]);
475 unset($this->_listOfRecords[$_offset]);
479 * Returns an array with ids of records to delete, to create or to update
481 * @param array $_toCompareWithRecordsIds Array to compare this record sets ids with
482 * @return array An array with sub array indices 'toDeleteIds', 'toCreateIds' and 'toUpdateIds'
484 * @deprecated please use diff() as this returns wrong result when idless records have been added
485 * @see 0007492: replace getMigration() with diff() when comparing Tinebase_Record_RecordSets
487 public function getMigration(array $_toCompareWithRecordsIds)
489 $existingRecordsIds = $this->getArrayOfIds();
493 $result['toDeleteIds'] = array_diff($existingRecordsIds, $_toCompareWithRecordsIds);
494 $result['toCreateIds'] = array_diff($_toCompareWithRecordsIds, $existingRecordsIds);
495 $result['toUpdateIds'] = array_intersect($existingRecordsIds, $_toCompareWithRecordsIds);
501 * adds indices to this record set
503 * @param array $_properties
508 public function addIndices(array $_properties)
514 * filter recordset and return subset
516 * @param string $_field
517 * @param string $_value
518 * @return Tinebase_Record_RecordSet
520 public function filter($_field, $_value = NULL, $_valueIsRegExp = FALSE)
522 $matchingRecords = $this->_getMatchingRecords($_field, $_value, $_valueIsRegExp);
524 $result = new Tinebase_Record_RecordSet($this->_recordClass, $matchingRecords);
530 * returns new set with records of this set
532 * @param bool $recordsByRef
533 * @return Tinebase_Record_RecordSet
535 public function getClone($recordsByRef=false)
538 $result = new Tinebase_Record_RecordSet($this->_recordClass, $this->_listOfRecords);
540 $result = clone $this;
547 * Finds the first matching record in this store by a specific property/value.
549 * @param string $_field
550 * @param string $_value
551 * @return Tinebase_Record_Abstract
553 public function find($_field, $_value, $_valueIsRegExp = FALSE)
555 $matchingRecords = array_values($this->_getMatchingRecords($_field, $_value, $_valueIsRegExp));
556 return count($matchingRecords) > 0 ? $matchingRecords[0] : NULL;
560 * filter recordset and return matching records
562 * @param string|function $_field
563 * @param string $_value
564 * @param boolean $_valueIsRegExp
567 protected function _getMatchingRecords($_field, $_value, $_valueIsRegExp = FALSE)
569 if (!is_string($_field) && is_callable($_field)) {
570 $matchingRecords = array_filter($this->_listOfRecords, $_field);
572 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " Filtering field '$_field' of '{$this->_recordClass}' without indices, expecting slow results");
573 $valueMap = $this->$_field;
575 if ($_valueIsRegExp) {
576 $matchingMap = preg_grep($_value, $valueMap);
578 $matchingMap = array_flip((array)array_keys($valueMap, $_value));
581 $matchingRecords = array_intersect_key($this->_listOfRecords, $matchingMap);
584 return $matchingRecords;
588 * returns first record of this set
590 * @return Tinebase_Record_Abstract|NULL
592 public function getFirstRecord()
594 if (count($this->_listOfRecords) > 0) {
595 foreach ($this->_listOfRecords as $idx => $record) {
604 * compares two recordsets / only compares the ids / returns all records that are different in an array:
605 * - removed -> all records that are in $this but not in $_recordSet
606 * - added -> all records that are in $_recordSet but not in $this
607 * - modified -> array of diffs for all different records that are in both record sets
609 * @param Tinebase_Record_RecordSet $recordSet
610 * @return Tinebase_Record_RecordSetDiff
612 public function diff($recordSet)
614 if (! $recordSet instanceof Tinebase_Record_RecordSet) {
615 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
616 . ' Did not get Tinebase_Record_RecordSet, skipping diff(' . $this->_recordClass . ')');
617 return new Tinebase_Record_RecordSetDiff(array(
618 'model' => $this->getRecordClassName()
622 if ($this->getRecordClassName() !== $recordSet->getRecordClassName()) {
623 throw new Tinebase_Exception_InvalidArgument('can only compare recordsets with the same type of records');
626 $existingRecordsIds = $this->getArrayOfIds();
627 $toCompareWithRecordsIds = $recordSet->getArrayOfIds();
629 $removedIds = array_diff($existingRecordsIds, $toCompareWithRecordsIds);
630 $addedIds = array_diff($toCompareWithRecordsIds, $existingRecordsIds);
631 $modifiedIds = array_intersect($existingRecordsIds, $toCompareWithRecordsIds);
633 $removed = new Tinebase_Record_RecordSet($this->getRecordClassName());
634 $added = new Tinebase_Record_RecordSet($this->getRecordClassName());
635 $modified = new Tinebase_Record_RecordSet('Tinebase_Record_Diff');
637 foreach ($addedIds as $id) {
638 $added->addRecord($recordSet->getById($id));
640 // consider records without id, too
641 foreach ($recordSet->getIdLessIndexes() as $index) {
642 $added->addRecord($recordSet->getByIndex($index));
644 foreach ($removedIds as $id) {
645 $removed->addRecord($this->getById($id));
647 // consider records without id, too
648 foreach ($this->getIdLessIndexes() as $index) {
649 $removed->addRecord($this->getByIndex($index));
651 foreach ($modifiedIds as $id) {
652 $diff = $this->getById($id)->diff($recordSet->getById($id));
653 if (! $diff->isEmpty()) {
654 $modified->addRecord($diff);
658 $result = new Tinebase_Record_RecordSetDiff(array(
659 'model' => $this->getRecordClassName(),
661 'removed' => $removed,
662 'modified' => $modified,
669 * merges records from given record set
671 * @param Tinebase_Record_RecordSet $_recordSet
674 public function merge(Tinebase_Record_RecordSet $_recordSet)
676 foreach ($_recordSet as $record) {
677 if (! in_array($record, $this->_listOfRecords, true)) {
678 $this->addRecord($record);
686 * merges records from given record set if id not yet present in current record set
688 * @param Tinebase_Record_RecordSet $_recordSet
691 public function mergeById(Tinebase_Record_RecordSet $_recordSet)
693 foreach ($_recordSet as $record) {
694 if (false === $this->getIndexById($record->getId())) {
695 $this->addRecord($record);
703 * sorts this recordset
705 * @param string $_field
706 * @param string $_direction
707 * @param string $_sortFunction
708 * @param int $_flags sort flags for asort/arsort
711 public function sort($_field, $_direction = 'ASC', $_sortFunction = 'asort', $_flags = SORT_REGULAR)
713 if (! is_string($_field) && is_callable($_field)) {
714 $_sortFunction = 'function';
716 $offsetToSortFieldMap = $this->__get($_field);
719 switch ($_sortFunction) {
721 $fn = $_direction == 'ASC' ? 'asort' : 'arsort';
722 $fn($offsetToSortFieldMap, $_flags);
725 natcasesort($offsetToSortFieldMap);
726 if ($_direction == 'DESC') {
727 // @todo check if this is working
728 $offsetToSortFieldMap = array_reverse($offsetToSortFieldMap);
732 uasort ($this->_listOfRecords , $_field);
733 $offsetToSortFieldMap = $this->_listOfRecords;
736 throw new Tinebase_Exception_InvalidArgument('Sort function unknown.');
740 $oldListOfRecords = $this->_listOfRecords;
742 // reset indexes and records
743 $this->_idLess = array();
744 $this->_idMap = array();
745 $this->_listOfRecords = array();
747 foreach (array_keys($offsetToSortFieldMap) as $oldOffset) {
748 $this->addRecord($oldListOfRecords[$oldOffset]);
755 * sorts this recordset by pagination sort info
757 * @param Tinebase_Model_Pagination $_pagination
760 public function sortByPagination($_pagination)
762 if ($_pagination !== NULL && $_pagination->sort) {
763 $sortField = is_array($_pagination->sort) ? $_pagination->sort[0] : $_pagination->sort;
764 $this->sort($sortField, ($_pagination->dir) ? $_pagination->dir : 'ASC');
771 * limits this recordset by pagination
772 * sorting should always be applied before to get the desired sequence
773 * @param Tinebase_Model_Pagination $_pagination
776 public function limitByPagination($_pagination)
778 if ($_pagination !== NULL && $_pagination->limit) {
779 $indices = range($_pagination->start, $_pagination->start + $_pagination->limit - 1);
780 foreach($this as $index => &$record) {
781 if(! in_array($index, $indices)) {
782 $this->offsetUnset($index);
790 * translate all member records of this set
792 public function translate()
794 foreach ($this->_listOfRecords as $record) {
795 $record->translate();