PageRenderTime 57ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/tine20/Tinebase/Backend/Sql/Abstract.php

https://gitlab.com/rsilveira1987/Expresso
PHP | 1373 lines | 711 code | 229 blank | 433 comment | 141 complexity | 799013b372c539526d950953517336ee MD5 | raw file
  1. <?php
  2. /**
  3. * Tine 2.0
  4. *
  5. * @package Tinebase
  6. * @subpackage Backend
  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 Philipp Schüle <p.schuele@metaways.de>
  10. *
  11. * @todo think about removing the appendForeignRecord* functions
  12. * @todo use const for type (set in constructor)
  13. * @todo move custom fields handling to controller?
  14. */
  15. /**
  16. * Abstract class for a Tine 2.0 sql backend
  17. *
  18. * @package Tinebase
  19. * @subpackage Backend
  20. */
  21. abstract class Tinebase_Backend_Sql_Abstract extends Tinebase_Backend_Abstract implements Tinebase_Backend_Sql_Interface
  22. {
  23. /**
  24. * placeholder for id column for search()/_getSelect()
  25. */
  26. const IDCOL = '_id_';
  27. /**
  28. * fetch single column with db query
  29. */
  30. const FETCH_MODE_SINGLE = 'fetch_single';
  31. /**
  32. * fetch two columns (id + X) with db query
  33. */
  34. const FETCH_MODE_PAIR = 'fetch_pair';
  35. /**
  36. * fetch all columns with db query
  37. */
  38. const FETCH_ALL = 'fetch_all';
  39. /**
  40. * backend type
  41. *
  42. * @var string
  43. */
  44. protected $_type = 'Sql';
  45. /**
  46. * Table name without prefix
  47. *
  48. * @var string
  49. */
  50. protected $_tableName = NULL;
  51. /**
  52. * Table prefix
  53. *
  54. * @var string
  55. */
  56. protected $_tablePrefix = NULL;
  57. /**
  58. * if modlog is active, we add 'is_deleted = 0' to select object in _getSelect()
  59. *
  60. * @var boolean
  61. */
  62. protected $_modlogActive = FALSE;
  63. /**
  64. * use subselect in searchCount fn
  65. *
  66. * @var boolean
  67. */
  68. protected $_useSubselectForCount = TRUE;
  69. /**
  70. * Identifier
  71. *
  72. * @var string
  73. */
  74. protected $_identifier = 'id';
  75. /**
  76. * @var Zend_Db_Adapter_Abstract
  77. */
  78. protected $_db;
  79. /**
  80. * @var Tinebase_Backend_Sql_Command_Interface
  81. */
  82. protected $_dbCommand;
  83. /**
  84. * schema of the table
  85. *
  86. * @var array
  87. */
  88. protected $_schema = NULL;
  89. /**
  90. * foreign tables
  91. * name => array(table, joinOn, field)
  92. *
  93. * @var array
  94. */
  95. protected $_foreignTables = array();
  96. /**
  97. * Additional columns _getSelect()
  98. */
  99. protected $_additionalColumns = array();
  100. /**
  101. * additional search count columns
  102. *
  103. * @var array
  104. */
  105. protected $_additionalSearchCountCols = array();
  106. /**
  107. * default secondary sort criteria
  108. *
  109. * @var string
  110. */
  111. protected $_defaultSecondarySort = NULL;
  112. /**
  113. * default column(s) for count
  114. *
  115. * @var string
  116. */
  117. protected $_defaultCountCol = '*';
  118. /**
  119. * the constructor
  120. *
  121. * allowed options:
  122. * - modelName
  123. * - tableName
  124. * - tablePrefix
  125. * - modlogActive
  126. * - useSubselectForCount
  127. *
  128. * @param Zend_Db_Adapter_Abstract $_db (optional)
  129. * @param array $_options (optional)
  130. * @throws Tinebase_Exception_Backend_Database
  131. */
  132. public function __construct($_dbAdapter = NULL, $_options = array())
  133. {
  134. $this->_db = ($_dbAdapter instanceof Zend_Db_Adapter_Abstract) ? $_dbAdapter : Tinebase_Core::getDb();
  135. $this->_dbCommand = Tinebase_Backend_Sql_Command::factory($this->_db);
  136. $this->_modelName = array_key_exists('modelName', $_options) ? $_options['modelName'] : $this->_modelName;
  137. $this->_tableName = array_key_exists('tableName', $_options) ? $_options['tableName'] : $this->_tableName;
  138. $this->_tablePrefix = array_key_exists('tablePrefix', $_options) ? $_options['tablePrefix'] : $this->_db->table_prefix;
  139. $this->_modlogActive = array_key_exists('modlogActive', $_options) ? $_options['modlogActive'] : $this->_modlogActive;
  140. $this->_useSubselectForCount = array_key_exists('useSubselectForCount', $_options) ? $_options['useSubselectForCount'] : $this->_useSubselectForCount;
  141. if (! ($this->_tableName && $this->_modelName)) {
  142. throw new Tinebase_Exception_Backend_Database('modelName and tableName must be configured or given.');
  143. }
  144. if (! $this->_db) {
  145. throw new Tinebase_Exception_Backend_Database('Database adapter must be configured or given.');
  146. }
  147. foreach ($this->_additionalColumns as $name => $query) {
  148. $this->_additionalColumns[$name] = str_replace("{prefix}", $this->_tablePrefix, $query);
  149. }
  150. try {
  151. $this->_schema = Tinebase_Db_Table::getTableDescriptionFromCache($this->_tablePrefix . $this->_tableName, $this->_db);
  152. } catch (Zend_Db_Adapter_Exception $zdae) {
  153. throw new Tinebase_Exception_Backend_Database('Connection failed: ' . $zdae->getMessage());
  154. }
  155. }
  156. /*************************** getters and setters *********************************/
  157. /**
  158. * sets modlog active flag
  159. *
  160. * @param $_bool
  161. * @return Tinebase_Backend_Sql_Abstract
  162. */
  163. public function setModlogActive($_bool)
  164. {
  165. $this->_modlogActive = (bool) $_bool;
  166. return $this;
  167. }
  168. /**
  169. * checks if modlog is active or not
  170. *
  171. * @return bool
  172. */
  173. public function getModlogActive()
  174. {
  175. return $this->_modlogActive;
  176. }
  177. /**
  178. * returns the db schema
  179. *
  180. * @return array
  181. */
  182. public function getSchema()
  183. {
  184. if (!$this->_schema){
  185. try {
  186. $this->_schema = Tinebase_Db_Table::getTableDescriptionFromCache($this->_tablePrefix . $this->_tableName, $this->_db);
  187. } catch (Zend_Db_Adapter_Exception $zdae) {
  188. Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . 'Connection failed: ' . $zdae->getMessage());
  189. throw new Tinebase_Exception_Backend_Database('Connection failed: ' . $zdae->getMessage());
  190. }
  191. }
  192. return $this->_schema;
  193. }
  194. /*************************** get/search funcs ************************************/
  195. /**
  196. * Gets one entry (by id)
  197. *
  198. * @param integer|Tinebase_Record_Interface $_id
  199. * @param $_getDeleted get deleted records
  200. * @return Tinebase_Record_Interface
  201. * @throws Tinebase_Exception_NotFound
  202. */
  203. public function get($_id, $_getDeleted = FALSE)
  204. {
  205. if (empty($_id)) {
  206. throw new Tinebase_Exception_NotFound('$_id can not be empty');
  207. }
  208. $id = Tinebase_Record_Abstract::convertId($_id, $this->_modelName);
  209. return $this->getByProperty($id, $this->_identifier, $_getDeleted);
  210. }
  211. /**
  212. * splits identifier if table name is given (i.e. for joined tables)
  213. *
  214. * @return string identifier name
  215. */
  216. protected function _getRecordIdentifier()
  217. {
  218. if (preg_match("/\./", $this->_identifier)) {
  219. list($table, $identifier) = explode('.', $this->_identifier);
  220. } else {
  221. $identifier = $this->_identifier;
  222. }
  223. return $identifier;
  224. }
  225. /**
  226. * Gets one entry (by property)
  227. *
  228. * @param mixed $value
  229. * @param string $property
  230. * @param bool $getDeleted
  231. * @return Tinebase_Record_Interface
  232. * @throws Tinebase_Exception_NotFound
  233. */
  234. public function getByProperty($value, $property = 'name', $getDeleted = FALSE)
  235. {
  236. $this->getAdapter(); // ensures that there is a connection
  237. $select = $this->_getSelect('*', $getDeleted)
  238. ->limit(1);
  239. if ($value !== NULL) {
  240. $select->where($this->_db->quoteIdentifier($this->_tableName . '.' . $property) . ' = ?', $value);
  241. } else {
  242. $select->where($this->_db->quoteIdentifier($this->_tableName . '.' . $property) . ' IS NULL');
  243. }
  244. Tinebase_Backend_Sql_Abstract::traitGroup($select);
  245. $stmt = $this->getAdapter()->query($select);
  246. $queryResult = $stmt->fetch();
  247. $stmt->closeCursor();
  248. if (!$queryResult) {
  249. $messageValue = ($value !== NULL) ? $value : 'NULL';
  250. throw new Tinebase_Exception_NotFound($this->_modelName . " record with $property = $messageValue not found!");
  251. }
  252. $result = $this->_rawDataToRecord($queryResult);
  253. return $result;
  254. }
  255. /**
  256. * fetch a single property for all records defined in array of $ids
  257. *
  258. * @param array|string $ids
  259. * @param string $property
  260. * @return array (key = id, value = property value)
  261. */
  262. public function getPropertyByIds($ids, $property)
  263. {
  264. $this->getAdapter(); // ensures that there is a connection
  265. $select = $this->_getSelect(array($property, $this->_identifier));
  266. $select->where($this->_db->quoteIdentifier($this->_tableName . '.' . $this->_identifier) . ' IN (?)', (array) $ids);
  267. Tinebase_Backend_Sql_Abstract::traitGroup($select);
  268. $this->_checkTracing($select);
  269. $stmt = $this->_db->query($select);
  270. $queryResult = $stmt->fetchAll();
  271. $stmt->closeCursor();
  272. $result = array();
  273. foreach($queryResult as $row) {
  274. $result[$row[$this->_identifier]] = $row[$property];
  275. }
  276. return $result;
  277. }
  278. /**
  279. * converts raw data from adapter into a single record
  280. *
  281. * @param array $_rawData
  282. * @return Tinebase_Record_Abstract
  283. */
  284. protected function _rawDataToRecord(array $_rawData)
  285. {
  286. $result = new $this->_modelName($_rawData, true);
  287. $this->_explodeForeignValues($result);
  288. return $result;
  289. }
  290. /**
  291. * explode foreign values
  292. *
  293. * @param Tinebase_Record_Interface $_record
  294. */
  295. protected function _explodeForeignValues(Tinebase_Record_Interface $_record)
  296. {
  297. foreach (array_keys($this->_foreignTables) as $field) {
  298. $isSingleValue = (array_key_exists('singleValue', $this->_foreignTables[$field]) && $this->_foreignTables[$field]['singleValue']);
  299. if (! $isSingleValue) {
  300. $_record->{$field} = (! empty($_record->{$field})) ? explode(',', $_record->{$field}) : array();
  301. }
  302. }
  303. }
  304. /**
  305. * gets multiple entries (by property)
  306. *
  307. * @param mixed $_value
  308. * @param string $_property
  309. * @param bool $_getDeleted
  310. * @param string $_orderBy defaults to $_property
  311. * @param string $_orderDirection defaults to 'ASC'
  312. * @return Tinebase_Record_RecordSet
  313. */
  314. public function getMultipleByProperty($_value, $_property='name', $_getDeleted = FALSE, $_orderBy = NULL, $_orderDirection = 'ASC')
  315. {
  316. $this->getAdapter(); // ensures that there is a connection
  317. $columnName = $this->_db->quoteIdentifier($this->_tableName . '.' . $_property);
  318. $rawDatas = array();
  319. if (! empty($_value)) {
  320. $value = (array)$_value;
  321. $orderBy = $this->_tableName . '.' . ($_orderBy ? $_orderBy : $_property);
  322. $select = $this->_getSelect('*', $_getDeleted)
  323. ->where($columnName . 'IN (?)', $value)
  324. ->order($orderBy . ' ' . $_orderDirection);
  325. Tinebase_Backend_Sql_Abstract::traitGroup($select);
  326. $this->_checkTracing($select);
  327. $stmt = $this->_db->query($select);
  328. $rawDatas = $stmt->fetchAll();
  329. }
  330. $resultSet = $this->_rawDataToRecordSet($rawDatas);
  331. $resultSet->addIndices(array($_property));
  332. return $resultSet;
  333. }
  334. /**
  335. * converts raw data from adapter into a set of records
  336. *
  337. * @param array $_rawDatas of arrays
  338. * @return Tinebase_Record_RecordSet
  339. */
  340. protected function _rawDataToRecordSet(array $_rawDatas)
  341. {
  342. $result = new Tinebase_Record_RecordSet($this->_modelName, $_rawDatas, true);
  343. if (! empty($this->_foreignTables)) {
  344. foreach ($result as $record) {
  345. $this->_explodeForeignValues($record);
  346. }
  347. }
  348. return $result;
  349. }
  350. /**
  351. * Get multiple entries
  352. *
  353. * @param string|array $_id Ids
  354. * @param array $_containerIds all allowed container ids that are added to getMultiple query
  355. * @return Tinebase_Record_RecordSet
  356. *
  357. * @todo get custom fields here as well
  358. */
  359. public function getMultiple($_id, $_containerIds = NULL)
  360. {
  361. $this->getAdapter(); // ensures that there is a connection
  362. // filter out any emtpy values
  363. $ids = array_filter((array) $_id, function($value) {
  364. return !empty($value);
  365. });
  366. if (empty($ids)) {
  367. return new Tinebase_Record_RecordSet($this->_modelName);
  368. }
  369. // replace objects with their id's
  370. foreach ($ids as &$id) {
  371. if ($id instanceof Tinebase_Record_Interface) {
  372. $id = $id->getId();
  373. }
  374. }
  375. $select = $this->_getSelect();
  376. $select->where($this->_db->quoteIdentifier($this->_tableName . '.' . $this->_identifier) . ' IN (?)', $ids);
  377. if ($_containerIds !== NULL && isset($this->_schema['container_id'])) {
  378. if (empty($_containerIds)) {
  379. $select->where('1=0 /* insufficient grants */');
  380. return $this->_rawDataToRecordSet(array());
  381. } else {
  382. $select->where($this->_db->quoteIdentifier($this->_tableName . '.container_id') . ' IN (?) /* add acl in getMultiple */', (array) $_containerIds);
  383. }
  384. }
  385. Tinebase_Backend_Sql_Abstract::traitGroup($select);
  386. $this->_checkTracing($select);
  387. $stmt = $this->_db->query($select);
  388. $queryResult = $stmt->fetchAll();
  389. $result = $this->_rawDataToRecordSet($queryResult);
  390. return $result;
  391. }
  392. /**
  393. * Gets all entries
  394. *
  395. * @param string $_orderBy Order result by
  396. * @param string $_orderDirection Order direction - allowed are ASC and DESC
  397. * @throws Tinebase_Exception_InvalidArgument
  398. * @return Tinebase_Record_RecordSet
  399. */
  400. public function getAll($_orderBy = NULL, $_orderDirection = 'ASC')
  401. {
  402. $this->getAdapter(); // ensures that there is a connection
  403. $orderBy = $_orderBy ? $_orderBy : $this->_tableName . '.' . $this->_identifier;
  404. if(!in_array($_orderDirection, array('ASC', 'DESC'))) {
  405. throw new Tinebase_Exception_InvalidArgument('$_orderDirection is invalid');
  406. }
  407. $select = $this->_getSelect();
  408. $select->order($orderBy . ' ' . $_orderDirection);
  409. Tinebase_Backend_Sql_Abstract::traitGroup($select);
  410. //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $select->__toString());
  411. $stmt = $this->_db->query($select);
  412. $queryResult = $stmt->fetchAll();
  413. //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($queryResult, true));
  414. $result = $this->_rawDataToRecordSet($queryResult);
  415. return $result;
  416. }
  417. /**
  418. * Search for records matching given filter
  419. *
  420. * @param Tinebase_Model_Filter_FilterGroup $_filter
  421. * @param Tinebase_Model_Pagination $_pagination
  422. * @param array|string|boolean $_cols columns to get, * per default / use self::IDCOL or TRUE to get only ids
  423. * @return Tinebase_Record_RecordSet|array
  424. */
  425. public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL, $_cols = '*')
  426. {
  427. if ($_pagination === NULL) {
  428. $_pagination = new Tinebase_Model_Pagination(NULL, TRUE);
  429. }
  430. // legacy: $_cols param was $_onlyIds (boolean) ...
  431. if ($_cols === TRUE) {
  432. $_cols = self::IDCOL;
  433. } else if ($_cols === FALSE) {
  434. $_cols = '*';
  435. }
  436. // (1) eventually get only ids or id/value pair
  437. list($colsToFetch, $getIdValuePair) = $this->_getColumnsToFetch($_cols, $_filter, $_pagination);
  438. // check if we should do one or two queries
  439. $doSecondQuery = true;
  440. if (!$getIdValuePair && $_cols !== self::IDCOL)
  441. {
  442. if ($this->_compareRequiredJoins($_cols, $colsToFetch)) {
  443. $doSecondQuery = false;
  444. }
  445. }
  446. if ($doSecondQuery) {
  447. $select = $this->_getSelect($colsToFetch);
  448. } else {
  449. $select = $this->_getSelect($_cols);
  450. }
  451. if ($_filter !== NULL) {
  452. $this->_addFilter($select, $_filter);
  453. }
  454. $this->_addSecondarySort($_pagination);
  455. $_pagination->appendPaginationSql($select);
  456. Tinebase_Backend_Sql_Abstract::traitGroup($select);
  457. if ($getIdValuePair) {
  458. return $this->_fetch($select, self::FETCH_MODE_PAIR);
  459. } elseif($_cols === self::IDCOL) {
  460. return $this->_fetch($select);
  461. }
  462. if (!$doSecondQuery) {
  463. $rows = $this->_fetch($select, self::FETCH_ALL);
  464. if (empty($rows)) {
  465. return new Tinebase_Record_RecordSet($this->_modelName);
  466. } else {
  467. return $this->_rawDataToRecordSet($rows);
  468. }
  469. }
  470. // (2) get other columns and do joins
  471. $ids = $this->_fetch($select);
  472. if (empty($ids)) {
  473. return new Tinebase_Record_RecordSet($this->_modelName);
  474. }
  475. $select = $this->_getSelect($_cols);
  476. $this->_addWhereIdIn($select, $ids);
  477. $_pagination->appendSort($select);
  478. $rows = $this->_fetch($select, self::FETCH_ALL);
  479. return $this->_rawDataToRecordSet($rows);
  480. }
  481. /**
  482. * add the fields to search for to the query
  483. *
  484. * @param Zend_Db_Select $_select current where filter
  485. * @param Tinebase_Model_Filter_FilterGroup $_filter the string to search for
  486. * @return void
  487. */
  488. protected function _addFilter(Zend_Db_Select $_select, /*Tinebase_Model_Filter_FilterGroup */$_filter)
  489. {
  490. Tinebase_Backend_Sql_Filter_FilterGroup::appendFilters($_select, $_filter, $this);
  491. }
  492. /**
  493. * Gets total count of search with $_filter
  494. *
  495. * @param Tinebase_Model_Filter_FilterGroup $_filter
  496. * @return int
  497. */
  498. public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter)
  499. {
  500. $defaultCountCol = $this->_defaultCountCol == '*' ? '*' : $this->_db->quoteIdentifier($this->_defaultCountCol);
  501. $searchCountCols = array_merge(array('count' => 'COUNT(' . $defaultCountCol . ')'), $this->_additionalSearchCountCols);
  502. if ($this->_useSubselectForCount) {
  503. list($colsToFetch, $getIdValuePair) = $this->_getColumnsToFetch(self::IDCOL, $_filter);
  504. $colsToFetch = !empty($this->_additionalSearchCountCols)
  505. ? array_merge($colsToFetch, array_keys($this->_additionalSearchCountCols))
  506. : $colsToFetch;
  507. $select = $this->_getSelect($colsToFetch);
  508. $this->_addFilter($select, $_filter);
  509. Tinebase_Backend_Sql_Abstract::traitGroup($select);
  510. $countSelect = $this->_db->select()->from($select, $searchCountCols);
  511. } else {
  512. $countSelect = $this->_getSelect($searchCountCols);
  513. $this->_addFilter($countSelect, $_filter);
  514. Tinebase_Backend_Sql_Abstract::traitGroup($countSelect);
  515. }
  516. if (!empty($this->_additionalSearchCountCols)) {
  517. $result = $this->_db->fetchRow($countSelect);
  518. } else {
  519. $result = $this->_db->fetchOne($countSelect);
  520. }
  521. return $result;
  522. }
  523. /**
  524. * returns columns to fetch in first query and if an id/value pair is requested
  525. *
  526. * @param array|string $_cols
  527. * @param Tinebase_Model_Filter_FilterGroup $_filter
  528. * @param Tinebase_Model_Pagination $_pagination
  529. * @return array
  530. */
  531. protected function _getColumnsToFetch($_cols, Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL)
  532. {
  533. $getIdValuePair = FALSE;
  534. if ($_cols === '*') {
  535. $colsToFetch = array('id' => self::IDCOL);
  536. } else {
  537. $colsToFetch = (array) $_cols;
  538. if (in_array(self::IDCOL, $colsToFetch) && count($colsToFetch) == 2) {
  539. // id/value pair requested
  540. $getIdValuePair = TRUE;
  541. } else if (! in_array(self::IDCOL, $colsToFetch) && count($colsToFetch) == 1) {
  542. // only one non-id column was requested -> add id and treat it like id/value pair
  543. array_push($colsToFetch, self::IDCOL);
  544. $getIdValuePair = TRUE;
  545. } else {
  546. $colsToFetch = array('id' => self::IDCOL);
  547. }
  548. }
  549. if ($_filter !== NULL) {
  550. $colsToFetch = $this->_addFilterColumns($colsToFetch, $_filter);
  551. }
  552. if ($_pagination instanceof Tinebase_Model_Pagination) {
  553. foreach((array) $_pagination->sort as $sort) {
  554. if (! (isset($colsToFetch[$sort]) || array_key_exists($sort, $colsToFetch))) {
  555. $colsToFetch[$sort] = (substr_count($sort, $this->_tableName) === 0) ? $this->_tableName . '.' . $sort : $sort;
  556. }
  557. }
  558. }
  559. return array($colsToFetch, $getIdValuePair);
  560. }
  561. /**
  562. * add columns from filter
  563. *
  564. * @param array $_colsToFetch
  565. * @param Tinebase_Model_Filter_FilterGroup $_filter
  566. * @return array
  567. */
  568. protected function _addFilterColumns($_colsToFetch, Tinebase_Model_Filter_FilterGroup $_filter)
  569. {
  570. // need to ask filter if it needs additional columns
  571. $filterCols = $_filter->getRequiredColumnsForSelect();
  572. foreach ($filterCols as $key => $filterCol) {
  573. if (! array_key_exists($key, $_colsToFetch)) {
  574. $_colsToFetch[$key] = $filterCol;
  575. }
  576. }
  577. return $_colsToFetch;
  578. }
  579. /**
  580. * add default secondary sort criteria
  581. *
  582. * @param Tinebase_Model_Pagination $_pagination
  583. */
  584. protected function _addSecondarySort(Tinebase_Model_Pagination $_pagination)
  585. {
  586. if (! empty($this->_defaultSecondarySort)) {
  587. if (! is_array($_pagination->sort) || ! in_array($this->_defaultSecondarySort, $_pagination->sort)) {
  588. $_pagination->sort = array_merge((array)$_pagination->sort, array($this->_defaultSecondarySort));
  589. }
  590. }
  591. }
  592. /**
  593. * adds 'id in (...)' where stmt
  594. *
  595. * @param Zend_Db_Select $_select
  596. * @param string|array $_ids
  597. * @return Zend_Db_Select
  598. */
  599. protected function _addWhereIdIn(Zend_Db_Select $_select, $_ids)
  600. {
  601. $_select->where($this->_db->quoteInto($this->_db->quoteIdentifier($this->_tableName . '.' . $this->_identifier) . ' in (?)', (array) $_ids));
  602. return $_select;
  603. }
  604. /**
  605. * Checks if backtrace and query should be logged
  606. *
  607. * For enabling this feature, you must add a key in config.inc.php:
  608. *
  609. * 'logger' =>
  610. * array(
  611. * // logger stuff
  612. * 'traceQueryOrigins' => true,
  613. * 'priority' => 8
  614. * ),
  615. *
  616. * @param Zend_Db_Select $select
  617. */
  618. protected function _checkTracing(Zend_Db_Select $select)
  619. {
  620. if (Tinebase_Core::isLogLevel(Zend_Log::TRACE) && $config = Tinebase_Core::getConfig()->logger) {
  621. if ($config->traceQueryOrigins) {
  622. $e = new Exception();
  623. Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . "\n" .
  624. "BACKTRACE: \n" . $e->getTraceAsString() . "\n" .
  625. "SQL QUERY: \n" . $select);
  626. }
  627. }
  628. }
  629. /**
  630. * fetch rows from db
  631. *
  632. * @param Zend_Db_Select $_select
  633. * @param string $_mode
  634. * @return array
  635. */
  636. protected function _fetch(Zend_Db_Select $_select, $_mode = self::FETCH_MODE_SINGLE)
  637. {
  638. $this->getAdapter(); // ensures that there is a connection
  639. if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $_select->__toString());
  640. Tinebase_Backend_Sql_Abstract::traitGroup($_select);
  641. $this->_checkTracing($_select);
  642. $stmt = $this->_db->query($_select);
  643. if ($_mode === self::FETCH_ALL) {
  644. $result = (array) $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
  645. } else {
  646. $result = array();
  647. while ($row = $stmt->fetch(Zend_Db::FETCH_NUM)) {
  648. if ($_mode === self::FETCH_MODE_SINGLE) {
  649. $result[] = $row[0];
  650. } else if ($_mode === self::FETCH_MODE_PAIR) {
  651. $result[$row[0]] = $row[1];
  652. }
  653. }
  654. }
  655. return $result;
  656. }
  657. /**
  658. * get the basic select object to fetch records from the database
  659. *
  660. * @param array|string $_cols columns to get, * per default
  661. * @param boolean $_getDeleted get deleted records (if modlog is active)
  662. * @return Zend_Db_Select
  663. */
  664. protected function _getSelect($_cols = '*', $_getDeleted = FALSE)
  665. {
  666. $this->getAdapter(); // ensures that there is a connection
  667. if ($_cols !== '*' ) {
  668. $cols = array();
  669. // make sure cols is an array, prepend tablename and fix keys
  670. foreach ((array) $_cols as $id => $col) {
  671. $key = (is_numeric($id)) ? ($col === self::IDCOL) ? $this->_identifier : $col : $id;
  672. $cols[$key] = ($col === self::IDCOL) ? $this->_tableName . '.' . $this->_identifier : $col;
  673. }
  674. } else {
  675. $cols = array('*');
  676. }
  677. foreach ($this->_additionalColumns as $name => $column) {
  678. $cols[$name] = $column;
  679. }
  680. if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($cols, TRUE));
  681. $select = $this->_db->select();
  682. $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName), $cols);
  683. if (!$_getDeleted && $this->_modlogActive) {
  684. // don't fetch deleted objects
  685. $select->where($this->_db->quoteIdentifier($this->_tableName . '.is_deleted') . ' = 0');
  686. }
  687. $this->_addForeignTableJoins($select, $cols);
  688. return $select;
  689. }
  690. /**
  691. * add foreign table joins
  692. *
  693. * @param Zend_Db_Select $_select
  694. * @param array|string $_cols columns to get, * per default
  695. *
  696. * @todo find a way to preserve columns if needed without the need for the preserve setting
  697. * @todo get joins from Zend_Db_Select before trying to join the same tables twice (+ remove try/catch)
  698. */
  699. protected function _addForeignTableJoins(Zend_Db_Select $_select, $_cols, $_groupBy = NULL)
  700. {
  701. $this->getAdapter(); // ensures that there is a connection
  702. if (! empty($this->_foreignTables)) {
  703. $groupBy = ($_groupBy !== NULL) ? $_groupBy : $this->_tableName . '.' . $this->_identifier;
  704. $_select->group($groupBy);
  705. $cols = (array) $_cols;
  706. foreach ($this->_foreignTables as $foreignColumn => $join) {
  707. // only join if field is in cols
  708. if (in_array('*', $cols) || array_key_exists($foreignColumn, $cols)) {
  709. if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' foreign column: ' . $foreignColumn);
  710. $selectArray = (array_key_exists('select', $join))
  711. ? $join['select']
  712. : ((array_key_exists('field', $join) && (! array_key_exists('singleValue', $join) || ! $join['singleValue']))
  713. ? array($foreignColumn => $this->_dbCommand->getAggregate($join['table'] . '.' . $join['field']))
  714. : array($foreignColumn => $join['table'] . '.id'));
  715. $joinId = (array_key_exists('joinId', $join)) ? $join['joinId'] : $this->_identifier;
  716. $this->_removeColFromSelect($_select, $cols, $foreignColumn);
  717. try {
  718. $_select->joinLeft(
  719. /* table */ array($join['table'] => $this->_tablePrefix . $join['table']),
  720. /* on */ $this->_db->quoteIdentifier($this->_tableName . '.' . $joinId) . ' = ' . $this->_db->quoteIdentifier($join['table'] . '.' . $join['joinOn']),
  721. /* select */ $selectArray
  722. );
  723. // need to add it to cols to prevent _removeColFromSelect from removing it
  724. if (array_key_exists('preserve', $join) && $join['preserve'] && array_key_exists($foreignColumn, $selectArray)) {
  725. $cols[$foreignColumn] = $selectArray[$foreignColumn];
  726. }
  727. } catch (Zend_Db_Select_Exception $zdse) {
  728. $_select->columns($selectArray, $join['table']);
  729. }
  730. }
  731. }
  732. }
  733. }
  734. /**
  735. * returns true if joins are equal, false if not
  736. *
  737. * @param array $finalCols
  738. * @param array $interimCols
  739. */
  740. protected function _compareRequiredJoins( $finalCols, $interimCols )
  741. {
  742. $ret = true;
  743. if (! empty($this->_foreignTables)) {
  744. $finalCols = (array) $finalCols;
  745. $finalColsJoins = array();
  746. $interimColsJoins = array();
  747. foreach ($this->_foreignTables as $foreignColumn => $join) {
  748. // only join if field is in cols
  749. if (in_array('*', $finalCols) || (isset($finalCols[$foreignColumn]) || array_key_exists($foreignColumn, $finalCols))) {
  750. $finalColsJoins[$join['table']] = 1;
  751. }
  752. if (in_array('*', $interimCols) || (isset($interimCols[$foreignColumn]) || array_key_exists($foreignColumn, $interimCols))) {
  753. $interimColsJoins[$join['table']] = 1;
  754. }
  755. }
  756. if (count(array_diff_key($finalColsJoins,$interimColsJoins)) > 0) {
  757. $ret = false;
  758. }
  759. }
  760. return $ret;
  761. }
  762. /**
  763. * remove column from select to avoid duplicates
  764. *
  765. * @param Zend_Db_Select $_select
  766. * @param array|string $_cols
  767. * @param string $_column
  768. */
  769. protected function _removeColFromSelect(Zend_Db_Select $_select, &$_cols, $_column)
  770. {
  771. if (! is_array($_cols)) {
  772. return;
  773. }
  774. foreach ($_cols as $name => $correlation) {
  775. if ($name == $_column) {
  776. if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Removing ' . $_column . ' from columns.');
  777. unset($_cols[$_column]);
  778. $_select->reset(Zend_Db_Select::COLUMNS);
  779. $_select->columns($_cols);
  780. }
  781. }
  782. }
  783. /*************************** create / update / delete ****************************/
  784. /**
  785. * Creates new entry
  786. *
  787. * @param Tinebase_Record_Interface $_record
  788. * @return Tinebase_Record_Interface
  789. * @throws Tinebase_Exception_InvalidArgument
  790. * @throws Tinebase_Exception_UnexpectedValue
  791. *
  792. * @todo remove autoincremental ids later
  793. */
  794. public function create(Tinebase_Record_Interface $_record)
  795. {
  796. $this->getAdapter(); // ensures that there is a connection
  797. $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($this->_db);
  798. try {
  799. $identifier = $_record->getIdProperty();
  800. if (!$_record instanceof $this->_modelName) {
  801. throw new Tinebase_Exception_InvalidArgument('invalid model type: $_record is instance of "' . get_class($_record) . '". but should be instance of ' . $this->_modelName);
  802. }
  803. // set uid if record has hash id and id is empty
  804. if ($this->_hasHashId() && empty($_record->$identifier)) {
  805. $newId = $_record->generateUID();
  806. $_record->setId($newId);
  807. }
  808. $recordArray = $this->_recordToRawData($_record);
  809. // unset id if autoincrement & still empty
  810. if (empty($_record->$identifier) || $_record->$identifier == 'NULL' ) {
  811. unset($recordArray['id']);
  812. }
  813. $recordArray = array_intersect_key($recordArray, $this->_schema);
  814. $this->_prepareData($recordArray);
  815. $this->_db->insert($this->_tablePrefix . $this->_tableName, $recordArray);
  816. if (!$this->_hasHashId()) {
  817. $newId = $this->_db->lastInsertId($this->getTablePrefix() . $this->getTableName(), $identifier);
  818. if(!$newId && isset($_record[$identifier])){
  819. $newId = $_record[$identifier];
  820. }
  821. }
  822. // if we insert a record without an id, we need to get back one
  823. if (empty($_record->$identifier) && $newId == 0) {
  824. throw new Tinebase_Exception_UnexpectedValue("Returned record id is 0.");
  825. }
  826. // if the record had no id set, set the id now
  827. if ($_record->$identifier == NULL || $_record->$identifier == 'NULL') {
  828. $_record->$identifier = $newId;
  829. }
  830. // add custom fields
  831. if ($_record->has('customfields') && !empty($_record->customfields)) {
  832. Tinebase_CustomField::getInstance()->saveRecordCustomFields($_record);
  833. }
  834. $this->_updateForeignKeys('create', $_record);
  835. $result = $this->get($_record->$identifier);
  836. $this->_inspectAfterCreate($result, $_record);
  837. Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
  838. } catch (Exception $e) {
  839. Tinebase_TransactionManager::getInstance()->rollBack();
  840. throw $e;
  841. }
  842. return $result;
  843. }
  844. /**
  845. * returns true if id is a hash value and false if integer
  846. *
  847. * @return boolean
  848. * @todo remove that when all tables use hash ids
  849. */
  850. protected function _hasHashId()
  851. {
  852. $identifier = $this->_getRecordIdentifier();
  853. $result = (in_array($this->_schema[$identifier]['DATA_TYPE'], array('varchar', 'VARCHAR2')) && $this->_schema[$identifier]['LENGTH'] == 40);
  854. return $result;
  855. }
  856. /**
  857. * converts record into raw data for adapter
  858. *
  859. * @param Tinebase_Record_Abstract $_record
  860. * @return array
  861. */
  862. protected function _recordToRawData($_record)
  863. {
  864. $readOnlyFields = $_record->getReadOnlyFields();
  865. $raw = $_record->toArray(FALSE);
  866. foreach ($raw as $key => $value) {
  867. if ($value instanceof Tinebase_Record_Interface) {
  868. $raw[$key] = $value->getId();
  869. }
  870. if (in_array($key, $readOnlyFields)) {
  871. unset($raw[$key]);
  872. }
  873. }
  874. return $raw;
  875. }
  876. /**
  877. * prepare record data array
  878. * - replace int and bool values by Zend_Db_Expr
  879. *
  880. * @param array &$_recordArray
  881. * @return array with the prepared data
  882. */
  883. protected function _prepareData(&$_recordArray)
  884. {
  885. foreach ($_recordArray as $key => $value) {
  886. if (is_bool($value)) {
  887. $_recordArray[$key] = ($value) ? new Zend_Db_Expr('1') : new Zend_Db_Expr('0');
  888. } elseif (is_null($value)) {
  889. $_recordArray[$key] = new Zend_Db_Expr('NULL');
  890. } elseif (is_int($value)) {
  891. $_recordArray[$key] = new Zend_Db_Expr((string) $value);
  892. }
  893. }
  894. }
  895. /**
  896. * update foreign key values
  897. *
  898. * @param string $_mode create|update
  899. * @param Tinebase_Record_Abstract $_record
  900. */
  901. protected function _updateForeignKeys($_mode, Tinebase_Record_Abstract $_record)
  902. {
  903. $this->getAdapter(); // ensures that there is a connection
  904. if (! empty($this->_foreignTables)) {
  905. foreach ($this->_foreignTables as $modelName => $join) {
  906. if (! array_key_exists('field', $join)) {
  907. continue;
  908. }
  909. $idsToAdd = array();
  910. $idsToRemove = array();
  911. if (!empty($_record->$modelName)) {
  912. $idsToAdd = $this->_getIdsFromMixed($_record->$modelName);
  913. }
  914. $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
  915. if ($_mode == 'update') {
  916. $select = $this->_db->select();
  917. $select->from(array($join['table'] => $this->_tablePrefix . $join['table']), array($join['field']))
  918. ->where($this->_db->quoteIdentifier($join['table'] . '.' . $join['joinOn']) . ' = ?', $_record->getId());
  919. Tinebase_Backend_Sql_Abstract::traitGroup($select);
  920. $stmt = $this->_db->query($select);
  921. $currentIds = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
  922. $stmt->closeCursor();
  923. $idsToRemove = array_diff($currentIds, $idsToAdd);
  924. $idsToAdd = array_diff($idsToAdd, $currentIds);
  925. }
  926. if (!empty($idsToRemove)) {
  927. $where = '(' .
  928. $this->_db->quoteInto($this->_db->quoteIdentifier($this->_tablePrefix . $join['table'] . '.' . $join['joinOn']) . ' = ?', $_record->getId()) .
  929. ' AND ' .
  930. $this->_db->quoteInto($this->_db->quoteIdentifier($this->_tablePrefix . $join['table'] . '.' . $join['field']) . ' IN (?)', $idsToRemove) .
  931. ')';
  932. $this->_db->delete($this->_tablePrefix . $join['table'], $where);
  933. }
  934. foreach ($idsToAdd as $id) {
  935. $recordArray = array (
  936. $join['joinOn'] => $_record->getId(),
  937. $join['field'] => $id
  938. );
  939. $this->getAdapter()->insert($this->_tablePrefix . $join['table'], $recordArray);
  940. }
  941. Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
  942. }
  943. }
  944. }
  945. /**
  946. * convert recordset, array of ids or records to array of ids
  947. *
  948. * @param mixed $_mixed
  949. * @return array
  950. */
  951. protected function _getIdsFromMixed($_mixed)
  952. {
  953. if ($_mixed instanceof Tinebase_Record_RecordSet) { // Record set
  954. $ids = $_mixed->getArrayOfIds();
  955. } elseif (is_array($_mixed)) { // array
  956. foreach ($_mixed as $mixed) {
  957. if ($mixed instanceof Tinebase_Record_Abstract) {
  958. $ids[] = $mixed->getId();
  959. } else {
  960. $ids[] = $mixed;
  961. }
  962. }
  963. } else { // string
  964. $ids[] = $_mixed instanceof Tinebase_Record_Abstract ? $_mixed->getId() : $_mixed;
  965. }
  966. return $ids;
  967. }
  968. /**
  969. * do something after creation of record
  970. *
  971. * @param Tinebase_Record_Abstract $_newRecord
  972. * @param Tinebase_Record_Abstract $_recordToCreate
  973. * @return void
  974. */
  975. protected function _inspectAfterCreate(Tinebase_Record_Abstract $_newRecord, Tinebase_Record_Abstract $_recordToCreate)
  976. {
  977. }
  978. /**
  979. * Updates existing entry
  980. *
  981. * @param Tinebase_Record_Interface $_record
  982. * @throws Tinebase_Exception_Record_Validation|Tinebase_Exception_InvalidArgument
  983. * @return Tinebase_Record_Interface Record|NULL
  984. */
  985. public function update(Tinebase_Record_Interface $_record)
  986. {
  987. $this->getAdapter(); // ensures that there is a connection
  988. $identifier = $_record->getIdProperty();
  989. if (!$_record instanceof $this->_modelName) {
  990. throw new Tinebase_Exception_InvalidArgument('invalid model type: $_record is instance of "' . get_class($_record) . '". but should be instance of ' . $this->_modelName);
  991. }
  992. $_record->isValid(TRUE);
  993. $id = $_record->getId();
  994. $recordArray = $this->_recordToRawData($_record);
  995. $recordArray = array_intersect_key($recordArray, $this->_schema);
  996. $this->_prepareData($recordArray);
  997. $where = array(
  998. $this->_db->quoteInto($this->_db->quoteIdentifier($identifier) . ' = ?', $id),
  999. );
  1000. $this->_db->update($this->_tablePrefix . $this->_tableName, $recordArray, $where);
  1001. // update custom fields
  1002. if ($_record->has('customfields')) {
  1003. Tinebase_CustomField::getInstance()->saveRecordCustomFields($_record);
  1004. }
  1005. $this->_updateForeignKeys('update', $_record);
  1006. $result = $this->get($id, true);
  1007. return $result;
  1008. }
  1009. /**
  1010. * Updates multiple entries
  1011. *
  1012. * @param array $_ids to update
  1013. * @param array $_data
  1014. * @return integer number of affected rows
  1015. * @throws Tinebase_Exception_Record_Validation|Tinebase_Exception_InvalidArgument
  1016. */
  1017. public function updateMultiple($_ids, $_data)
  1018. {
  1019. $this->getAdapter(); // ensures that there is a connection
  1020. if (empty($_ids)) {
  1021. if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' No records updated.');
  1022. return 0;
  1023. }
  1024. // separate CustomFields
  1025. $myFields = array();
  1026. $customFields = array();
  1027. foreach($_data as $key => $value) {
  1028. if(stristr($key, '#')) $customFields[substr($key,1)] = $value;
  1029. else $myFields[$key] = $value;
  1030. }
  1031. // handle CustomFields
  1032. if(count($customFields)) {
  1033. if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' CustomFields found.');
  1034. Tinebase_CustomField::getInstance()->saveMultipleCustomFields($this->_modelName, $_ids, $customFields);
  1035. }
  1036. // handle StdFields
  1037. if(!count($myFields)) { return 0; }
  1038. $identifier = $this->_getRecordIdentifier();
  1039. $recordArray = $myFields;
  1040. $recordArray = array_intersect_key($recordArray, $this->_schema);
  1041. $this->_prepareData($recordArray);
  1042. $where = array(
  1043. $this->_db->quoteInto($this->_db->quoteIdentifier($identifier) . ' IN (?)', $_ids),
  1044. );
  1045. //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($where, TRUE));
  1046. return $this->_db->update($this->_tablePrefix . $this->_tableName, $recordArray, $where);
  1047. }
  1048. /**
  1049. * Deletes entries
  1050. *
  1051. * @param string|integer|Tinebase_Record_Interface|array $_id
  1052. * @return void
  1053. * @return int The number of affected rows.
  1054. */
  1055. public function delete($_id)
  1056. {
  1057. $this->getAdapter(); // ensures that there is a connection
  1058. if (empty($_id)) {
  1059. if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' No records deleted.');
  1060. return 0;
  1061. }
  1062. $idArray = (! is_array($_id)) ? array(Tinebase_Record_Abstract::convertId($_id, $this->_modelName)) : $_id;
  1063. $identifier = $this->_getRecordIdentifier();
  1064. $where = array(
  1065. $this->_db->quoteInto($this->_db->quoteIdentifier($identifier) . ' IN (?)', $idArray)
  1066. );
  1067. return $this->_db->delete($this->_tablePrefix . $this->_tableName, $where);
  1068. }
  1069. /**
  1070. * delete rows by property
  1071. *
  1072. * @param string|array $_value
  1073. * @param string $_property
  1074. * @param string $_operator (equals|in)
  1075. * @return integer The number of affected rows.
  1076. * @throws Tinebase_Exception_InvalidArgument
  1077. */
  1078. public function deleteByProperty($_value, $_property, $_operator = 'equals')
  1079. {
  1080. $this->getAdapter(); // ensures that there is a connection
  1081. if (! array_key_exists($_property, $this->_schema)) {
  1082. throw new Tinebase_Exception_InvalidArgument('Property ' . $_property . ' does not exist in table ' . $this->_tableName);
  1083. }
  1084. switch ($_operator) {
  1085. case 'equals':
  1086. $op = ' = ?';
  1087. break;
  1088. case 'in':
  1089. $op = ' IN (?)';
  1090. $_value = (array) $_value;
  1091. break;
  1092. default:
  1093. throw new Tinebase_Exception_InvalidArgument('Invalid operator: ' . $_operator);
  1094. }
  1095. $where = array(
  1096. $this->_db->quoteInto($this->getAdapter()->quoteIdentifier($_property) . $op, $_value)
  1097. );
  1098. return $this->_db->delete($this->_tablePrefix . $this->_tableName, $where);
  1099. }
  1100. /*************************** foreign record fetchers *******************************/
  1101. /**
  1102. * appends foreign record (1:1 relation) to given record
  1103. *
  1104. * @param Tinebase_Record_Abstract $_record Record to append the foreign record to
  1105. * @param string $_appendTo Property in the record where to append the foreign record to
  1106. * @param string $_recordKey Property in the record where the foreign key value is in
  1107. * @param string $_foreignKey Key property in foreign table of the record to append
  1108. * @param Tinebase_Backend_Sql_Abstract $_foreignBackend Foreign table backend
  1109. */
  1110. public function appendForeignRecordToRecord($_record, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
  1111. {
  1112. try {
  1113. $_record->$_appendTo = $_foreignBackend->getByProperty($_record->$_recordKey, $_foreignKey);
  1114. } catch (Tinebase_Exception_NotFound $e) {
  1115. $_record->$_appendTo = NULL;
  1116. }
  1117. }
  1118. /**
  1119. * appends foreign recordSet (1:n relation) to given record
  1120. *
  1121. * @param Tinebase_Record_Abstract $_record Record to append the foreign records to
  1122. * @param string $_appendTo Property in the record where to append the foreign records to
  1123. * @param string $_recordKey Property in the record where the foreign key value is in
  1124. * @param string $_foreignKey Key property in foreign table of the records to append
  1125. * @param Tinebase_Backend_Sql_Abstract $_foreignBackend Foreign table backend
  1126. */
  1127. public function appendForeignRecordSetToRecord($_record, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
  1128. {
  1129. $_record->$_appendTo = $_foreignBackend->getMultipleByProperty($_record->$_recordKey, $_foreignKey);
  1130. }
  1131. /**
  1132. * appends foreign record (1:1/n:1 relation) to given recordSet
  1133. *
  1134. * @param Tinebase_Record_RecordSet $_recordSet Records to append the foreign record to
  1135. * @param string $_appendTo Property in the records where to append the foreign record to
  1136. * @param string $_recordKey Property in the records where the foreign key value is in
  1137. * @param string $_foreignKey Key property in foreign table of the record to append
  1138. * @param Tinebase_Backend_Sql_Abstract $_foreignBackend Foreign table backend
  1139. */
  1140. public function appendForeignRecordToRecordSet($_recordSet, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
  1141. {
  1142. $allForeignRecords = $_foreignBackend->getMultipleByProperty($_recordSet->$_recordKey, $_foreignKey);
  1143. foreach ($_recordSet as $record) {
  1144. $record->$_appendTo = $allForeignRecords->filter($_foreignKey, $record->$_recordKey)->g