Fix Application uninstall
[tine20] / tine20 / Tinebase / Relation / Backend / Sql.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
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>
10  * 
11  * @todo        remove db table usage and extend Tinebase_Backend_Sql_Abstract
12  */
13
14
15 /**
16  * class Tinebase_Relation_Backend_Sql
17  * 
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.
20  * 
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.
25  * 
26  * NOTE: Relations are viewed as time dependend properties of records. As such, relations could
27  * be broken, but never become deleted.
28  * 
29  * @package     Tinebase
30  * @subpackage  Relations
31  */
32 class Tinebase_Relation_Backend_Sql extends Tinebase_Backend_Sql_Abstract
33 {
34     /**
35      * @var Zend_Db_Adapter_Abstract
36      */
37     protected $_db;
38     
39     /**
40      * Holds instance for SQL_TABLE_PREFIX . 'record_relations' table
41      * 
42      * @var Tinebase_Db_Table
43      */
44     protected $_dbTable;
45     
46     /**
47      * constructor
48      */
49     public function __construct()
50     {
51         $this->_db = Tinebase_Core::getDb();
52         $this->_dbCommand = Tinebase_Backend_Sql_Command::factory($this->_db);
53         
54         // temporary on the fly creation of table
55         $this->_dbTable = new Tinebase_Db_Table(array(
56             'name' => SQL_TABLE_PREFIX . 'relations',
57             'primary' => 'id'
58         ));
59     }
60     
61     /**
62      * adds a new relation
63      * 
64      * @param  Tinebase_Model_Relation $_relation 
65      * @return Tinebase_Model_Relation the new relation
66      * 
67      * @todo    move check existance and update / modlog to controller?
68      */
69     public function addRelation($_relation)
70     {
71         if ($_relation->getId()) {
72             throw new Tinebase_Exception_Record_NotAllowed('Could not add existing relation');
73         }
74         
75         $relId = $_relation->generateUID();
76         $_relation->setId($relId);
77
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);
81             $where = array(
82                 $this->_db->quoteInto($this->_db->quoteIdentifier('rel_id') . ' = ?', $deletedRelId)
83             );
84             $this->_dbTable->delete($where);
85         } 
86                 
87         $data = $_relation->toArray();
88         $data['rel_id'] = $data['id'];
89         $data['id'] = $_relation->generateUID();
90         unset($data['related_record']);
91         
92         if (isset($data['remark']) && is_array($data['remark'])) {
93             $data['remark'] = Zend_Json::encode($data['remark']);
94         }
95         
96         $this->_dbTable->insert($data);
97         
98         $swappedData = $this->_swapRoles($data);
99         $swappedData['id'] = $_relation->generateUID();
100         $this->_dbTable->insert($swappedData);
101                 
102         return $this->getRelation($relId, $_relation['own_model'], $_relation['own_backend'], $_relation['own_id']);
103     }
104     
105     /**
106      * update an existing relation
107      * 
108      * @param  Tinebase_Model_Relation $_relation 
109      * @return Tinebase_Model_Relation the updated relation
110      */
111     public function updateRelation($_relation)
112     {
113         $id = $_relation->getId();
114         
115         $data = $_relation->toArray();
116         $data['rel_id'] = $data['id'];
117         unset($data['id']);
118         unset($data['related_record']);
119         
120         if (isset($data['remark']) && is_array($data['remark'])) {
121             $data['remark'] = Zend_Json::encode($data['remark']);
122         }
123         
124         foreach (array($data, $this->_swapRoles($data)) as $toUpdate) {
125             $where = array(
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']),
130             );
131             $this->_dbTable->update($toUpdate, $where);
132         }
133         
134         return $this->getRelation($id, $_relation['own_model'], $_relation['own_backend'], $_relation['own_id']);
135             
136     }
137     
138     /**
139      * breaks a relation
140      * 
141      * @param Tinebase_Model_Relation $_relation 
142      * @return void 
143      */
144     public function breakRelation($_id)
145     {
146         $where = array(
147             $this->_db->quoteIdentifier('rel_id') . ' = ' . $this->_db->quote($_id)
148         );
149         
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)
154         ), $where);
155     }
156     
157     /**
158      * breaks all relations, optionally only of given role
159      * 
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
165      * @return void
166      */
167     public function breakAllRelations($_model, $_backend, $_id, $_degree = NULL, array $_type = array())
168     {
169         $relationIds = $this->getAllRelations($_model, $_backend, $_id, $_degree, $_type)->getArrayOfIds();
170         if (!empty($relationIds)) {
171             $where = array(
172                 $this->_db->quoteInto($this->_db->quoteIdentifier('rel_id') . ' IN (?)', $relationIds)
173             );
174         
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)
179             ), $where);
180         }
181     }
182     
183     /**
184      * returns all relations of a given record and optionally only of given role
185      * 
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
194      */
195     public function getAllRelations($_model, $_backend, $_id, $_degree = NULL, array $_type = array(), $_returnAll = false, $_relatedModels = NULL)
196     {
197         $_id = $_id ? (array)$_id : array('');
198         $where = 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),
202         );
203         
204         if (is_array($_relatedModels) && ! empty($_relatedModels)) {
205             $where[] = $this->_db->quoteInto($this->_db->quoteIdentifier('related_model') .' IN (?)', $_relatedModels);
206         }
207         
208         if (!$_returnAll) {
209             $where[] = $this->_db->quoteIdentifier('is_deleted') . ' = '.(int)FALSE;
210         }
211         if ($_degree) {
212             $where[] = $this->_db->quoteInto($this->_db->quoteIdentifier('own_degree') . ' = ?', $_degree);
213         }
214         if (! empty($_type)) {
215             $where[] = $this->_db->quoteInto($this->_db->quoteIdentifier('type') . ' IN (?)', $_type);
216         }
217         
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));
221         }
222         return $relations;
223     }
224     
225     /**
226      * returns one side of a relation
227      *
228      * @param  string $_id
229      * @param  string $_ownModel 
230      * @param  string $_ownBackend
231      * @param  string $_ownId
232      * @param  bool   $_returnBroken
233      * @return Tinebase_Model_Relation
234      */
235     public function getRelation($_id, $_ownModel, $_ownBackend, $_ownId, $_returnBroken = false)
236     {
237         $where = array(
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),
242         );
243         if ($_returnBroken !== true) {
244             $where[] = $this->_db->quoteIdentifier('is_deleted') . ' = '. (int)FALSE;
245         }
246         $relationRow = $this->_dbTable->fetchRow($where);
247         
248         if($relationRow) {
249             return $this->_rawDataToRecord($relationRow->toArray());
250         } else {
251             throw new Tinebase_Exception_Record_NotDefined("No relation found.");
252         }
253     }
254     
255     /**
256      * converts raw data from adapter into a single record
257      *
258      * @param  array $_rawData
259      * @return Tinebase_Record_Abstract
260      */
261     protected function _rawDataToRecord(array $_rawData)
262     {
263         $_rawData['id'] = $_rawData['rel_id'];
264         $result = new Tinebase_Model_Relation($_rawData, true);
265         
266         return $result;
267     }
268     
269     /**
270      * purges(removes from table) all relations
271      * 
272      * @param  string $_ownModel 
273      * @param  string $_ownBackend
274      * @param  string $_ownId
275      * @return void
276      * 
277      * @todo should this function only purge deleted/broken relations?
278      */
279     public function purgeAllRelations($_ownModel, $_ownBackend, $_ownId)
280     {
281         $relationIds = $this->getAllRelations($_ownModel, $_ownBackend, $_ownId, NULL, array(), true)->getArrayOfIds();
282         
283         if (!empty($relationIds)) {
284             $where = array(
285                 $this->_db->quoteInto($this->_db->quoteIdentifier('rel_id') . ' IN (?)', $relationIds)
286             );
287         
288             $this->_dbTable->delete($where);
289         }
290     }
291     
292     /**
293      * Search for records matching given filter
294      *
295      * @param Tinebase_Model_Filter_FilterGroup $_filter
296      * @param Tinebase_Model_Pagination $_pagination
297      * @param boolean $_onlyIds
298      * @return Tinebase_Record_RecordSet|array
299      */
300     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL, $_onlyIds = FALSE)    
301     {
302         $backend = new Tinebase_Backend_Sql(array(
303             'modelName' => 'Tinebase_Model_Relation', 
304             'tableName' => 'relations',
305         ));
306         
307         $_filter->addFilter(new Tinebase_Model_Filter_Bool('is_deleted', 'equals', (int)FALSE));
308         
309         return $backend->search($_filter, $_pagination, $_onlyIds);
310     }
311     
312     /**
313      * swaps roles own/related
314      * 
315      * @param  array data of a relation
316      * @return array data with swaped roles
317      */
318     protected function _swapRoles($_data)
319     {
320         $data = $_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;
330                 break;
331             case Tinebase_Model_Relation::DEGREE_CHILD:
332                 $data['own_degree'] = Tinebase_Model_Relation::DEGREE_PARENT;
333                 break;
334         }
335         return $data;
336     }
337     
338     /**
339      * check if relation already exists but is_deleted
340      *
341      * @param Tinebase_Model_Relation $_relation
342      * @return string relation id
343      */
344     protected function _checkExistance($_relation)
345     {
346         $where = array(
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'
352         );
353         $relationRow = $this->_dbTable->fetchRow($where);
354         
355         if ($relationRow) {
356             return $relationRow->rel_id;
357         } else {
358             return FALSE;
359         }
360     }
361     
362     /**
363      * transfers relations
364      * 
365      * @param string $sourceId
366      * @param string $destinationId
367      * @param string $model
368      * 
369      * @return array
370      */
371     public function transferRelations($sourceId, $destinationId, $model)
372     {
373         $controller = Tinebase_Controller_Record_Abstract::getController($model);
374         
375         // just for validation, the records aren't needed
376         $controller->get($sourceId);
377         $controller->get($destinationId);
378         
379         $tableName = SQL_TABLE_PREFIX . 'relations';
380         
381         // own side
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();
387         
388         // rel side
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();
394         
395         $skipped = array();
396         
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'])
401             );
402             $select->from($tableName);
403             $stmt = $this->_db->query($select);
404             $existing = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
405             $stmt->closeCursor();
406             
407             if (count($existing) > 0) {
408                 $skipped[$entry['rel_id']] = $entry;
409             } else {
410                 $this->_dbTable->update(
411                     array('own_id' => $destinationId),
412                     $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $entry['id'])
413                 );
414             }
415         }
416         
417         
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'])
422             );
423             $select->from($tableName);
424             $stmt = $this->_db->query($select);
425             $existing = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
426             $stmt->closeCursor();
427             
428             if (count($existing) > 0) {
429             } else {
430                 $this->_dbTable->update(
431                     array('related_id' => $destinationId), 
432                     $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $entry['id'])
433                 );
434             }
435         }
436
437         return $skipped;
438     }
439
440     /**
441      * counts related records, gropued by Model, Type and Id but excludes relations which will be updated by $excludeCount
442      *
443      * @param string $ownModel
444      * @param Tinebase_Record_RecordSet $relations
445      * @return array
446      */
447     public function countRelatedConstraints($ownModel, $relations, $excludeCount)
448     {
449         if ($relations->count() == 0) {
450             return array();
451         }
452     
453         $adapter = $this->_dbTable->getAdapter();
454         $tableName = SQL_TABLE_PREFIX . 'relations';
455         
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 ';
465         
466         if (! empty($excludeCount)) {
467             $sql .= ' AND '. $this->_db->quoteInto($this->_db->quoteIdentifier('id'). ' NOT IN (?) ', $excludeCount);
468         }
469         
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');
471
472         $result = $adapter->fetchAssoc($sql);
473     
474         return $result;
475     }
476
477     /**
478      * remove all relations for application
479      *
480      * @param string $applicationName
481      *
482      * @return void
483      */
484     public function removeApplication($applicationName)
485     {
486         $tableName = SQL_TABLE_PREFIX . 'relations';
487
488         $select = $this->_db->select()->from($tableName)->columns('rel_id')
489             ->where($this->_db->quoteIdentifier('own_model') . ' LIKE ?', $applicationName . '_%');
490
491         $relation_ids = $this->_db->fetchCol($select);
492
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));
495         }
496     }
497 }