From 5e434adfb236f4c0fb267534d4741ffdd5dd559d Mon Sep 17 00:00:00 2001 From: Adam Roses Wight Date: Sat, 30 Nov 2013 16:31:22 -0800 Subject: [PATCH] Split code generation into components. Parsing the specification has become its own class. Templating has been abstracted, now beautifying is built into all php generation. We lose the singleton efficiency, it gained us a few seconds at most. Generation tasks are split into ITasks, and are fed the main CG object as if it were a configuration object. Eventually we will want to make the configuration a first-class object so that it is customizable. ---------------------------------------- * CRM-13871: Provide an ORM layer: Doctrine http://issues.civicrm.org/jira/browse/CRM-13871 --- .gitignore | 2 +- CRM/Core/CodeGen/BaseTask.php | 15 + CRM/Core/CodeGen/Config.php | 65 ++ CRM/Core/CodeGen/DAO.php | 29 + CRM/Core/CodeGen/I18n.php | 56 ++ CRM/Core/CodeGen/ITask.php | 18 + CRM/Core/CodeGen/Main.php | 900 +------------------------- CRM/Core/CodeGen/Reflection.php | 17 + CRM/Core/CodeGen/Schema.php | 121 ++++ CRM/Core/CodeGen/Specification.php | 590 +++++++++++++++++ CRM/Core/CodeGen/Test.php | 27 + CRM/Core/CodeGen/Util/Template.php | 92 +++ CRM/Core/CodeGen/Util/Xml.php | 16 + tests/phpunit/CiviTest/.gitignore | 1 + xml/templates/civicrm_version_sql.tpl | 1 + 15 files changed, 1075 insertions(+), 875 deletions(-) create mode 100644 CRM/Core/CodeGen/BaseTask.php create mode 100644 CRM/Core/CodeGen/Config.php create mode 100644 CRM/Core/CodeGen/DAO.php create mode 100644 CRM/Core/CodeGen/I18n.php create mode 100644 CRM/Core/CodeGen/ITask.php create mode 100644 CRM/Core/CodeGen/Reflection.php create mode 100644 CRM/Core/CodeGen/Schema.php create mode 100644 CRM/Core/CodeGen/Specification.php create mode 100644 CRM/Core/CodeGen/Test.php create mode 100644 CRM/Core/CodeGen/Util/Template.php create mode 100644 CRM/Core/CodeGen/Util/Xml.php create mode 100644 tests/phpunit/CiviTest/.gitignore create mode 100644 xml/templates/civicrm_version_sql.tpl diff --git a/.gitignore b/.gitignore index fa113526e0..f0b077398a 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ CRM/Contact/DAO/RelationshipType.php CRM/Contact/DAO/SavedSearch.php CRM/Contact/DAO/SubscriptionHistory.php CRM/Contribute/DAO +CRM/Core/BAO/AllCoreTables.php CRM/Core/DAO/.listAll.php CRM/Core/DAO/listAll.php CRM/Core/DAO/ActionLog.php @@ -27,7 +28,6 @@ CRM/Core/DAO/ActionMapping.php CRM/Core/DAO/ActionSchedule.php CRM/Core/DAO/Address.php CRM/Core/DAO/AddressFormat.php -CRM/Core/DAO/AllCoreTables.php CRM/Core/DAO/Batch.php CRM/Core/DAO/Cache.php CRM/Core/DAO/Component.php diff --git a/CRM/Core/CodeGen/BaseTask.php b/CRM/Core/CodeGen/BaseTask.php new file mode 100644 index 0000000000..a4cd151fb8 --- /dev/null +++ b/CRM/Core/CodeGen/BaseTask.php @@ -0,0 +1,15 @@ +config = $config; + $this->tables = $this->config->tables; + } +} diff --git a/CRM/Core/CodeGen/Config.php b/CRM/Core/CodeGen/Config.php new file mode 100644 index 0000000000..de7ae4b99a --- /dev/null +++ b/CRM/Core/CodeGen/Config.php @@ -0,0 +1,65 @@ +generateTemplateVersion(); + + $this->setupCms(); + } + + function generateTemplateVersion() { + file_put_contents($this->config->tplCodePath . "/CRM/common/version.tpl", $this->config->db_version); + } + + function setupCms() { + if (!in_array($this->config->cms, array( + 'drupal', 'joomla', 'wordpress'))) { + echo "Config file for '{$this->config->cms}' not known."; + exit(); + } + elseif ($this->config->cms !== 'joomla') { + $configTemplate = $this->findConfigTemplate($this->config->cms); + if ($configTemplate) { + echo "Generating civicrm.config.php\n"; + copy($configTemplate, '../civicrm.config.php'); + } else { + throw new Exception("Failed to locate template for civicrm.config.php"); + } + } + + echo "Generating civicrm-version file\n"; + $template = new CRM_Core_CodeGen_Util_Template('php'); + $template->assign('db_version', $this->config->db_version); + $template->assign('cms', ucwords($this->config->cms)); + $template->run('civicrm_version.tpl', $this->config->phpCodePath . "civicrm-version.php"); + } + + /** + * @param string $cms "drupal"|"wordpress" + * @return null|string path to config template + */ + public function findConfigTemplate($cms) { + $candidates = array(); + switch ($cms) { + case 'drupal': + $candidates[] = "../drupal/civicrm.config.php.drupal"; + $candidates[] = "../../drupal/civicrm.config.php.drupal"; + break; + case 'wordpress': + $candidates[] = "../../civicrm.config.php.wordpress"; + $candidates[] = "../WordPress/civicrm.config.php.wordpress"; + $candidates[] = "../drupal/civicrm.config.php.drupal"; + break; + } + foreach ($candidates as $candidate) { + if (file_exists($candidate)) { + return $candidate; + break; + } + } + return NULL; + } +} diff --git a/CRM/Core/CodeGen/DAO.php b/CRM/Core/CodeGen/DAO.php new file mode 100644 index 0000000000..5ee51d9c0b --- /dev/null +++ b/CRM/Core/CodeGen/DAO.php @@ -0,0 +1,29 @@ +generateDAOs(); + } + + function generateDAOs() { + foreach (array_keys($this->tables) as $name) { + echo "Generating $name as " . $this->tables[$name]['fileName'] . "\n"; + + if (empty($this->tables[$name]['base'])) { + echo "No base defined for $name, skipping output generation\n"; + continue; + } + + $template = new CRM_Core_CodeGen_Util_Template('php'); + $template->assign('table', $this->tables[$name]); + + $directory = $this->config->phpCodePath . $this->tables[$name]['base']; + CRM_Core_CodeGen_Util_File::createDir($directory); + + $template->run('dao.tpl', $directory . $this->tables[$name]['fileName']); + } + } +} diff --git a/CRM/Core/CodeGen/I18n.php b/CRM/Core/CodeGen/I18n.php new file mode 100644 index 0000000000..42aa2e0af3 --- /dev/null +++ b/CRM/Core/CodeGen/I18n.php @@ -0,0 +1,56 @@ +generateInstallLangs(); + $this->generateSchemaStructure(); + } + + function generateInstallLangs() { + // CRM-7161: generate install/langs.php from the languages template + // grep it for enabled languages and create a 'xx_YY' => 'Language name' $langs mapping + $matches = array(); + preg_match_all('/, 1, \'([a-z][a-z]_[A-Z][A-Z])\', \'..\', \{localize\}\'\{ts escape="sql"\}(.+)\{\/ts\}\'\{\/localize\}, /', file_get_contents('templates/languages.tpl'), $matches); + $langs = array(); + for ($i = 0; $i < count($matches[0]); $i++) { + $langs[$matches[1][$i]] = $matches[2][$i]; + } + file_put_contents('../install/langs.php', "tables as $table) { + if ($table['localizable']) { + $columns[$table['name']] = array(); + } + else { + continue; + } + foreach ($table['fields'] as $field) { + if ($field['localizable']) { + $columns[$table['name']][$field['name']] = $field['sqlType']; + } + } + if (isset($table['index'])) { + foreach ($table['index'] as $index) { + if ($index['localizable']) { + $indices[$table['name']][$index['name']] = $index; + } + } + } + } + + $template = new CRM_Core_CodeGen_Util_Template('php'); + + $template->assign('columns', $columns); + $template->assign('indices', $indices); + + $template->run('schema_structure.tpl', $this->config->phpCodePath . "/CRM/Core/I18n/SchemaStructure.php"); + } +} diff --git a/CRM/Core/CodeGen/ITask.php b/CRM/Core/CodeGen/ITask.php new file mode 100644 index 0000000000..b95be65719 --- /dev/null +++ b/CRM/Core/CodeGen/ITask.php @@ -0,0 +1,18 @@ +CoreDAOCodePath = $CoreDAOCodePath; $this->sqlCodePath = $sqlCodePath; $this->phpCodePath = $phpCodePath; $this->tplCodePath = $tplCodePath; - $this->cms = $argCms; - - require_once 'Smarty/Smarty.class.php'; - $this->smarty = new Smarty(); - $this->smarty->template_dir = './templates'; - $this->smarty->plugins_dir = $smartyPluginDirs; - $this->compileDir = CRM_Core_CodeGen_Util_File::createTempDir('templates_c_'); - $this->smarty->compile_dir = $this->compileDir; - $this->smarty->clear_all_cache(); - // CRM-5308 / CRM-3507 - we need {localize} to work in the templates - require_once 'CRM/Core/Smarty/plugins/block.localize.php'; - $this->smarty->register_block('localize', 'smarty_block_localize'); - - require_once 'PHP/Beautifier.php'; - // create an instance - $this->beautifier = new PHP_Beautifier(); - $this->beautifier->addFilter('ArrayNested'); - // add one or more filters - $this->beautifier->addFilter('Pear'); - // add one or more filters - $this->beautifier->addFilter('NewLines', array('after' => 'class, public, require, comment')); - $this->beautifier->setIndentChar(' '); - $this->beautifier->setIndentNumber(2); - $this->beautifier->setNewLine("\n"); + // default cms is 'drupal', if not specified + $this->cms = isset($argCms) ? strtolower($argCms) : 'drupal'; - CRM_Core_CodeGen_Util_File::createDir($this->sqlCodePath); + CRM_Core_CodeGen_Util_Template::$smartyPluginDirs = $smartyPluginDirs; $versionFile = "version.xml"; - $versionXML = &$this->parseInput($versionFile); + $versionXML = CRM_Core_CodeGen_Util_Xml::parse($versionFile); $this->db_version = $versionXML->version_no; $this->buildVersion = preg_replace('/^(\d{1,2}\.\d{1,2})\.(\d{1,2}|\w{4,7})$/i', '$1', $this->db_version); if (isset($argVersion)) { @@ -60,10 +34,6 @@ class CRM_Core_CodeGen_Main { $this->schemaPath = $schemaPath; } - function __destruct() { - CRM_Core_CodeGen_Util_File::removeDir($this->compileDir); - } - /** * Automatically generate a variety of files * @@ -85,853 +55,35 @@ Alternatively you can get a version of CiviCRM that matches your PHP version exit(); } - $this->generateTemplateVersion(); - - $this->setupCms($this->db_version); - - echo "Parsing input file ".$this->schemaPath."\n"; - $dbXML = $this->parseInput($this->schemaPath); - // print_r( $dbXML ); - - echo "Extracting database information\n"; - $database = &$this->getDatabase($dbXML); - // print_r( $database ); - - $this->classNames = array(); - - echo "Extracting table information\n"; - $tables = &$this->getTables($dbXML, $database); - - $this->resolveForeignKeys($tables, $this->classNames); - $tables = $this->orderTables($tables); - - // add archive tables here - $archiveTables = array( ); - foreach ($tables as $name => $table ) { - if ( $table['archive'] == 'true' ) { - $name = 'archive_' . $table['name']; - $table['name'] = $name; - $table['archive'] = 'false'; - if ( isset($table['foreignKey']) ) { - foreach ($table['foreignKey'] as $fkName => $fkValue) { - if ($tables[$fkValue['table']]['archive'] == 'true') { - $table['foreignKey'][$fkName]['table'] = 'archive_' . $table['foreignKey'][$fkName]['table']; - $table['foreignKey'][$fkName]['uniqName'] = - str_replace( 'FK_', 'FK_archive_', $table['foreignKey'][$fkName]['uniqName'] ); - } - } - $archiveTables[$name] = $table; - } - } - } - - $this->generateListAll($tables); - $this->generateCiviTestTruncate($tables); - $this->generateCreateSql($database, $tables, 'civicrm.mysql'); - $this->generateDropSql($tables, 'civicrm_drop.mysql'); - - // also create the archive tables - // $this->generateCreateSql($database, $archiveTables, 'civicrm_archive.mysql' ); - // $this->generateDropSql($archiveTables, 'civicrm_archive_drop.mysql'); - - $this->generateNavigation(); - $this->generateLocalDataSql($this->findLocales()); - $this->generateSample(); - $this->generateInstallLangs(); - $this->generateDAOs($tables); - $this->generateSchemaStructure($tables); - } - - function generateListAll($tables) { - $this->smarty->clear_all_assign(); - $this->smarty->assign('tables', $tables); - file_put_contents($this->CoreDAOCodePath . "AllCoreTables.php", $this->smarty->fetch('listAll.tpl')); - } - - function generateCiviTestTruncate($tables) { - echo "Generating tests truncate file\n"; - - $truncate = ' - - '; - $tbls = array_keys($tables); - foreach ($tbls as $d => $t) { - $truncate = $truncate . "\n <$t />\n"; - } - - $truncate = $truncate . "\n"; - file_put_contents($this->sqlCodePath . "../tests/phpunit/CiviTest/truncate.xml", $truncate); - unset($truncate); - } - - function generateCreateSql($database, $tables, $fileName = 'civicrm.mysql') { - echo "Generating sql file\n"; - $this->reset_smarty_assignments(); - $this->smarty->assign_by_ref('database', $database); - $this->smarty->assign_by_ref('tables', $tables); - $dropOrder = array_reverse(array_keys($tables)); - $this->smarty->assign_by_ref('dropOrder', $dropOrder); - $this->smarty->assign('mysql', 'modern'); - file_put_contents($this->sqlCodePath . $fileName, $this->smarty->fetch('schema.tpl')); - } - - function generateDropSql($tables, $fileName = 'civicrm_drop.mysql') { - echo "Generating sql drop tables file\n"; - $dropOrder = array_reverse(array_keys($tables)); - $this->smarty->assign_by_ref('dropOrder', $dropOrder); - file_put_contents($this->sqlCodePath . $fileName, $this->smarty->fetch('drop.tpl')); - } - - function generateNavigation() { - echo "Generating navigation file\n"; - $this->reset_smarty_assignments(); - file_put_contents($this->sqlCodePath . "civicrm_navigation.mysql", $this->smarty->fetch('civicrm_navigation.tpl')); - } - - function generateLocalDataSql($locales) { - $this->reset_smarty_assignments(); - - global $tsLocale; - $oldTsLocale = $tsLocale; - foreach ($locales as $locale) { - echo "Generating data files for $locale\n"; - $tsLocale = $locale; - $this->smarty->assign('locale', $locale); - - $data = array(); - $data[] = $this->smarty->fetch('civicrm_country.tpl'); - $data[] = $this->smarty->fetch('civicrm_state_province.tpl'); - $data[] = $this->smarty->fetch('civicrm_currency.tpl'); - $data[] = $this->smarty->fetch('civicrm_data.tpl'); - $data[] = $this->smarty->fetch('civicrm_navigation.tpl'); - - $data[] = " UPDATE civicrm_domain SET version = '" . $this->db_version . "';"; - - $data = implode("\n", $data); - - $ext = ($locale != 'en_US' ? ".$locale" : ''); - // write the initialize base-data sql script - file_put_contents($this->sqlCodePath . "civicrm_data$ext.mysql", $data); - - // write the acl sql script - file_put_contents($this->sqlCodePath . "civicrm_acl$ext.mysql", $this->smarty->fetch('civicrm_acl.tpl')); - } - $tsLocale = $oldTsLocale; - } - - function generateSample() { - $this->reset_smarty_assignments(); - $sample = $this->smarty->fetch('civicrm_sample.tpl'); - $sample .= $this->smarty->fetch('civicrm_acl.tpl'); - file_put_contents($this->sqlCodePath . 'civicrm_sample.mysql', $sample); - } - - function generateInstallLangs() { - // CRM-7161: generate install/langs.php from the languages template - // grep it for enabled languages and create a 'xx_YY' => 'Language name' $langs mapping - $matches = array(); - preg_match_all('/, 1, \'([a-z][a-z]_[A-Z][A-Z])\', \'..\', \{localize\}\'\{ts escape="sql"\}(.+)\{\/ts\}\'\{\/localize\}, /', file_get_contents('templates/languages.tpl'), $matches); - $langs = array(); - for ($i = 0; $i < count($matches[0]); $i++) { - $langs[$matches[1][$i]] = $matches[2][$i]; - } - file_put_contents('../install/langs.php', "smarty->clear_all_cache(); - echo "Generating $name as " . $tables[$name]['fileName'] . "\n"; - $this->reset_smarty_assignments(); - - $this->smarty->assign_by_ref('table', $tables[$name]); - $php = $this->smarty->fetch('dao.tpl'); - - $this->beautifier->setInputString($php); - - if (empty($tables[$name]['base'])) { - echo "No base defined for $name, skipping output generation\n"; - continue; - } - - $directory = $this->phpCodePath . $tables[$name]['base']; - CRM_Core_CodeGen_Util_File::createDir($directory); - $this->beautifier->setOutputFile($directory . $tables[$name]['fileName']); - // required - $this->beautifier->process(); - - $this->beautifier->save(); - } - } - - function generateSchemaStructure($tables) { - echo "Generating CRM_Core_I18n_SchemaStructure...\n"; - $columns = array(); - $indices = array(); - foreach ($tables as $table) { - if ($table['localizable']) { - $columns[$table['name']] = array(); - } - else { - continue; - } - foreach ($table['fields'] as $field) { - if ($field['localizable']) { - $columns[$table['name']][$field['name']] = $field['sqlType']; - } - } - if (isset($table['index'])) { - foreach ($table['index'] as $index) { - if ($index['localizable']) { - $indices[$table['name']][$index['name']] = $index; - } - } - } - } - - $this->reset_smarty_assignments(); - $this->smarty->assign_by_ref('columns', $columns); - $this->smarty->assign_by_ref('indices', $indices); - - $this->beautifier->setInputString($this->smarty->fetch('schema_structure.tpl')); - $this->beautifier->setOutputFile($this->phpCodePath . "/CRM/Core/I18n/SchemaStructure.php"); - $this->beautifier->process(); - $this->beautifier->save(); - } - - function generateTemplateVersion() { - file_put_contents($this->tplCodePath . "/CRM/common/version.tpl", $this->db_version); - } - - function findLocales() { - require_once 'CRM/Core/Config.php'; - $config = CRM_Core_Config::singleton(FALSE); - $locales = array(); - if (substr($config->gettextResourceDir, 0, 1) === '/') { - $localeDir = $config->gettextResourceDir; - } - else { - $localeDir = '../' . $config->gettextResourceDir; - } - if (file_exists($localeDir)) { - $config->gettextResourceDir = $localeDir; - $locales = preg_grep('/^[a-z][a-z]_[A-Z][A-Z]$/', scandir($localeDir)); - } - - $localesMask = getenv('CIVICRM_LOCALES'); - if (!empty($localesMask)) { - $mask = explode(',', $localesMask); - $locales = array_intersect($locales, $mask); - } - - if (!in_array('en_US', $locales)) { - array_unshift($locales, 'en_US'); - } - - return $locales; - } - - function setupCms() { - // default cms is 'drupal', if not specified - $this->cms = isset($this->cms) ? strtolower($this->cms) : 'drupal'; - if (!in_array($this->cms, array( - 'drupal', 'joomla', 'wordpress'))) { - echo "Config file for '{$this->cms}' not known."; - exit(); - } - elseif ($this->cms !== 'joomla') { - $configTemplate = $this->findConfigTemplate($this->cms); - if ($configTemplate) { - echo "Generating civicrm.config.php\n"; - copy($configTemplate, '../civicrm.config.php'); - } else { - throw new Exception("Failed to locate template for civicrm.config.php"); - } - } - - echo "Generating civicrm-version file\n"; - $this->smarty->assign('db_version', $this->db_version); - $this->smarty->assign('cms', ucwords($this->cms)); - file_put_contents($this->phpCodePath . "civicrm-version.php", $this->smarty->fetch('civicrm_version.tpl')); - } - - /** - * @param string $cms "drupal"|"wordpress" - * @return null|string path to config template - */ - public function findConfigTemplate($cms) { - $candidates = array(); - switch ($cms) { - case 'drupal': - $candidates[] = "../drupal/civicrm.config.php.drupal"; - $candidates[] = "../../drupal/civicrm.config.php.drupal"; - break; - case 'wordpress': - $candidates[] = "../../civicrm.config.php.wordpress"; - $candidates[] = "../WordPress/civicrm.config.php.wordpress"; - $candidates[] = "../drupal/civicrm.config.php.drupal"; - break; - } - foreach ($candidates as $candidate) { - if (file_exists($candidate)) { - return $candidate; - break; - } - } - return NULL; - } - - // ----------------------------- - // ---- Schema manipulation ---- - // ----------------------------- - function &parseInput($file) { - $dom = new DomDocument(); - $dom->load($file); - $dom->xinclude(); - $dbXML = simplexml_import_dom($dom); - return $dbXML; - } - - function &getDatabase(&$dbXML) { - $database = array('name' => trim((string ) $dbXML->name)); - - $attributes = ''; - $this->checkAndAppend($attributes, $dbXML, 'character_set', 'DEFAULT CHARACTER SET ', ''); - $this->checkAndAppend($attributes, $dbXML, 'collate', 'COLLATE ', ''); - $database['attributes'] = $attributes; - - $tableAttributes_modern = $tableAttributes_simple = ''; - $this->checkAndAppend($tableAttributes_modern, $dbXML, 'table_type', 'ENGINE=', ''); - $this->checkAndAppend($tableAttributes_simple, $dbXML, 'table_type', 'TYPE=', ''); - $database['tableAttributes_modern'] = trim($tableAttributes_modern . ' ' . $attributes); - $database['tableAttributes_simple'] = trim($tableAttributes_simple); - - $database['comment'] = $this->value('comment', $dbXML, ''); - - return $database; - } - - function &getTables(&$dbXML, &$database) { - $tables = array(); - foreach ($dbXML->tables as $tablesXML) { - foreach ($tablesXML->table as $tableXML) { - if ($this->value('drop', $tableXML, 0) > 0 and $this->value('drop', $tableXML, 0) <= $this->buildVersion) { - continue; - } - - if ($this->value('add', $tableXML, 0) <= $this->buildVersion) { - $this->getTable($tableXML, $database, $tables); - } - } - } - - return $tables; - } - - function resolveForeignKeys(&$tables, &$classNames) { - foreach (array_keys($tables) as $name) { - $this->resolveForeignKey($tables, $classNames, $name); - } - } - - function resolveForeignKey(&$tables, &$classNames, $name) { - if (!array_key_exists('foreignKey', $tables[$name])) { - return; - } - - foreach (array_keys($tables[$name]['foreignKey']) as $fkey) { - $ftable = $tables[$name]['foreignKey'][$fkey]['table']; - if (!array_key_exists($ftable, $classNames)) { - echo "$ftable is not a valid foreign key table in $name\n"; - continue; - } - $tables[$name]['foreignKey'][$fkey]['className'] = $classNames[$ftable]; - $tables[$name]['foreignKey'][$fkey]['fileName'] = str_replace('_', '/', $classNames[$ftable]) . '.php'; - $tables[$name]['fields'][$fkey]['FKClassName'] = $classNames[$ftable]; - } - } - - function orderTables(&$tables) { - $ordered = array(); - - while (!empty($tables)) { - foreach (array_keys($tables) as $name) { - if ($this->validTable($tables, $ordered, $name)) { - $ordered[$name] = $tables[$name]; - unset($tables[$name]); - } - } - } - return $ordered; - } - - function validTable(&$tables, &$valid, $name) { - if (!array_key_exists('foreignKey', $tables[$name])) { - return TRUE; - } + $specification = new CRM_Core_CodeGen_Specification(); + $specification->parse($this->schemaPath, $this->buildVersion); + # cheese: + $this->database = $specification->database; + $this->tables = $specification->tables; - foreach (array_keys($tables[$name]['foreignKey']) as $fkey) { - $ftable = $tables[$name]['foreignKey'][$fkey]['table']; - if (!array_key_exists($ftable, $valid) && $ftable !== $name) { - return FALSE; - } - } - return TRUE; + $this->runAllTasks(); } - function getTable($tableXML, &$database, &$tables) { - $name = trim((string ) $tableXML->name); - $klass = trim((string ) $tableXML->class); - $base = $this->value('base', $tableXML); - $sourceFile = "xml/schema/{$base}/{$klass}.xml"; - $daoPath = "{$base}/DAO/"; - $pre = str_replace('/', '_', $daoPath); - $this->classNames[$name] = $pre . $klass; - - $localizable = FALSE; - foreach ($tableXML->field as $fieldXML) { - if ($fieldXML->localizable) { - $localizable = TRUE; - break; - } - } - - $table = array( - 'name' => $name, - 'base' => $daoPath, - 'sourceFile' => $sourceFile, - 'fileName' => $klass . '.php', - 'objectName' => $klass, - 'labelName' => substr($name, 8), - 'className' => $this->classNames[$name], - 'attributes_simple' => trim($database['tableAttributes_simple']), - 'attributes_modern' => trim($database['tableAttributes_modern']), - 'comment' => $this->value('comment', $tableXML), - 'localizable' => $localizable, - 'log' => $this->value('log', $tableXML, 'false'), - 'archive' => $this->value('archive', $tableXML, 'false'), + function runAllTasks() { + // TODO: This configuration can be manipulated dynamically. + $components = array( + 'CRM_Core_CodeGen_Config', + 'CRM_Core_CodeGen_Reflection', + 'CRM_Core_CodeGen_Schema', + 'CRM_Core_CodeGen_DAO', + 'CRM_Core_CodeGen_Test', + 'CRM_Core_CodeGen_I18n', ); + foreach ($components as $component) { + $task = new $component($this); - $fields = array(); - foreach ($tableXML->field as $fieldXML) { - if ($this->value('drop', $fieldXML, 0) > 0 and $this->value('drop', $fieldXML, 0) <= $this->buildVersion) { - continue; - } - - if ($this->value('add', $fieldXML, 0) <= $this->buildVersion) { - $this->getField($fieldXML, $fields); - } - } - - $table['fields'] = &$fields; - $table['hasEnum'] = FALSE; - foreach ($table['fields'] as $field) { - if ($field['crmType'] == 'CRM_Utils_Type::T_ENUM') { - $table['hasEnum'] = TRUE; - break; - } - } - - if ($this->value('primaryKey', $tableXML)) { - $this->getPrimaryKey($tableXML->primaryKey, $fields, $table); - } - - // some kind of refresh? - CRM_Core_Config::singleton(FALSE); - if ($this->value('index', $tableXML)) { - $index = array(); - foreach ($tableXML->index as $indexXML) { - if ($this->value('drop', $indexXML, 0) > 0 and $this->value('drop', $indexXML, 0) <= $this->buildVersion) { - continue; - } - - $this->getIndex($indexXML, $fields, $index); - } - $table['index'] = &$index; - } - - if ($this->value('foreignKey', $tableXML)) { - $foreign = array(); - foreach ($tableXML->foreignKey as $foreignXML) { - // print_r($foreignXML); - - if ($this->value('drop', $foreignXML, 0) > 0 and $this->value('drop', $foreignXML, 0) <= $this->buildVersion) { - continue; - } - if ($this->value('add', $foreignXML, 0) <= $this->buildVersion) { - $this->getForeignKey($foreignXML, $fields, $foreign, $name); - } - } - $table['foreignKey'] = &$foreign; - } - - if ($this->value('dynamicForeignKey', $tableXML)) { - $dynamicForeign = array(); - foreach ($tableXML->dynamicForeignKey as $foreignXML) { - if ($this->value('drop', $foreignXML, 0) > 0 and $this->value('drop', $foreignXML, 0) <= $this->buildVersion) { - continue; - } - if ($this->value('add', $foreignXML, 0) <= $this->buildVersion) { - $this->getDynamicForeignKey($foreignXML, $dynamicForeign, $name); - } - } - $table['dynamicForeignKey'] = $dynamicForeign; - } - - $tables[$name] = &$table; - return; - } - - function getField(&$fieldXML, &$fields) { - $name = trim((string ) $fieldXML->name); - $field = array('name' => $name, 'localizable' => $fieldXML->localizable); - $type = (string ) $fieldXML->type; - switch ($type) { - case 'varchar': - case 'char': - $field['length'] = (int) $fieldXML->length; - $field['sqlType'] = "$type({$field['length']})"; - $field['phpType'] = 'string'; - $field['crmType'] = 'CRM_Utils_Type::T_STRING'; - $field['size'] = $this->getSize($fieldXML); - break; - - case 'enum': - $value = (string ) $fieldXML->values; - $field['sqlType'] = 'enum('; - $field['values'] = array(); - $field['enumValues'] = $value; - $values = explode(',', $value); - $first = TRUE; - foreach ($values as $v) { - $v = trim($v); - $field['values'][] = $v; - - if (!$first) { - $field['sqlType'] .= ', '; - } - $first = FALSE; - $field['sqlType'] .= "'$v'"; - } - $field['sqlType'] .= ')'; - $field['phpType'] = $field['sqlType']; - $field['crmType'] = 'CRM_Utils_Type::T_ENUM'; - break; - - case 'text': - $field['sqlType'] = $field['phpType'] = $type; - $field['crmType'] = 'CRM_Utils_Type::T_' . strtoupper($type); - $field['rows'] = $this->value('rows', $fieldXML); - $field['cols'] = $this->value('cols', $fieldXML); - break; - - case 'datetime': - $field['sqlType'] = $field['phpType'] = $type; - $field['crmType'] = 'CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME'; - break; - - case 'boolean': - // need this case since some versions of mysql do not have boolean as a valid column type and hence it - // is changed to tinyint. hopefully after 2 yrs this case can be removed. - $field['sqlType'] = 'tinyint'; - $field['phpType'] = $type; - $field['crmType'] = 'CRM_Utils_Type::T_' . strtoupper($type); - break; - - case 'decimal': - $length = $fieldXML->length ? $fieldXML->length : '20,2'; - $field['sqlType'] = 'decimal(' . $length . ')'; - $field['phpType'] = 'float'; - $field['crmType'] = 'CRM_Utils_Type::T_MONEY'; - break; - - case 'float': - $field['sqlType'] = 'double'; - $field['phpType'] = 'float'; - $field['crmType'] = 'CRM_Utils_Type::T_FLOAT'; - break; - - default: - $field['sqlType'] = $field['phpType'] = $type; - if ($type == 'int unsigned') { - $field['crmType'] = 'CRM_Utils_Type::T_INT'; - } - else { - $field['crmType'] = 'CRM_Utils_Type::T_' . strtoupper($type); - } - break; - } - - $field['required'] = $this->value('required', $fieldXML); - $field['collate'] = $this->value('collate', $fieldXML); - $field['comment'] = $this->value('comment', $fieldXML); - $field['default'] = $this->value('default', $fieldXML); - $field['import'] = $this->value('import', $fieldXML); - if ($this->value('export', $fieldXML)) { - $field['export'] = $this->value('export', $fieldXML); - } - else { - $field['export'] = $this->value('import', $fieldXML); - } - $field['rule'] = $this->value('rule', $fieldXML); - $field['title'] = $this->value('title', $fieldXML); - if (!$field['title']) { - $field['title'] = $this->composeTitle($name); - } - $field['headerPattern'] = $this->value('headerPattern', $fieldXML); - $field['dataPattern'] = $this->value('dataPattern', $fieldXML); - $field['uniqueName'] = $this->value('uniqueName', $fieldXML); - $field['pseudoconstant'] = $this->value('pseudoconstant', $fieldXML); - if(!empty($field['pseudoconstant'])){ - //ok this is a bit long-winded but it gets there & is consistent with above approach - $field['pseudoconstant'] = array(); - $validOptions = array( - // Fields can specify EITHER optionGroupName OR table, not both - // (since declaring optionGroupName means we are using the civicrm_option_value table) - 'optionGroupName', - 'table', - // If table is specified, keyColumn and labelColumn are also required - 'keyColumn', - 'labelColumn', - // Non-translated machine name for programmatic lookup. Defaults to 'name' if that column exists - 'nameColumn', - // Where clause snippet (will be joined to the rest of the query with AND operator) - 'condition', - ); - foreach ($validOptions as $pseudoOption) { - if(!empty($fieldXML->pseudoconstant->$pseudoOption)){ - $field['pseudoconstant'][$pseudoOption] = $this->value($pseudoOption, $fieldXML->pseudoconstant); - } - } - // For now, fields that have option lists that are not in the db can simply - // declare an empty pseudoconstant tag and we'll add this placeholder. - // That field's BAO::buildOptions fn will need to be responsible for generating the option list - if (empty($field['pseudoconstant'])) { - $field['pseudoconstant'] = 'not in database'; - } - } - $fields[$name] = &$field; - } - - function composeTitle($name) { - $names = explode('_', strtolower($name)); - $title = ''; - for ($i = 0; $i < count($names); $i++) { - if ($names[$i] === 'id' || $names[$i] === 'is') { - // id's do not get titles - return NULL; - } - - if ($names[$i] === 'im') { - $names[$i] = 'IM'; - } - else { - $names[$i] = ucfirst(trim($names[$i])); - } - - $title = $title . ' ' . $names[$i]; - } - return trim($title); - } - - function getPrimaryKey(&$primaryXML, &$fields, &$table) { - $name = trim((string ) $primaryXML->name); - - /** need to make sure there is a field of type name */ - if (!array_key_exists($name, $fields)) { - echo "primary key $name in $table->name does not have a field definition, ignoring\n"; - return; - } - - // set the autoincrement property of the field - $auto = $this->value('autoincrement', $primaryXML); - $fields[$name]['autoincrement'] = $auto; - $primaryKey = array( - 'name' => $name, - 'autoincrement' => $auto, - ); - $table['primaryKey'] = &$primaryKey; - } - - function getIndex(&$indexXML, &$fields, &$indices) { - //echo "\n\n*******************************************************\n"; - //echo "entering getIndex\n"; - - $index = array(); - // empty index name is fine - $indexName = trim((string)$indexXML->name); - $index['name'] = $indexName; - $index['field'] = array(); - - // populate fields - foreach ($indexXML->fieldName as $v) { - $fieldName = (string)($v); - $length = (string)($v['length']); - if (strlen($length) > 0) { - $fieldName = "$fieldName($length)"; - } - $index['field'][] = $fieldName; - } - - $index['localizable'] = FALSE; - foreach ($index['field'] as $fieldName) { - if (isset($fields[$fieldName]) and $fields[$fieldName]['localizable']) { - $index['localizable'] = TRUE; - break; - } - } - - // check for unique index - if ($this->value('unique', $indexXML)) { - $index['unique'] = TRUE; - } - - //echo "\$index = \n"; - //print_r($index); - - // field array cannot be empty - if (empty($index['field'])) { - echo "No fields defined for index $indexName\n"; - return; - } - - // all fieldnames have to be defined and should exist in schema. - foreach ($index['field'] as $fieldName) { - if (!$fieldName) { - echo "Invalid field defination for index $indexName\n"; - return; - } - $parenOffset = strpos($fieldName, '('); - if ($parenOffset > 0) { - $fieldName = substr($fieldName, 0, $parenOffset); - } - if (!array_key_exists($fieldName, $fields)) { - echo "Table does not contain $fieldName\n"; - print_r($fields); - CRM_Core_CodeGen_Util_File::removeDir($this->compileDir); + if (is_a($task, 'CRM_Core_CodeGen_ITask')) { + $task->setConfig($this); + $task->run(); + } else { + echo "Bad news: we tried to run a codegen task of an unrecognized type: {$component}\n"; exit(); } } - $indices[$indexName] = &$index; - } - - function getForeignKey(&$foreignXML, &$fields, &$foreignKeys, &$currentTableName) { - $name = trim((string ) $foreignXML->name); - - /** need to make sure there is a field of type name */ - if (!array_key_exists($name, $fields)) { - echo "foreign $name in $currentTableName does not have a field definition, ignoring\n"; - return; - } - - /** need to check for existence of table and key **/ - $table = trim($this->value('table', $foreignXML)); - $foreignKey = array( - 'name' => $name, - 'table' => $table, - 'uniqName' => "FK_{$currentTableName}_{$name}", - 'key' => trim($this->value('key', $foreignXML)), - 'import' => $this->value('import', $foreignXML, FALSE), - 'export' => $this->value('import', $foreignXML, FALSE), - // we do this matching in a seperate phase (resolveForeignKeys) - 'className' => NULL, - 'onDelete' => $this->value('onDelete', $foreignXML, FALSE), - ); - $foreignKeys[$name] = &$foreignKey; - } - - function getDynamicForeignKey(&$foreignXML, &$dynamicForeignKeys) { - $foreignKey = array( - 'idColumn' => trim($foreignXML->idColumn), - 'typeColumn' => trim($foreignXML->typeColumn), - 'key' => trim($this->value('key', $foreignXML)), - ); - $dynamicForeignKeys[] = $foreignKey; - } - - protected function value($key, &$object, $default = NULL) { - if (isset($object->$key)) { - return (string ) $object->$key; - } - return $default; - } - - protected function checkAndAppend(&$attributes, &$object, $name, $pre = NULL, $post = NULL) { - if (!isset($object->$name)) { - return; - } - - $value = $pre . trim($object->$name) . $post; - $this->append($attributes, ' ', trim($value)); - } - - protected function append(&$str, $delim, $name) { - if (empty($name)) { - return; - } - - if (is_array($name)) { - foreach ($name as $n) { - if (empty($n)) { - continue; - } - if (empty($str)) { - $str = $n; - } - else { - $str .= $delim . $n; - } - } - } - else { - if (empty($str)) { - $str = $name; - } - else { - $str .= $delim . $name; - } - } - } - - /** - * Sets the size property of a textfield - * See constants defined in CRM_Utils_Type for possible values - */ - protected function getSize($fieldXML) { - // Extract from tag if supplied - if ($this->value('size', $fieldXML)) { - $const = 'CRM_Utils_Type::' . strtoupper($fieldXML->size); - if (defined($const)) { - return $const; - } - } - // Infer from tag if was not explicitly set or was invalid - - // This map is slightly different from CRM_Core_Form_Renderer::$_sizeMapper - // Because we usually want fields to render as smaller than their maxlength - $sizes = array( - 2 => 'TWO', - 4 => 'FOUR', - 6 => 'SIX', - 8 => 'EIGHT', - 16 => 'TWELVE', - 32 => 'MEDIUM', - 64 => 'BIG', - ); - foreach ($sizes as $length => $name) { - if ($fieldXML->length <= $length) { - return "CRM_Utils_Type::$name"; - } - } - return 'CRM_Utils_Type::HUGE'; - } - - /** - * Clear the smarty cache and assign default values - */ - function reset_smarty_assignments() { - $this->smarty->clear_all_assign(); - $this->smarty->clear_all_cache(); - $this->smarty->assign('generated', "DO NOT EDIT. Generated by " . basename(__FILE__)); } } diff --git a/CRM/Core/CodeGen/Reflection.php b/CRM/Core/CodeGen/Reflection.php new file mode 100644 index 0000000000..1a70dafce1 --- /dev/null +++ b/CRM/Core/CodeGen/Reflection.php @@ -0,0 +1,17 @@ +generateListAll(); + } + + function generateListAll() { + $template = new CRM_Core_CodeGen_Util_Template('php'); + $template->assign('tables', $this->tables); + + $template->run('listAll.tpl', $this->config->CoreDAOCodePath . "AllCoreTables.php"); + } +} diff --git a/CRM/Core/CodeGen/Schema.php b/CRM/Core/CodeGen/Schema.php new file mode 100644 index 0000000000..51bdf6dc79 --- /dev/null +++ b/CRM/Core/CodeGen/Schema.php @@ -0,0 +1,121 @@ +locales = $this->findLocales(); + } + + function run() { + CRM_Core_CodeGen_Util_File::createDir($this->config->sqlCodePath); + + $this->generateCreateSql(); + $this->generateDropSql(); + + $this->generateLocaleDataSql(); + + // also create the archive tables + // $this->generateCreateSql('civicrm_archive.mysql' ); + // $this->generateDropSql('civicrm_archive_drop.mysql'); + + $this->generateNavigation(); + $this->generateSample(); + } + + function generateCreateSql($fileName = 'civicrm.mysql') { + echo "Generating sql file\n"; + $template = new CRM_Core_CodeGen_Util_Template('sql'); + + $template->assign('database', $this->config->database); + $template->assign('tables', $this->tables); + $dropOrder = array_reverse(array_keys($this->tables)); + $template->assign('dropOrder', $dropOrder); + $template->assign('mysql', 'modern'); + + $template->run('schema.tpl', $this->config->sqlCodePath . $fileName); + } + + function generateDropSql($fileName = 'civicrm_drop.mysql') { + echo "Generating sql drop tables file\n"; + $dropOrder = array_reverse(array_keys($this->tables)); + $template = new CRM_Core_CodeGen_Util_Template('sql'); + $template->assign('dropOrder', $dropOrder); + $template->run('drop.tpl', $this->config->sqlCodePath . $fileName); + } + + function generateNavigation() { + echo "Generating navigation file\n"; + $template = new CRM_Core_CodeGen_Util_Template('sql'); + $template->run('civicrm_navigation.tpl', $this->config->sqlCodePath . "civicrm_navigation.mysql"); + } + + function generateLocaleDataSql() { + $template = new CRM_Core_CodeGen_Util_Template('sql'); + + global $tsLocale; + $oldTsLocale = $tsLocale; + foreach ($this->locales as $locale) { + echo "Generating data files for $locale\n"; + $tsLocale = $locale; + $template->assign('locale', $locale); + $template->assign('db_version', $this->config->db_version); + + $sections = array( + 'civicrm_country.tpl', + 'civicrm_state_province.tpl', + 'civicrm_currency.tpl', + 'civicrm_data.tpl', + 'civicrm_navigation.tpl', + 'civicrm_version_sql.tpl', + ); + + $ext = ($locale != 'en_US' ? ".$locale" : ''); + // write the initialize base-data sql script + $template->runConcat($sections, $this->config->sqlCodePath . "civicrm_data$ext.mysql"); + + // write the acl sql script + $template->run('civicrm_acl.tpl', $this->config->sqlCodePath . "civicrm_acl$ext.mysql"); + } + $tsLocale = $oldTsLocale; + } + + function generateSample() { + $template = new CRM_Core_CodeGen_Util_Template('sql'); + $sections = array( + 'civicrm_sample.tpl', + 'civicrm_acl.tpl', + ); + $template->runConcat($sections, $this->config->sqlCodePath . 'civicrm_sample.mysql'); + } + + function findLocales() { + require_once 'CRM/Core/Config.php'; + $config = CRM_Core_Config::singleton(FALSE); + $locales = array(); + if (substr($config->gettextResourceDir, 0, 1) === '/') { + $localeDir = $config->gettextResourceDir; + } + else { + $localeDir = '../' . $config->gettextResourceDir; + } + if (file_exists($localeDir)) { + $config->gettextResourceDir = $localeDir; + $locales = preg_grep('/^[a-z][a-z]_[A-Z][A-Z]$/', scandir($localeDir)); + } + + $localesMask = getenv('CIVICRM_LOCALES'); + if (!empty($localesMask)) { + $mask = explode(',', $localesMask); + $locales = array_intersect($locales, $mask); + } + + if (!in_array('en_US', $locales)) { + array_unshift($locales, 'en_US'); + } + + return $locales; + } +} diff --git a/CRM/Core/CodeGen/Specification.php b/CRM/Core/CodeGen/Specification.php new file mode 100644 index 0000000000..4e667336ef --- /dev/null +++ b/CRM/Core/CodeGen/Specification.php @@ -0,0 +1,590 @@ +buildVersion = $buildVersion; + + echo "Parsing schema description ".$schemaPath."\n"; + $dbXML = CRM_Core_CodeGen_Util_Xml::parse($schemaPath); + // print_r( $dbXML ); + + echo "Extracting database information\n"; + $this->database = &$this->getDatabase($dbXML); + // print_r( $this->database ); + + $this->classNames = array(); + + # TODO: peel DAO-specific stuff out of getTables, and spec reading into its own class + echo "Extracting table information\n"; + $this->tables = $this->getTables($dbXML, $this->database); + + $this->resolveForeignKeys($this->tables, $this->classNames); + $this->tables = $this->orderTables($this->tables); + + // add archive tables here + $archiveTables = array( ); + foreach ($this->tables as $name => $table ) { + if ( $table['archive'] == 'true' ) { + $name = 'archive_' . $table['name']; + $table['name'] = $name; + $table['archive'] = 'false'; + if ( isset($table['foreignKey']) ) { + foreach ($table['foreignKey'] as $fkName => $fkValue) { + if ($this->tables[$fkValue['table']]['archive'] == 'true') { + $table['foreignKey'][$fkName]['table'] = 'archive_' . $table['foreignKey'][$fkName]['table']; + $table['foreignKey'][$fkName]['uniqName'] = + str_replace( 'FK_', 'FK_archive_', $table['foreignKey'][$fkName]['uniqName'] ); + } + } + $archiveTables[$name] = $table; + } + } + } + } + + function &getDatabase(&$dbXML) { + $database = array('name' => trim((string ) $dbXML->name)); + + $attributes = ''; + $this->checkAndAppend($attributes, $dbXML, 'character_set', 'DEFAULT CHARACTER SET ', ''); + $this->checkAndAppend($attributes, $dbXML, 'collate', 'COLLATE ', ''); + $database['attributes'] = $attributes; + + $tableAttributes_modern = $tableAttributes_simple = ''; + $this->checkAndAppend($tableAttributes_modern, $dbXML, 'table_type', 'ENGINE=', ''); + $this->checkAndAppend($tableAttributes_simple, $dbXML, 'table_type', 'TYPE=', ''); + $database['tableAttributes_modern'] = trim($tableAttributes_modern . ' ' . $attributes); + $database['tableAttributes_simple'] = trim($tableAttributes_simple); + + $database['comment'] = $this->value('comment', $dbXML, ''); + + return $database; + } + + function getTables($dbXML, &$database) { + $tables = array(); + foreach ($dbXML->tables as $tablesXML) { + foreach ($tablesXML->table as $tableXML) { + if ($this->value('drop', $tableXML, 0) > 0 and $this->value('drop', $tableXML, 0) <= $this->buildVersion) { + continue; + } + + if ($this->value('add', $tableXML, 0) <= $this->buildVersion) { + $this->getTable($tableXML, $database, $tables); + } + } + } + + return $tables; + } + + function resolveForeignKeys(&$tables, &$classNames) { + foreach (array_keys($tables) as $name) { + $this->resolveForeignKey($tables, $classNames, $name); + } + } + + function resolveForeignKey(&$tables, &$classNames, $name) { + if (!array_key_exists('foreignKey', $tables[$name])) { + return; + } + + foreach (array_keys($tables[$name]['foreignKey']) as $fkey) { + $ftable = $tables[$name]['foreignKey'][$fkey]['table']; + if (!array_key_exists($ftable, $classNames)) { + echo "$ftable is not a valid foreign key table in $name\n"; + continue; + } + $tables[$name]['foreignKey'][$fkey]['className'] = $classNames[$ftable]; + $tables[$name]['foreignKey'][$fkey]['fileName'] = str_replace('_', '/', $classNames[$ftable]) . '.php'; + $tables[$name]['fields'][$fkey]['FKClassName'] = $classNames[$ftable]; + } + } + + function orderTables(&$tables) { + $ordered = array(); + + while (!empty($tables)) { + foreach (array_keys($tables) as $name) { + if ($this->validTable($tables, $ordered, $name)) { + $ordered[$name] = $tables[$name]; + unset($tables[$name]); + } + } + } + return $ordered; + } + + function validTable(&$tables, &$valid, $name) { + if (!array_key_exists('foreignKey', $tables[$name])) { + return TRUE; + } + + foreach (array_keys($tables[$name]['foreignKey']) as $fkey) { + $ftable = $tables[$name]['foreignKey'][$fkey]['table']; + if (!array_key_exists($ftable, $valid) && $ftable !== $name) { + return FALSE; + } + } + return TRUE; + } + + function getTable($tableXML, &$database, &$tables) { + $name = trim((string ) $tableXML->name); + $klass = trim((string ) $tableXML->class); + $base = $this->value('base', $tableXML); + $sourceFile = "xml/schema/{$base}/{$klass}.xml"; + $daoPath = "{$base}/DAO/"; + $pre = str_replace('/', '_', $daoPath); + $this->classNames[$name] = $pre . $klass; + + $localizable = FALSE; + foreach ($tableXML->field as $fieldXML) { + if ($fieldXML->localizable) { + $localizable = TRUE; + break; + } + } + + $table = array( + 'name' => $name, + 'base' => $daoPath, + 'sourceFile' => $sourceFile, + 'fileName' => $klass . '.php', + 'objectName' => $klass, + 'labelName' => substr($name, 8), + 'className' => $this->classNames[$name], + 'attributes_simple' => trim($database['tableAttributes_simple']), + 'attributes_modern' => trim($database['tableAttributes_modern']), + 'comment' => $this->value('comment', $tableXML), + 'localizable' => $localizable, + 'log' => $this->value('log', $tableXML, 'false'), + 'archive' => $this->value('archive', $tableXML, 'false'), + ); + + $fields = array(); + foreach ($tableXML->field as $fieldXML) { + if ($this->value('drop', $fieldXML, 0) > 0 and $this->value('drop', $fieldXML, 0) <= $this->buildVersion) { + continue; + } + + if ($this->value('add', $fieldXML, 0) <= $this->buildVersion) { + $this->getField($fieldXML, $fields); + } + } + + $table['fields'] = &$fields; + $table['hasEnum'] = FALSE; + foreach ($table['fields'] as $field) { + if ($field['crmType'] == 'CRM_Utils_Type::T_ENUM') { + $table['hasEnum'] = TRUE; + break; + } + } + + if ($this->value('primaryKey', $tableXML)) { + $this->getPrimaryKey($tableXML->primaryKey, $fields, $table); + } + + // some kind of refresh? + CRM_Core_Config::singleton(FALSE); + if ($this->value('index', $tableXML)) { + $index = array(); + foreach ($tableXML->index as $indexXML) { + if ($this->value('drop', $indexXML, 0) > 0 and $this->value('drop', $indexXML, 0) <= $this->buildVersion) { + continue; + } + + $this->getIndex($indexXML, $fields, $index); + } + $table['index'] = &$index; + } + + if ($this->value('foreignKey', $tableXML)) { + $foreign = array(); + foreach ($tableXML->foreignKey as $foreignXML) { + // print_r($foreignXML); + + if ($this->value('drop', $foreignXML, 0) > 0 and $this->value('drop', $foreignXML, 0) <= $this->buildVersion) { + continue; + } + if ($this->value('add', $foreignXML, 0) <= $this->buildVersion) { + $this->getForeignKey($foreignXML, $fields, $foreign, $name); + } + } + $table['foreignKey'] = &$foreign; + } + + if ($this->value('dynamicForeignKey', $tableXML)) { + $dynamicForeign = array(); + foreach ($tableXML->dynamicForeignKey as $foreignXML) { + if ($this->value('drop', $foreignXML, 0) > 0 and $this->value('drop', $foreignXML, 0) <= $this->buildVersion) { + continue; + } + if ($this->value('add', $foreignXML, 0) <= $this->buildVersion) { + $this->getDynamicForeignKey($foreignXML, $dynamicForeign, $name); + } + } + $table['dynamicForeignKey'] = $dynamicForeign; + } + + $tables[$name] = &$table; + return; + } + + function getField(&$fieldXML, &$fields) { + $name = trim((string ) $fieldXML->name); + $field = array('name' => $name, 'localizable' => $fieldXML->localizable); + $type = (string ) $fieldXML->type; + switch ($type) { + case 'varchar': + case 'char': + $field['length'] = (int) $fieldXML->length; + $field['sqlType'] = "$type({$field['length']})"; + $field['phpType'] = 'string'; + $field['crmType'] = 'CRM_Utils_Type::T_STRING'; + $field['size'] = $this->getSize($fieldXML); + break; + + case 'enum': + $value = (string ) $fieldXML->values; + $field['sqlType'] = 'enum('; + $field['values'] = array(); + $field['enumValues'] = $value; + $values = explode(',', $value); + $first = TRUE; + foreach ($values as $v) { + $v = trim($v); + $field['values'][] = $v; + + if (!$first) { + $field['sqlType'] .= ', '; + } + $first = FALSE; + $field['sqlType'] .= "'$v'"; + } + $field['sqlType'] .= ')'; + $field['phpType'] = $field['sqlType']; + $field['crmType'] = 'CRM_Utils_Type::T_ENUM'; + break; + + case 'text': + $field['sqlType'] = $field['phpType'] = $type; + $field['crmType'] = 'CRM_Utils_Type::T_' . strtoupper($type); + $field['rows'] = $this->value('rows', $fieldXML); + $field['cols'] = $this->value('cols', $fieldXML); + break; + + case 'datetime': + $field['sqlType'] = $field['phpType'] = $type; + $field['crmType'] = 'CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME'; + break; + + case 'boolean': + // need this case since some versions of mysql do not have boolean as a valid column type and hence it + // is changed to tinyint. hopefully after 2 yrs this case can be removed. + $field['sqlType'] = 'tinyint'; + $field['phpType'] = $type; + $field['crmType'] = 'CRM_Utils_Type::T_' . strtoupper($type); + break; + + case 'decimal': + $length = $fieldXML->length ? $fieldXML->length : '20,2'; + $field['sqlType'] = 'decimal(' . $length . ')'; + $field['phpType'] = 'float'; + $field['crmType'] = 'CRM_Utils_Type::T_MONEY'; + break; + + case 'float': + $field['sqlType'] = 'double'; + $field['phpType'] = 'float'; + $field['crmType'] = 'CRM_Utils_Type::T_FLOAT'; + break; + + default: + $field['sqlType'] = $field['phpType'] = $type; + if ($type == 'int unsigned') { + $field['crmType'] = 'CRM_Utils_Type::T_INT'; + } + else { + $field['crmType'] = 'CRM_Utils_Type::T_' . strtoupper($type); + } + break; + } + + $field['required'] = $this->value('required', $fieldXML); + $field['collate'] = $this->value('collate', $fieldXML); + $field['comment'] = $this->value('comment', $fieldXML); + $field['default'] = $this->value('default', $fieldXML); + $field['import'] = $this->value('import', $fieldXML); + if ($this->value('export', $fieldXML)) { + $field['export'] = $this->value('export', $fieldXML); + } + else { + $field['export'] = $this->value('import', $fieldXML); + } + $field['rule'] = $this->value('rule', $fieldXML); + $field['title'] = $this->value('title', $fieldXML); + if (!$field['title']) { + $field['title'] = $this->composeTitle($name); + } + $field['headerPattern'] = $this->value('headerPattern', $fieldXML); + $field['dataPattern'] = $this->value('dataPattern', $fieldXML); + $field['uniqueName'] = $this->value('uniqueName', $fieldXML); + $field['pseudoconstant'] = $this->value('pseudoconstant', $fieldXML); + if(!empty($field['pseudoconstant'])){ + //ok this is a bit long-winded but it gets there & is consistent with above approach + $field['pseudoconstant'] = array(); + $validOptions = array( + // Fields can specify EITHER optionGroupName OR table, not both + // (since declaring optionGroupName means we are using the civicrm_option_value table) + 'optionGroupName', + 'table', + // If table is specified, keyColumn and labelColumn are also required + 'keyColumn', + 'labelColumn', + // Non-translated machine name for programmatic lookup. Defaults to 'name' if that column exists + 'nameColumn', + // Where clause snippet (will be joined to the rest of the query with AND operator) + 'condition', + ); + foreach ($validOptions as $pseudoOption) { + if(!empty($fieldXML->pseudoconstant->$pseudoOption)){ + $field['pseudoconstant'][$pseudoOption] = $this->value($pseudoOption, $fieldXML->pseudoconstant); + } + } + // For now, fields that have option lists that are not in the db can simply + // declare an empty pseudoconstant tag and we'll add this placeholder. + // That field's BAO::buildOptions fn will need to be responsible for generating the option list + if (empty($field['pseudoconstant'])) { + $field['pseudoconstant'] = 'not in database'; + } + } + $fields[$name] = &$field; + } + + function composeTitle($name) { + $names = explode('_', strtolower($name)); + $title = ''; + for ($i = 0; $i < count($names); $i++) { + if ($names[$i] === 'id' || $names[$i] === 'is') { + // id's do not get titles + return NULL; + } + + if ($names[$i] === 'im') { + $names[$i] = 'IM'; + } + else { + $names[$i] = ucfirst(trim($names[$i])); + } + + $title = $title . ' ' . $names[$i]; + } + return trim($title); + } + + function getPrimaryKey(&$primaryXML, &$fields, &$table) { + $name = trim((string ) $primaryXML->name); + + /** need to make sure there is a field of type name */ + if (!array_key_exists($name, $fields)) { + echo "primary key $name in $table->name does not have a field definition, ignoring\n"; + return; + } + + // set the autoincrement property of the field + $auto = $this->value('autoincrement', $primaryXML); + $fields[$name]['autoincrement'] = $auto; + $primaryKey = array( + 'name' => $name, + 'autoincrement' => $auto, + ); + $table['primaryKey'] = &$primaryKey; + } + + function getIndex(&$indexXML, &$fields, &$indices) { + //echo "\n\n*******************************************************\n"; + //echo "entering getIndex\n"; + + $index = array(); + // empty index name is fine + $indexName = trim((string)$indexXML->name); + $index['name'] = $indexName; + $index['field'] = array(); + + // populate fields + foreach ($indexXML->fieldName as $v) { + $fieldName = (string)($v); + $length = (string)($v['length']); + if (strlen($length) > 0) { + $fieldName = "$fieldName($length)"; + } + $index['field'][] = $fieldName; + } + + $index['localizable'] = FALSE; + foreach ($index['field'] as $fieldName) { + if (isset($fields[$fieldName]) and $fields[$fieldName]['localizable']) { + $index['localizable'] = TRUE; + break; + } + } + + // check for unique index + if ($this->value('unique', $indexXML)) { + $index['unique'] = TRUE; + } + + //echo "\$index = \n"; + //print_r($index); + + // field array cannot be empty + if (empty($index['field'])) { + echo "No fields defined for index $indexName\n"; + return; + } + + // all fieldnames have to be defined and should exist in schema. + foreach ($index['field'] as $fieldName) { + if (!$fieldName) { + echo "Invalid field defination for index $indexName\n"; + return; + } + $parenOffset = strpos($fieldName, '('); + if ($parenOffset > 0) { + $fieldName = substr($fieldName, 0, $parenOffset); + } + if (!array_key_exists($fieldName, $fields)) { + echo "Table does not contain $fieldName\n"; + print_r($fields); + exit(); + } + } + $indices[$indexName] = &$index; + } + + function getForeignKey(&$foreignXML, &$fields, &$foreignKeys, &$currentTableName) { + $name = trim((string ) $foreignXML->name); + + /** need to make sure there is a field of type name */ + if (!array_key_exists($name, $fields)) { + echo "foreign $name in $currentTableName does not have a field definition, ignoring\n"; + return; + } + + /** need to check for existence of table and key **/ + $table = trim($this->value('table', $foreignXML)); + $foreignKey = array( + 'name' => $name, + 'table' => $table, + 'uniqName' => "FK_{$currentTableName}_{$name}", + 'key' => trim($this->value('key', $foreignXML)), + 'import' => $this->value('import', $foreignXML, FALSE), + 'export' => $this->value('import', $foreignXML, FALSE), + // we do this matching in a seperate phase (resolveForeignKeys) + 'className' => NULL, + 'onDelete' => $this->value('onDelete', $foreignXML, FALSE), + ); + $foreignKeys[$name] = &$foreignKey; + } + + function getDynamicForeignKey(&$foreignXML, &$dynamicForeignKeys) { + $foreignKey = array( + 'idColumn' => trim($foreignXML->idColumn), + 'typeColumn' => trim($foreignXML->typeColumn), + 'key' => trim($this->value('key', $foreignXML)), + ); + $dynamicForeignKeys[] = $foreignKey; + } + + protected function value($key, &$object, $default = NULL) { + if (isset($object->$key)) { + return (string ) $object->$key; + } + return $default; + } + + protected function checkAndAppend(&$attributes, &$object, $name, $pre = NULL, $post = NULL) { + if (!isset($object->$name)) { + return; + } + + $value = $pre . trim($object->$name) . $post; + $this->append($attributes, ' ', trim($value)); + } + + protected function append(&$str, $delim, $name) { + if (empty($name)) { + return; + } + + if (is_array($name)) { + foreach ($name as $n) { + if (empty($n)) { + continue; + } + if (empty($str)) { + $str = $n; + } + else { + $str .= $delim . $n; + } + } + } + else { + if (empty($str)) { + $str = $name; + } + else { + $str .= $delim . $name; + } + } + } + + /** + * Sets the size property of a textfield + * See constants defined in CRM_Utils_Type for possible values + */ + protected function getSize($fieldXML) { + // Extract from tag if supplied + if ($this->value('size', $fieldXML)) { + $const = 'CRM_Utils_Type::' . strtoupper($fieldXML->size); + if (defined($const)) { + return $const; + } + } + // Infer from tag if was not explicitly set or was invalid + + // This map is slightly different from CRM_Core_Form_Renderer::$_sizeMapper + // Because we usually want fields to render as smaller than their maxlength + $sizes = array( + 2 => 'TWO', + 4 => 'FOUR', + 6 => 'SIX', + 8 => 'EIGHT', + 16 => 'TWELVE', + 32 => 'MEDIUM', + 64 => 'BIG', + ); + foreach ($sizes as $length => $name) { + if ($fieldXML->length <= $length) { + return "CRM_Utils_Type::$name"; + } + } + return 'CRM_Utils_Type::HUGE'; + } +} diff --git a/CRM/Core/CodeGen/Test.php b/CRM/Core/CodeGen/Test.php new file mode 100644 index 0000000000..3754ae93b2 --- /dev/null +++ b/CRM/Core/CodeGen/Test.php @@ -0,0 +1,27 @@ +generateCiviTestTruncate(); + } + + function generateCiviTestTruncate() { + echo "Generating tests truncate file\n"; + + # TODO template + $truncate = ' + + '; + $tbls = array_keys($this->tables); + foreach ($tbls as $d => $t) { + $truncate = $truncate . "\n <$t />\n"; + } + + $truncate = $truncate . "\n"; + file_put_contents($this->config->sqlCodePath . "../tests/phpunit/CiviTest/truncate.xml", $truncate); + unset($truncate); + } +} diff --git a/CRM/Core/CodeGen/Util/Template.php b/CRM/Core/CodeGen/Util/Template.php new file mode 100644 index 0000000000..6f36544421 --- /dev/null +++ b/CRM/Core/CodeGen/Util/Template.php @@ -0,0 +1,92 @@ +compileDir = CRM_Core_CodeGen_Util_File::createTempDir('templates_c_'); + + // TODO use Core Smarty + require_once 'Smarty/Smarty.class.php'; + $this->smarty = new Smarty(); + $this->smarty->template_dir = './templates'; + $this->smarty->plugins_dir = self::$smartyPluginDirs; + $this->smarty->compile_dir = $this->compileDir; + $this->smarty->clear_all_cache(); + + $this->assign('generated', "DO NOT EDIT. Generated by CRM_Core_CodeGen"); + + // CRM-5308 / CRM-3507 - we need {localize} to work in the templates + require_once 'CRM/Core/Smarty/plugins/block.localize.php'; + $this->smarty->register_block('localize', 'smarty_block_localize'); + + if ($this->filetype === 'php') { + require_once 'PHP/Beautifier.php'; + // create an instance + $this->beautifier = new PHP_Beautifier(); + $this->beautifier->addFilter('ArrayNested'); + // add one or more filters + $this->beautifier->addFilter('Pear'); + // add one or more filters + $this->beautifier->addFilter('NewLines', array('after' => 'class, public, require, comment')); + $this->beautifier->setIndentChar(' '); + $this->beautifier->setIndentNumber(2); + $this->beautifier->setNewLine("\n"); + } + } + + function __destruct() { + CRM_Core_CodeGen_Util_File::removeDir($this->compileDir); + } + + /** + * @param array $inputs template filenames + * @param string $outpath full path to the desired output file + */ + function runConcat($inputs, $outpath) { + unlink($outpath); + foreach ($inputs as $infile) { + // FIXME: does not beautify. Document. + file_put_contents($outpath, $this->smarty->fetch($infile), FILE_APPEND); + } + } + + /** + * @param string $infile filename of the template, without a path + * @param string $outpath full path to the desired output file + */ + function run($infile, $outpath) { + $renderedContents = $this->smarty->fetch($infile); + + if ($this->filetype === 'php') { + $this->beautifier->setInputString($renderedContents); + $this->beautifier->setOutputFile($outpath); + $this->beautifier->process(); + $this->beautifier->save(); + } else { + file_put_contents($outpath, $renderedContents); + } + } + + function assign($key, $value) { + $this->smarty->assign_by_ref($key, $value); + } + + /** + * Clear the smarty cache and assign default values + * FIXME: unused cos we no longer do evil singleton magick + */ + protected function reset() { + $this->smarty->clear_all_assign(); + $this->smarty->clear_all_cache(); + } +} diff --git a/CRM/Core/CodeGen/Util/Xml.php b/CRM/Core/CodeGen/Util/Xml.php new file mode 100644 index 0000000000..4b6219af07 --- /dev/null +++ b/CRM/Core/CodeGen/Util/Xml.php @@ -0,0 +1,16 @@ +load($file); + $dom->xinclude(); + $xml = simplexml_import_dom($dom); + return $xml; + } +} diff --git a/tests/phpunit/CiviTest/.gitignore b/tests/phpunit/CiviTest/.gitignore new file mode 100644 index 0000000000..49a7420804 --- /dev/null +++ b/tests/phpunit/CiviTest/.gitignore @@ -0,0 +1 @@ +truncate.xml diff --git a/xml/templates/civicrm_version_sql.tpl b/xml/templates/civicrm_version_sql.tpl new file mode 100644 index 0000000000..3a199a484c --- /dev/null +++ b/xml/templates/civicrm_version_sql.tpl @@ -0,0 +1 @@ +UPDATE civicrm_domain SET version = '{$db_version}'; -- 2.25.1