6 * @subpackage Relations
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>
11 * @todo remove db table usage and extend Tinebase_Backend_Sql_Abstract
16 * class Tinebase_Relation_Backend_Sql
18 * Tinebase_Relation_Backend_Sql enables records to define cross application relations to other records.
19 * It acts as a gneralised storage backend for the records relation property of these records.
21 * Relations between records have a certain degree (PARENT, CHILD and SIBLING). This degrees are defined
22 * in Tinebase_Model_Relation. Moreover Relations are of a type which is defined by the application defining
23 * the relation. In case of users manually created relations this type is 'MANUAL'. This manually created
24 * relatiions can also hold a free-form remark.
26 * NOTE: Relations are viewed as time dependend properties of records. As such, relations could
27 * be broken, but never become deleted.
30 * @subpackage Relations
32 class Tinebase_Relation_Backend_Sql extends Tinebase_Backend_Sql_Abstract
35 * @var Zend_Db_Adapter_Abstract
40 * Holds instance for SQL_TABLE_PREFIX . 'record_relations' table
42 * @var Tinebase_Db_Table
49 public function __construct()
51 $this->_db = Tinebase_Core::getDb();
52 $this->_dbCommand = Tinebase_Backend_Sql_Command::factory($this->_db);
54 // temporary on the fly creation of table
55 $this->_dbTable = new Tinebase_Db_Table(array(
56 'name' => SQL_TABLE_PREFIX . 'relations',
64 * @param Tinebase_Model_Relation $_relation
65 * @return Tinebase_Model_Relation the new relation
67 * @todo move check existance and update / modlog to controller?
69 public function addRelation($_relation)
71 if ($_relation->getId()) {
72 throw new Tinebase_Exception_Record_NotAllowed('Could not add existing relation');
75 $relId = $_relation->generateUID();
76 $_relation->setId($relId);
78 // check if relation is already set (with is_deleted=1)
79 if ($deletedRelId = $this->_checkExistance($_relation)) {
80 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Removing existing relation (rel_id): ' . $deletedRelId);
82 $this->_db->quoteInto($this->_db->quoteIdentifier('rel_id') . ' = ?', $deletedRelId)
84 $this->_dbTable->delete($where);
87 $data = $_relation->toArray();
88 $data['rel_id'] = $data['id'];
89 $data['id'] = $_relation->generateUID();
90 unset($data['related_record']);
92 if (isset($data['remark']) && is_array($data['remark'])) {
93 $data['remark'] = Zend_Json::encode($data['remark']);
96 $this->_dbTable->insert($data);
98 $swappedData = $this->_swapRoles($data);
99 $swappedData['id'] = $_relation->generateUID();
100 $this->_dbTable->insert($swappedData);
102 return $this->getRelation($relId, $_relation['own_model'], $_relation['own_backend'], $_relation['own_id']);
106 * update an existing relation
108 * @param Tinebase_Model_Relation $_relation
109 * @return Tinebase_Model_Relation the updated relation
111 public function updateRelation($_relation)
113 $id = $_relation->getId();
115 $data = $_relation->toArray();
116 $data['rel_id'] = $data['id'];
118 unset($data['related_record']);
120 if (isset($data['remark']) && is_array($data['remark'])) {
121 $data['remark'] = Zend_Json::encode($data['remark']);
124 foreach (array($data, $this->_swapRoles($data)) as $toUpdate) {
126 $this->_db->quoteIdentifier('rel_id') . ' = ' . $this->_db->quote($id),
127 $this->_db->quoteIdentifier('own_model') . ' = ' . $this->_db->quote($toUpdate['own_model']),
128 $this->_db->quoteIdentifier('own_backend') . ' = ' . $this->_db->quote($toUpdate['own_backend']),
129 $this->_db->quoteIdentifier('own_id') . ' = ' . $this->_db->quote($toUpdate['own_id']),
131 $this->_dbTable->update($toUpdate, $where);
134 return $this->getRelation($id, $_relation['own_model'], $_relation['own_backend'], $_relation['own_id']);
141 * @param Tinebase_Model_Relation $_relation
144 public function breakRelation($_id)
147 $this->_db->quoteIdentifier('rel_id') . ' = ' . $this->_db->quote($_id)
150 $this->_dbTable->update(array(
151 'is_deleted' => (int)true,
152 'deleted_by' => Tinebase_Core::getUser()->getId(),
153 'deleted_time' => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG)
158 * breaks all relations, optionally only of given role
160 * @param string $_model own model to break all relations for
161 * @param string $_backend own backend to break all relations for
162 * @param string $_id own id to break all relations for
163 * @param string $_degree only breaks relations of given degree
164 * @param array $_type only breaks relations of given type
167 public function breakAllRelations($_model, $_backend, $_id, $_degree = NULL, array $_type = array())
169 $relationIds = $this->getAllRelations($_model, $_backend, $_id, $_degree, $_type)->getArrayOfIds();
170 if (!empty($relationIds)) {
172 $this->_db->quoteInto($this->_db->quoteIdentifier('rel_id') . ' IN (?)', $relationIds)
175 $this->_dbTable->update(array(
176 'is_deleted' => (int)true,
177 'deleted_by' => Tinebase_Core::getUser()->getId(),
178 'deleted_time' => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG)
184 * returns all relations of a given record and optionally only of given role
186 * @param string $_model own model to get all relations for
187 * @param string $_backend own backend to get all relations for
188 * @param string|array $_id own id to get all relations for
189 * @param string $_degree only return relations of given degree
190 * @param array $_type only return relations of given type
191 * @param boolean $_returnAll gets all relations (default: only get not deleted/broken relations)
192 * @param array $_relatedModels only return relations having this related model
193 * @return Tinebase_Record_RecordSet of Tinebase_Model_Relation
195 public function getAllRelations($_model, $_backend, $_id, $_degree = NULL, array $_type = array(), $_returnAll = false, $_relatedModels = NULL)
197 $_id = $_id ? (array)$_id : array('');
199 $this->_db->quoteInto($this->_db->quoteIdentifier('own_model') .' = ?', $_model),
200 $this->_db->quoteInto($this->_db->quoteIdentifier('own_backend') .' = ?',$_backend),
201 $this->_db->quoteInto($this->_db->quoteIdentifier('own_id') .' IN (?)' , $_id),
204 if (is_array($_relatedModels) && ! empty($_relatedModels)) {
205 $where[] = $this->_db->quoteInto($this->_db->quoteIdentifier('related_model') .' IN (?)', $_relatedModels);
209 $where[] = $this->_db->quoteIdentifier('is_deleted') . ' = '.(int)FALSE;
212 $where[] = $this->_db->quoteInto($this->_db->quoteIdentifier('own_degree') . ' = ?', $_degree);
214 if (! empty($_type)) {
215 $where[] = $this->_db->quoteInto($this->_db->quoteIdentifier('type') . ' IN (?)', $_type);
218 $relations = new Tinebase_Record_RecordSet('Tinebase_Model_Relation', array(), true);
219 foreach ($this->_dbTable->fetchAll($where) as $relation) {
220 $relations->addRecord($this->_rawDataToRecord($relation->toArray(), true));
226 * returns one side of a relation
229 * @param string $_ownModel
230 * @param string $_ownBackend
231 * @param string $_ownId
232 * @param bool $_returnBroken
233 * @return Tinebase_Model_Relation
235 public function getRelation($_id, $_ownModel, $_ownBackend, $_ownId, $_returnBroken = false)
238 $this->_db->quoteInto($this->_db->quoteIdentifier('rel_id') . ' = ?', $_id),
239 $this->_db->quoteInto($this->_db->quoteIdentifier('own_model') . ' = ?', $_ownModel),
240 $this->_db->quoteInto($this->_db->quoteIdentifier('own_backend') . ' = ?', $_ownBackend),
241 $this->_db->quoteInto($this->_db->quoteIdentifier('own_id') . ' = ?', $_ownId),
243 if ($_returnBroken !== true) {
244 $where[] = $this->_db->quoteIdentifier('is_deleted') . ' = '. (int)FALSE;
246 $relationRow = $this->_dbTable->fetchRow($where);
249 return $this->_rawDataToRecord($relationRow->toArray());
251 throw new Tinebase_Exception_Record_NotDefined("No relation found.");
256 * converts raw data from adapter into a single record
258 * @param array $_rawData
259 * @return Tinebase_Record_Abstract
261 protected function _rawDataToRecord(array $_rawData)
263 $_rawData['id'] = $_rawData['rel_id'];
264 $result = new Tinebase_Model_Relation($_rawData, true);
270 * purges(removes from table) all relations
272 * @param string $_ownModel
273 * @param string $_ownBackend
274 * @param string $_ownId
277 * @todo should this function only purge deleted/broken relations?
279 public function purgeAllRelations($_ownModel, $_ownBackend, $_ownId)
281 $relationIds = $this->getAllRelations($_ownModel, $_ownBackend, $_ownId, NULL, array(), true)->getArrayOfIds();
283 if (!empty($relationIds)) {
285 $this->_db->quoteInto($this->_db->quoteIdentifier('rel_id') . ' IN (?)', $relationIds)
288 $this->_dbTable->delete($where);
293 * Search for records matching given filter
295 * @param Tinebase_Model_Filter_FilterGroup $_filter
296 * @param Tinebase_Model_Pagination $_pagination
297 * @param boolean $_onlyIds
298 * @return Tinebase_Record_RecordSet|array
300 public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL, $_onlyIds = FALSE)
302 $backend = new Tinebase_Backend_Sql(array(
303 'modelName' => 'Tinebase_Model_Relation',
304 'tableName' => 'relations',
307 $_filter->addFilter(new Tinebase_Model_Filter_Bool('is_deleted', 'equals', (int)FALSE));
309 return $backend->search($_filter, $_pagination, $_onlyIds);
313 * swaps roles own/related
315 * @param array data of a relation
316 * @return array data with swaped roles
318 protected function _swapRoles($_data)
321 $data['own_model'] = $_data['related_model'];
322 $data['own_backend'] = $_data['related_backend'];
323 $data['own_id'] = $_data['related_id'];
324 $data['related_model'] = $_data['own_model'];
325 $data['related_backend'] = $_data['own_backend'];
326 $data['related_id'] = $_data['own_id'];
327 switch ($_data['own_degree']) {
328 case Tinebase_Model_Relation::DEGREE_PARENT:
329 $data['own_degree'] = Tinebase_Model_Relation::DEGREE_CHILD;
331 case Tinebase_Model_Relation::DEGREE_CHILD:
332 $data['own_degree'] = Tinebase_Model_Relation::DEGREE_PARENT;
339 * check if relation already exists but is_deleted
341 * @param Tinebase_Model_Relation $_relation
342 * @return string relation id
344 protected function _checkExistance($_relation)
347 $this->_db->quoteInto($this->_db->quoteIdentifier('own_model') . ' = ?', $_relation->own_model),
348 $this->_db->quoteInto($this->_db->quoteIdentifier('own_backend') . ' = ?', $_relation->own_backend),
349 $this->_db->quoteInto($this->_db->quoteIdentifier('own_id') . ' = ?', $_relation->own_id),
350 $this->_db->quoteInto($this->_db->quoteIdentifier('related_id') . ' = ?', $_relation->related_id),
351 $this->_db->quoteIdentifier('is_deleted') . ' = 1'
353 $relationRow = $this->_dbTable->fetchRow($where);
356 return $relationRow->rel_id;
363 * transfers relations
365 * @param string $sourceId
366 * @param string $destinationId
367 * @param string $model
371 public function transferRelations($sourceId, $destinationId, $model)
373 $controller = Tinebase_Controller_Record_Abstract::getController($model);
375 // just for validation, the records aren't needed
376 $controller->get($sourceId);
377 $controller->get($destinationId);
379 $tableName = SQL_TABLE_PREFIX . 'relations';
382 $select = $this->_db->select()->where($this->_db->quoteIdentifier('own_id') . ' = ?', $sourceId);
383 $select->from($tableName);
384 $stmt = $this->_db->query($select);
385 $entries = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
386 $stmt->closeCursor();
389 $select = $this->_db->select()->where($this->_db->quoteIdentifier('related_id') . ' = ?', $sourceId);
390 $select->from($tableName);
391 $stmt = $this->_db->query($select);
392 $relentries = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
393 $stmt->closeCursor();
397 foreach($entries as $entry) {
398 $select = $this->_db->select()->where(
399 $this->_db->quoteInto($this->_db->quoteIdentifier('own_id') . ' = ?', $destinationId). ' AND ' .
400 $this->_db->quoteInto($this->_db->quoteIdentifier('related_id') . ' = ?', $entry['related_id'])
402 $select->from($tableName);
403 $stmt = $this->_db->query($select);
404 $existing = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
405 $stmt->closeCursor();
407 if (count($existing) > 0) {
408 $skipped[$entry['rel_id']] = $entry;
410 $this->_dbTable->update(
411 array('own_id' => $destinationId),
412 $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $entry['id'])
418 foreach($relentries as $entry) {
419 $select = $this->_db->select()->where(
420 $this->_db->quoteInto($this->_db->quoteIdentifier('related_id') . ' = ?', $destinationId). ' AND ' .
421 $this->_db->quoteInto($this->_db->quoteIdentifier('own_id') . ' = ?', $entry['own_id'])
423 $select->from($tableName);
424 $stmt = $this->_db->query($select);
425 $existing = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
426 $stmt->closeCursor();
428 if (count($existing) > 0) {
430 $this->_dbTable->update(
431 array('related_id' => $destinationId),
432 $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $entry['id'])
441 * counts related records, gropued by Model, Type and Id but excludes relations which will be updated by $excludeCount
443 * @param string $ownModel
444 * @param Tinebase_Record_RecordSet $relations
447 public function countRelatedConstraints($ownModel, $relations, $excludeCount)
449 if ($relations->count() == 0) {
453 $adapter = $this->_dbTable->getAdapter();
454 $tableName = SQL_TABLE_PREFIX . 'relations';
456 $sql = 'SELECT '. $this->_dbCommand->getConcat(array($this->_db->quoteIdentifier('related_model'), "'--'", $this->_db->quoteIdentifier('type'), "'--'", $this->_db->quoteIdentifier('own_id'))) . '
457 AS ' . $this->_db->quoteIdentifier('id') . ',
458 ' . $this->_db->quoteIdentifier('related_model') .', ' . $this->_db->quoteIdentifier('type') .',
459 ' . $this->_db->quoteIdentifier('own_model') .', COUNT(*)
460 AS ' . $this->_db->quoteIdentifier('count') . '
461 FROM ' . $this->_db->quoteIdentifier($tableName) . '
462 WHERE ' . $this->_db->quoteInto($this->_db->quoteIdentifier('own_id') . ' IN (?) ', $relations->related_id) . '
463 AND '. $this->_db->quoteInto($this->_db->quoteIdentifier('related_model'). ' = ? ', $ownModel) . '
464 AND '. $this->_db->quoteIdentifier('is_deleted'). ' = 0 ';
466 if (! empty($excludeCount)) {
467 $sql .= ' AND '. $this->_db->quoteInto($this->_db->quoteIdentifier('id'). ' NOT IN (?) ', $excludeCount);
470 $sql .= 'GROUP BY '. $this->_db->quoteIdentifier('own_id') .','.$this->_db->quoteIdentifier('related_model') . ', ' . $this->_db->quoteIdentifier('own_model') . ', ' . $this->_db->quoteIdentifier('type') . ', ' . $this->_db->quoteIdentifier('related_id');
472 $result = $adapter->fetchAssoc($sql);
478 * remove all relations for application
480 * @param string $applicationName
484 public function removeApplication($applicationName)
486 $tableName = $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . 'relations');
488 $select = $this->_db->select()->from($tableName)->columns('rel_id')
489 ->where($this->_db->quoteIdentifier('own_model') . ' LIKE ?', $applicationName . '_%');
491 $relation_ids = $this->_db->fetchCol($select);
493 if (is_array($relation_ids) && count($relation_ids) > 0) {
494 $this->_db->delete($tableName, $this->_db->quoteInto($this->_db->quoteIdentifier('rel_id') . ' IN (?)', $relation_ids));