Merge branch '2015.11-develop' into 2016.11
[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         // read description from database
310         $columns = $this->_db->describeTable(SQL_TABLE_PREFIX . $_tableName);
311         return (isset($columns[$_columnName]) || array_key_exists($_columnName, $columns));
312     }
313     
314     /**
315      * drop column/field in database table
316      * 
317      * @param string tableName
318      * @param string column/field name 
319      */    
320     public function dropCol($_tableName, $_colName)
321     {
322         $statement = 'ALTER TABLE ' . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . ' DROP COLUMN ' . $this->_db->quoteIdentifier($_colName);
323         $this->execQueryVoid($statement);
324     }
325     
326     /**
327      * add a primary key to database table
328      * 
329      * Delegates to {@see addPrimaryKey()}
330      * 
331      * @param string tableName 
332      * @param Setup_Backend_Schema_Index_Abstract declaration
333      */
334     public function addPrimaryKey($_tableName, Setup_Backend_Schema_Index_Abstract $_declaration)
335     {
336         $this->addIndex($_tableName, $_declaration);
337     }
338     
339     /**
340      * removes a primary key from database table
341      * 
342      * @param string tableName (there is just one primary key...)
343      */
344     public function dropPrimaryKey($_tableName)
345     {
346         $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . " DROP PRIMARY KEY " ;
347         $this->execQueryVoid($statement);
348     }
349
350     /**
351      * add a foreign key to database table
352      * 
353      * @param string tableName
354      * @param Setup_Backend_Schema_Index_Abstract declaration
355      */
356     public function addForeignKey($_tableName, Setup_Backend_Schema_Index_Abstract $_declaration)
357     {
358         $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . " ADD " 
359                     . $this->getForeignKeyDeclarations($_declaration, $_tableName);
360         $this->execQueryVoid($statement);
361     }
362     
363     /**
364      * removes a foreign key from database table
365      * 
366      * @param string tableName
367      * @param string foreign key name
368      */
369     public function dropForeignKey($_tableName, $_name)
370     {
371         try {
372             $this->_dropForeignKey($_tableName, SQL_TABLE_PREFIX . $_name);
373         } catch (Zend_Db_Statement_Exception $zdse) {
374             // try it again without table prefix
375             try {
376                 $this->_dropForeignKey($_tableName, $_name);
377             } catch (Zend_Db_Statement_Exception $zdse) {
378                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
379                     . ' ' . $zdse);
380                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
381                     . ' At first remove constraint, then remove key ...');
382                 
383                 $constraint = str_replace(array(
384                     '::',
385                     '--'
386                 ), '??', $_name);
387                 try {
388                     $this->_dropForeignKey($_tableName, SQL_TABLE_PREFIX . $constraint);
389                     $this->_dropForeignKey($_tableName, SQL_TABLE_PREFIX . $_name, FALSE);
390                 } catch (Zend_Db_Statement_Exception $zdse) {
391                     // do it again without prefix
392                     $this->_dropForeignKey($_tableName, $constraint);
393                     $this->_dropForeignKey($_tableName, $_name, FALSE);
394                 }
395             }
396         }
397     }
398     
399     /**
400      * helper function for removing (foreign) keys
401      * 
402      * @param string tableName
403      * @param string $keyName
404      * @param boolean $foreign
405      */
406     protected function _dropForeignKey($tableName, $keyName, $foreign = TRUE)
407     {
408         $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $tableName) 
409             . " DROP" . ($foreign ? ' FOREIGN' : '') . " KEY `" . $keyName . "`" ;
410         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
411             . ' ' . $statement);
412         $this->execQueryVoid($statement);
413     }
414     
415     /**
416      * removes a key from database table
417      * 
418      * @param string tableName 
419      * @param string key name
420      */
421     public function dropIndex($_tableName, $_indexName)
422     {
423         $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . " DROP INDEX " . $this->_db->quoteIdentifier($_indexName);
424         try {
425             $this->execQueryVoid($statement);
426         } catch (Zend_Db_Statement_Exception $zdse) {
427             if (Setup_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
428                 . ' ' . $zdse);
429             
430             // try it again with table prefix
431             $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . " DROP INDEX " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_indexName);
432             $this->execQueryVoid($statement);
433         }
434     }
435
436     /**
437      * create the right mysql-statement-snippet for columns/fields
438      *
439      * @param Setup_Backend_Schema_Field_Abstract field / column
440      * @param String | optional $_tableName [Not used in this backend (MySQL)]
441      * @return string
442      */
443     public function getFieldDeclarations(Setup_Backend_Schema_Field_Abstract $_field, $_tableName = '')
444     {
445         $buffer = $this->_getFieldDeclarations($_field, $_tableName);
446
447         $definition = implode(' ', $buffer);
448
449         return $definition;
450     }
451     
452     /**
453      * Concrete implementation of 
454      * @see tine20/Setup/Backend/Setup_Backend_Interface#checkTable($_table)
455      */
456     public function checkTable(Setup_Backend_Schema_Table_Abstract $_table)
457     {
458         $dbTable = $this->getExistingSchema($_table->name);
459         return $dbTable->equals($_table);
460     }
461
462     /**
463      * Backup Database
464      *
465      * @param $options
466      * @throws Setup_Backend_Exception_NotImplemented
467      */
468     public function backup($options)
469     {
470         throw new Setup_Backend_Exception_NotImplemented('backup not yet implemented');
471     }
472
473     /**
474      * Restore Database
475      *
476      * @param $options
477      * @throws Setup_Backend_Exception_NotImplemented
478      */
479     public function restore($options)
480     {
481         throw new Setup_Backend_Exception_NotImplemented('restore not yet implemented');
482     }
483
484     /**
485      * create the right mysql-statement-snippet for columns/fields
486      *
487      * @param Setup_Backend_Schema_Field_Abstract field / column
488      * @param String | optional $_tableName [Not used in this backend (MySQL)]
489      * @return string
490      */
491     protected function _getFieldDeclarations(Setup_Backend_Schema_Field_Abstract $_field, $_tableName = '')
492     {
493         $buffer = array();
494         $buffer[] = '  ' . $this->_db->quoteIdentifier($_field->name);
495
496         $buffer = $this->_addDeclarationFieldType($buffer, $_field, $_tableName);
497         $buffer = $this->_addDeclarationUnsigned($buffer, $_field);
498         $buffer = $this->_addDeclarationDefaultValue($buffer, $_field);
499         $buffer = $this->_addDeclarationNotNull($buffer, $_field);
500         $buffer = $this->_addDeclarationAutoincrement($buffer, $_field);
501         $buffer = $this->_addDeclarationComment($buffer, $_field);
502         
503         return $buffer;
504     }
505     
506     protected function _addDeclarationFieldType(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field, $_tableName = '')
507     {
508         $typeMapping = $this->getTypeMapping($_field->type);
509         if (!$typeMapping) {
510             throw new Setup_Backend_Exception_InvalidSchema("Could not get field declaration for field {$_field->name}: The given field type {$_field->type} is not supported");
511         }
512         
513         $fieldType = $typeMapping['defaultType'];
514         if (isset($typeMapping['declarationMethod'])) {
515             $fieldBuffer = call_user_func(array($this, $typeMapping['declarationMethod']), $_field, $_tableName);
516             $_buffer = array_merge($_buffer, $fieldBuffer);
517         } else {
518             if ($_field->length !== NULL) {
519                 if ($this->_db instanceof Zend_Db_Adapter_Oracle) {
520                     if ($_field->type == 'integer' && $_field->length == '64') {
521                         $_field->length = '38';
522                     }
523                 }
524                 if (isset($typeMapping['lengthTypes']) && is_array($typeMapping['lengthTypes'])) {
525                     foreach ($typeMapping['lengthTypes'] as $maxLength => $type) {
526                         if ($_field->length <= $maxLength) {
527                             $fieldType = $type;
528                             $scale  = '';
529                             if (isset($_field->scale)) {
530                                 $scale = ',' . $_field->scale;
531                             } elseif(isset($typeMapping['defaultScale'])) {
532                                 $scale = ',' . $typeMapping['defaultScale'];
533                             }
534
535                             if (!isset($typeMapping['lengthLessTypes']) || ! in_array($type, $typeMapping['lengthLessTypes'])) {
536                                 $options = "({$_field->length}{$scale})";
537                             } else {
538                                 $options = '';
539                             }
540                             break;
541                         }
542                     }
543                     if (!isset($options)) {
544                         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}");
545                     }
546                 } else {
547                     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}");
548                 }
549             } else {
550                 $options = '';
551                 if (isset($_field->value)) {
552                     foreach ($_field->value as $value) {
553                         $values[] = $value;
554                     }
555                     $options = "('" . implode("','", $values) . "')";
556                 } elseif(isset($typeMapping['defaultLength'])) {
557                     $scale = isset($typeMapping['defaultScale']) ? ',' . $typeMapping['defaultScale'] : '';
558                     $options = "({$typeMapping['defaultLength']}{$scale})";
559                 }
560             }
561
562             $_buffer[] = $fieldType . $options;
563         }
564         
565         return $_buffer;
566     }
567     
568     protected function _addDeclarationDefaultValue(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
569     {
570         if (isset($_field->default)) {
571             $_buffer[] = $this->_db->quoteInto("DEFAULT ?", $_field->default) ;
572         }
573         return $_buffer;
574     }
575     
576     protected function _addDeclarationNotNull(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
577     {
578         if ($_field->notnull === true) {
579             $_buffer[] = 'NOT NULL';
580         }
581         return $_buffer;
582     }
583     
584     protected function _addDeclarationUnsigned(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
585     {
586         if (isset($_field->unsigned) && $_field->unsigned === true) {
587             $_buffer[] = 'unsigned';
588         }
589         return $_buffer;
590     }
591
592     protected function _addDeclarationAutoincrement(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
593     {
594         if (isset($_field->autoincrement) && $_field->autoincrement === true) {
595             $_buffer[] = 'auto_increment';
596         }
597         return $_buffer;
598     }
599
600     protected function _addDeclarationComment(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
601     {
602         if (isset($_field->comment)) {
603             $_buffer[] = "COMMENT '" .  $_field->comment . "'";
604         }
605         return $_buffer;
606     }
607     
608     protected function _sanititzeName($_name)
609     {
610         if (strlen($_name) > Setup_Backend_Abstract::MAX_NAME_LENGTH) {
611             $_name = substr(md5($_name), 0 , Setup_Backend_Abstract::MAX_NAME_LENGTH);
612         }
613         return $_name;
614     }
615 }