d82c4e2414ad32a27634adb35396584d5cd5ae2a
[tine20] / tine20 / Setup / Backend / Abstract.php
1 <?php
2 /**
3  * Tine 2.0 - http://www.tine20.org
4  * 
5  * @package     Setup
6  * @subpackage  Backend
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Matthias Greiling <m.greiling@metaways.de>
9  * @copyright   Copyright (c) 2007-2012 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * interface for backend class
14  * 
15  * @package     Setup
16  * @subpackage  Backend
17  */
18 abstract class Setup_Backend_Abstract implements Setup_Backend_Interface
19 {
20     /**
21      * Maximum length of table-, index-, contraint- and field names.
22      * 
23      * @var integer
24      */
25     const MAX_NAME_LENGTH = 30;
26     
27     /**
28      * default length of integer fields
29      * 
30      * @var integer
31      */
32     const INTEGER_DEFAULT_LENGTH = 11;
33
34     /**
35      * Define how database agnostic data types get mapped to database sepcific data types
36      * 
37      * @var array
38      */
39     protected $_typeMappings = array();
40  
41     /**
42      * @var Zend_Db_Adapter_Abstract
43      */
44     protected $_db = NULL;
45     
46     /**
47      * config object
48      *
49      * @var Zend_Config
50      */
51     protected $_config = NULL;
52     
53     /**
54      * Return the mapping from the given database-agnostic data {@param $_type} to the
55      * corresponding database specific data type
56      * 
57      * @param String $_type
58      * @return array | null
59      */
60     public function getTypeMapping($_type)
61     {
62         if ((isset($this->_typeMappings[$_type]) || array_key_exists($_type, $this->_typeMappings))) {
63             return $this->_typeMappings[$_type];
64         }
65         return null;
66     }
67     
68     /**
69      * constructor
70      *
71      */
72     public function __construct()
73     {
74         $this->_config = Tinebase_Core::getConfig();
75         $this->_db = Tinebase_Core::getDb();
76     }
77     
78     /**
79      * get db adapter
80      * 
81      * @return Zend_Db_Adapter_Abstract
82      */
83     public function getDb()
84     {
85         return $this->_db;
86     }
87     
88     /**
89      * checks if application is installed at all
90      *
91      * @param unknown_type $_application
92      * @return boolean
93      */
94     public function applicationExists($_application)
95     {
96         if ($this->tableExists('applications')) {
97             if ($this->applicationVersionQuery($_application) != false) {
98                 return true;
99             }
100         }
101         
102         return false;
103     }
104     
105     /**
106      * check's a given database table version 
107      *
108      * @param string $_tableName
109      * @return boolean|string "version" if the table exists, otherwise false
110      */
111     public function tableVersionQuery($_tableName)
112     {
113         $select = $this->_db->select()
114             ->from(SQL_TABLE_PREFIX . 'application_tables')
115             ->where($this->_db->quoteIdentifier('name') . ' = ?', SQL_TABLE_PREFIX . $_tableName)
116             ->orwhere($this->_db->quoteIdentifier('name') . ' = ?', $_tableName);
117
118         $stmt = $select->query();
119         $version = $stmt->fetchAll();
120         
121         return (! empty($version)) ? $version[0]['version'] : FALSE;
122     }
123     
124     /**
125      * truncate table in database
126      * 
127      * @param string tableName
128      */
129     public function truncateTable($_tableName)
130     {
131         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Truncate table ' . $_tableName);
132         $statement = "TRUNCATE TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName);
133         $this->execQueryVoid($statement);
134     }
135     
136     /**
137      * check's a given application version
138      *
139      * @param string $_application
140      * @return boolean return string "version" if the table exists, otherwise false
141      */
142     public function applicationVersionQuery($_application)
143     {
144         $select = $this->_db->select()
145             ->from( SQL_TABLE_PREFIX . 'applications')
146             ->where($this->_db->quoteIdentifier('name') . ' = ?', $_application);
147
148         $stmt = $select->query();
149         $version = $stmt->fetchAll();
150         
151         if (empty($version)) {
152             return false;
153         } else {
154             return $version[0]['version'];
155         }
156     }
157     
158     /**
159      * execute insert statement for default values (records)
160      * handles some special fields, which can't contain static values
161      * 
162      * @param   SimpleXMLElement $_record
163      * @throws  Setup_Exception
164      */
165     public function execInsertStatement(SimpleXMLElement $_record)
166     {
167         $data = array();
168         
169         foreach ($_record->field as $field) {
170             if (isset($field->value['special'])) {
171                 switch(strtolower($field->value['special'])) {
172                     case 'now':
173                         $value = Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG);
174                         break;
175                     
176                     case 'account_id':
177                         break;
178                     
179                     case 'application_id':
180                         $application = Tinebase_Application::getInstance()->getApplicationByName((string) $field->value);
181                         $value = $application->id;
182                         break;
183                     
184                     case 'uid':
185                         $value = Tinebase_Record_Abstract::generateUID();
186                         break;
187                         
188                     default:
189                         throw new Setup_Exception('Unsupported special type ' . strtolower($field->value['special']));
190                     }
191             } else {
192                 $value = $field->value;
193             }
194             // buffer for insert statement
195             $data[(string)$field->name] = (string)$value;
196         }
197         
198         #$table = new Tinebase_Db_Table(array(
199         #   'name' => SQL_TABLE_PREFIX . $_record->table->name
200         #));
201
202         #// final insert process
203         #$table->insert($data);
204         
205         #var_dump($data);
206         #var_dump(SQL_TABLE_PREFIX . $_record->table->name);
207         $this->_db->insert(SQL_TABLE_PREFIX . $_record->table->name, $data);
208     }
209
210     /**
211      * execute statement without return values
212      * 
213      * @param string statement
214      */    
215     public function execQueryVoid($_statement, $bind = array())
216     {
217         $stmt = $this->_db->query($_statement, $bind);
218     }
219     
220     /**
221      * execute statement  return values
222      * 
223      * @param string statement
224      * @return stdClass object
225      */
226     public function execQuery($_statement, $bind = array())
227     {
228         $stmt = $this->_db->query($_statement, $bind);
229         
230         return $stmt->fetchAll();
231     }
232     
233     /**
234      * checks if a given table exists
235      *
236      * @param string $_tableSchema
237      * @param string $_tableName
238      * @return boolean return true if the table exists, otherwise false
239      */
240     public function tableExists($_tableName)
241     {
242         $tableName = SQL_TABLE_PREFIX . $_tableName;
243         try {
244             $tableInfo = $this->_db->describeTable($tableName);
245         } catch (Zend_Db_Statement_Exception $e) {
246             $tableInfo = null;
247         }
248         return !empty($tableInfo);
249     }
250     
251     /**
252      * takes the xml stream and creates a table
253      *
254      * @param object $_table xml stream
255      * @param string $_appName if appname and tablename are given, we create an entry in the application table
256      * @param string $_tableName
257      */
258     public function createTable(Setup_Backend_Schema_Table_Abstract $_table, $_appName = NULL, $_tableName = NULL)
259     {
260         $statement = $this->getCreateStatement($_table);
261         $this->execQueryVoid($statement);
262         
263         if ($_appName !== NULL && $_tableName !== NULL) {
264             Tinebase_Application::getInstance()->addApplicationTable(
265                 Tinebase_Application::getInstance()->getApplicationByName($_appName), 
266                 $_tableName, 
267                 1
268             );
269         }
270     }
271     
272     /**
273      * removes table from database (and from application table if app id is given
274      * 
275      * @param string $_tableName
276      * @param string $_applicationId
277      */
278     public function dropTable($_tableName, $_applicationId = NULL)
279     {
280         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Dropping table ' . $_tableName);
281         $statement = "DROP TABLE IF EXISTS " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName);
282         $this->execQueryVoid($statement);
283         
284         if ($_applicationId !== NULL) {
285             Tinebase_Application::getInstance()->removeApplicationTable($_applicationId, $_tableName);
286         }
287     }
288     
289     /**
290      * renames table in database
291      * 
292      * @param string tableName
293      */
294     public function renameTable($_tableName, $_newName)
295     {
296         $statement = 'ALTER TABLE ' . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . ' RENAME TO ' . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_newName);
297         $this->execQueryVoid($statement);
298     }
299     
300     /**
301      * checks if a given column {@param $_columnName} exists in table {@param $_tableName}.
302      *
303      * @param string $_columnName
304      * @param string $_tableName
305      * @return boolean
306      */
307     public function columnExists($_columnName, $_tableName)
308     {
309         $columns = Tinebase_Db_Table::getTableDescriptionFromCache(SQL_TABLE_PREFIX . $_tableName, $this->_db); 
310         return (isset($columns[$_columnName]) || array_key_exists($_columnName, $columns));
311     }
312     
313     /**
314      * drop column/field in database table
315      * 
316      * @param string tableName
317      * @param string column/field name 
318      */    
319     public function dropCol($_tableName, $_colName)
320     {
321         $statement = 'ALTER TABLE ' . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . ' DROP COLUMN ' . $this->_db->quoteIdentifier($_colName);
322         $this->execQueryVoid($statement);
323     }
324     
325     /**
326      * add a primary key to database table
327      * 
328      * Delegates to {@see addPrimaryKey()}
329      * 
330      * @param string tableName 
331      * @param Setup_Backend_Schema_Index_Abstract declaration
332      */
333     public function addPrimaryKey($_tableName, Setup_Backend_Schema_Index_Abstract $_declaration)
334     {
335         $this->addIndex($_tableName, $_declaration);
336     }
337     
338     /**
339      * removes a primary key from database table
340      * 
341      * @param string tableName (there is just one primary key...)
342      */
343     public function dropPrimaryKey($_tableName)
344     {
345         $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . " DROP PRIMARY KEY " ;
346         $this->execQueryVoid($statement);
347     }
348
349     /**
350      * add a foreign key to database table
351      * 
352      * @param string tableName
353      * @param Setup_Backend_Schema_Index_Abstract declaration
354      */
355     public function addForeignKey($_tableName, Setup_Backend_Schema_Index_Abstract $_declaration)
356     {
357         $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . " ADD " 
358                     . $this->getForeignKeyDeclarations($_declaration, $_tableName);
359         $this->execQueryVoid($statement);
360     }
361     
362     /**
363      * removes a foreign key from database table
364      * 
365      * @param string tableName
366      * @param string foreign key name
367      */
368     public function dropForeignKey($_tableName, $_name)
369     {
370         try {
371             $this->_dropForeignKey($_tableName, SQL_TABLE_PREFIX . $_name);
372         } catch (Zend_Db_Statement_Exception $zdse) {
373             // try it again without table prefix
374             try {
375                 $this->_dropForeignKey($_tableName, $_name);
376             } catch (Zend_Db_Statement_Exception $zdse) {
377                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
378                     . ' ' . $zdse);
379                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
380                     . ' At first remove constraint, then remove key ...');
381                 
382                 $constraint = str_replace(array(
383                     '::',
384                     '--'
385                 ), '??', $_name);
386                 try {
387                     $this->_dropForeignKey($_tableName, SQL_TABLE_PREFIX . $constraint);
388                     $this->_dropForeignKey($_tableName, SQL_TABLE_PREFIX . $_name, FALSE);
389                 } catch (Zend_Db_Statement_Exception $zdse) {
390                     // do it again without prefix
391                     $this->_dropForeignKey($_tableName, $constraint);
392                     $this->_dropForeignKey($_tableName, $_name, FALSE);
393                 }
394             }
395         }
396     }
397     
398     /**
399      * helper function for removing (foreign) keys
400      * 
401      * @param string tableName
402      * @param string $keyName
403      * @param boolean $foreign
404      */
405     protected function _dropForeignKey($tableName, $keyName, $foreign = TRUE)
406     {
407         $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $tableName) 
408             . " DROP" . ($foreign ? ' FOREIGN' : '') . " KEY `" . $keyName . "`" ;
409         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
410             . ' ' . $statement);
411         $this->execQueryVoid($statement);
412     }
413     
414     /**
415      * removes a key from database table
416      * 
417      * @param string tableName 
418      * @param string key name
419      */
420     public function dropIndex($_tableName, $_indexName)
421     {
422         $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . " DROP INDEX " . $this->_db->quoteIdentifier($_indexName);
423         try {
424             $this->execQueryVoid($statement);
425         } catch (Zend_Db_Statement_Exception $zdse) {
426             if (Setup_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
427                 . ' ' . $zdse);
428             
429             // try it again with table prefix
430             $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . " DROP INDEX " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_indexName);
431             $this->execQueryVoid($statement);
432         }
433     }
434
435     /**
436      * create the right mysql-statement-snippet for columns/fields
437      *
438      * @param Setup_Backend_Schema_Field_Abstract field / column
439      * @param String | optional $_tableName [Not used in this backend (MySQL)]
440      * @return string
441      */
442     public function getFieldDeclarations(Setup_Backend_Schema_Field_Abstract $_field, $_tableName = '')
443     {
444         $buffer = $this->_getFieldDeclarations($_field, $_tableName);
445
446         $definition = implode(' ', $buffer);
447
448         return $definition;
449     }
450     
451     /**
452      * Concrete implementation of 
453      * @see tine20/Setup/Backend/Setup_Backend_Interface#checkTable($_table)
454      */
455     public function checkTable(Setup_Backend_Schema_Table_Abstract $_table)
456     {
457         $dbTable = $this->getExistingSchema($_table->name);
458         return $dbTable->equals($_table);
459     }
460     
461     /**
462      * create the right mysql-statement-snippet for columns/fields
463      *
464      * @param Setup_Backend_Schema_Field_Abstract field / column
465      * @param String | optional $_tableName [Not used in this backend (MySQL)]
466      * @return string
467      */
468     protected function _getFieldDeclarations(Setup_Backend_Schema_Field_Abstract $_field, $_tableName = '')
469     {
470         $buffer = array();
471         $buffer[] = '  ' . $this->_db->quoteIdentifier($_field->name);
472
473         $buffer = $this->_addDeclarationFieldType($buffer, $_field, $_tableName);
474         $buffer = $this->_addDeclarationUnsigned($buffer, $_field);
475         $buffer = $this->_addDeclarationDefaultValue($buffer, $_field);
476         $buffer = $this->_addDeclarationNotNull($buffer, $_field);
477         $buffer = $this->_addDeclarationAutoincrement($buffer, $_field);
478         $buffer = $this->_addDeclarationComment($buffer, $_field);
479         
480         return $buffer;
481     }
482     
483     protected function _addDeclarationFieldType(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field, $_tableName = '')
484     {
485         $typeMapping = $this->getTypeMapping($_field->type);
486         if (!$typeMapping) {
487             throw new Setup_Backend_Exception_InvalidSchema("Could not get field declaration for field {$_field->name}: The given field type {$_field->type} is not supported");
488         }
489         
490         $fieldType = $typeMapping['defaultType'];
491         if (isset($typeMapping['declarationMethod'])) {
492             $fieldBuffer = call_user_func(array($this, $typeMapping['declarationMethod']), $_field, $_tableName);
493             $_buffer = array_merge($_buffer, $fieldBuffer);
494         } else {
495             if ($_field->length !== NULL) {
496                 if ($this->_db instanceof Zend_Db_Adapter_Oracle) {
497                     if ($_field->type == 'integer' && $_field->length == '64') {
498                         $_field->length = '38';
499                     }
500                 }
501                 if (isset($typeMapping['lengthTypes']) && is_array($typeMapping['lengthTypes'])) {
502                     foreach ($typeMapping['lengthTypes'] as $maxLength => $type) {
503                         if ($_field->length <= $maxLength) {
504                             $fieldType = $type;
505                             $scale  = '';
506                             if (isset($_field->scale)) {
507                                 $scale = ',' . $_field->scale;
508                             } elseif(isset($typeMapping['defaultScale'])) {
509                                 $scale = ',' . $typeMapping['defaultScale'];
510                             }
511                              
512                             $options = "({$_field->length}{$scale})";
513                             break;
514                         }
515                     }
516                     if (!isset($options)) {
517                         throw new Setup_Backend_Exception_InvalidSchema("Could not get field declaration for field {$_field->name}: The given length of {$_field->length} is not supported by field type {$_field->type}");
518                     }
519                 } else {
520                     throw new Setup_Backend_Exception_InvalidSchema("Could not get field declaration for field {$_field->name}: Length option was specified but is not supported by field type {$_field->type}");
521                 }
522             } else {
523                 $options = '';
524                 if (isset($_field->value)) {
525                     foreach ($_field->value as $value) {
526                         $values[] = $value;
527                     }
528                     $options = "('" . implode("','", $values) . "')";
529                 } elseif(isset($typeMapping['defaultLength'])) {
530                     $scale = isset($typeMapping['defaultScale']) ? ',' . $typeMapping['defaultScale'] : '';
531                     $options = "({$typeMapping['defaultLength']}{$scale})";
532                 }
533             }
534
535             $_buffer[] = $fieldType . $options;
536         }
537         
538         return $_buffer;
539     }
540     
541     protected function _addDeclarationDefaultValue(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
542     {
543         if (isset($_field->default)) {
544             $_buffer[] = $this->_db->quoteInto("DEFAULT ?", $_field->default) ;
545         }
546         return $_buffer;
547     }
548     
549     protected function _addDeclarationNotNull(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
550     {
551         if ($_field->notnull === true) {
552             $_buffer[] = 'NOT NULL';
553         }
554         return $_buffer;
555     }
556     
557     protected function _addDeclarationUnsigned(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
558     {
559         if (isset($_field->unsigned) && $_field->unsigned === true) {
560             $_buffer[] = 'unsigned';
561         }
562         return $_buffer;
563     }
564
565     protected function _addDeclarationAutoincrement(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
566     {
567         if (isset($_field->autoincrement) && $_field->autoincrement === true) {
568             $_buffer[] = 'auto_increment';
569         }
570         return $_buffer;
571     }
572
573     protected function _addDeclarationComment(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
574     {
575         if (isset($_field->comment)) {
576             $_buffer[] = "COMMENT '" .  $_field->comment . "'";
577         }
578         return $_buffer;
579     }
580     
581     protected function _sanititzeName($_name)
582     {
583         if (strlen($_name) > Setup_Backend_Abstract::MAX_NAME_LENGTH) {
584             $_name = substr(md5($_name), 0 , Setup_Backend_Abstract::MAX_NAME_LENGTH);
585         }
586         return $_name;
587     }
588 }