*/ class Service_Rest_RestTool { /** * Stores absolute file path of rest configuration ini file (rest-config.ini) * @var string */ protected $configFile = ''; /** * Stores configuration loaded from configuration ini file * @var array */ protected $config = array(); /** * Stores absolute filename patgh of database xml schema * @var string */ protected $dbXmlSchemaFile = ''; /** * Stores information of pmos tables * @var array */ protected $dbInfo = array(); /** * Stores obsolute base path to output generated files * @var string */ protected $basePath = ''; /** * Init method to initialize the class after previous configurations */ public function init() { self::out('ProcessMaker Rest Crud Api Generator Tool v1.0', 'success', true); echo PHP_EOL; // configuring paths by default if there were not configured before. if (empty($this->dbXmlSchemaFile)) { $this->dbXmlSchemaFile = $this->basePath . 'config/schema.xml'; } if (empty($this->configFile)) { $this->configFile = $this->basePath . 'config/rest-config.ini'; } } public function setConfigFile($configFile) { $this->configFile = $configFile; } public function setDbXmlSchemaFile($dbXmlSchemaFile) { $this->dbXmlSchemaFile = $dbXmlSchemaFile; } public function setBasePath($basePath) { $this->basePath = $basePath; } /** * Load rest ini. configuration */ protected function loadConfig() { if (! file_exists($this->configFile)) { throw new Exception(sprintf("Runtime Error: Configuration file '%s' doesn't exist!", $this->configFile)); } self::out('Loading config from: ', 'info', false); echo $this->configFile . "\n"; $config = @parse_ini_file($this->configFile, true); // parse composed sections names like [TABLE:SOME_TABLE] foreach ($config as $key => $value) { $sectionParts = explode(':', $key); if (count($sectionParts) == 2 && strtolower($sectionParts[0]) == 'table') { $this->config['_tables'][$sectionParts[1]] = $value; } else { $this->config[$key] = $value; } } } /** * Load db schema from xml file */ protected function loadDbXmlSchema() { if (! file_exists($this->dbXmlSchemaFile)) { throw new Exception(sprintf( "Runtime Error: Configuration file '%s' doesn't exist!", $this->dbXmlSchemaFile )); } print "\n"; self::out('Loading Xml Schema from: ', 'info', false); print $this->dbXmlSchemaFile . "\n"; $doc = new Xml_DOMDocumentExtended(); $doc->load($this->dbXmlSchemaFile); // dump to array $data = $doc->toArray(); $tables = $data['database']['table']; // process just relevant information foreach ($tables as $table) { $this->dbInfo[$table['@name']]['pKeys'] = array(); $this->dbInfo[$table['@name']]['columns'] = array(); $this->dbInfo[$table['@name']]['required_columns'] = array(); //Adding data types $this->dbInfo[$table['@name']]['type']['name'] = array(); $this->dbInfo[$table['@name']]['type']['Length'] = array(); foreach ($table['column'] as $column) { $this->dbInfo[$table['@name']]['columns'][] = $column['@name']; $this->dbInfo[$table['@name']]['type']['name'][] = $column['@type']; //adding size to typeLength if exists if (array_key_exists('@size', $column) && self::cast($column['@size'])) { $this->dbInfo[$table['@name']]['type']['Length'][] = $column['@size']; } else{ $this->dbInfo[$table['@name']]['type']['Length'][] = '0'; } //adding name to pkeys if exists primary key exists if (array_key_exists('@primaryKey', $column) && self::cast($column['@primaryKey'])) { $this->dbInfo[$table['@name']]['pKeys'][] = $column['@name']; } //adding name to requiered_columns if required field exists if (array_key_exists('@required', $column) && self::cast($column['@required'])) { $this->dbInfo[$table['@name']]['required_columns'][] = $column['@name']; } } } } /** * Build Rest configuration file * @param string $filename configutaion filename to be generated */ public function buildConfigIni($filename = '') { $this->loadDbXmlSchema(); $configFile = empty($filename) ? $this->configFile : $filename; $configIniStr = <<] ; ; inside of each section there are two config keys: ; ; "ALLOW_METHODS" -> Use this param to set allowed methods (separeted by a single space). ; Complete example: ; ALLOW_METHODS = GET POST PUT DELETE ; ; The others params are: "EXPOSE_COLUMNS_GET", "EXPOSE_COLUMNS_POST" and "EXPOSE_COLUMNS_PUT" ; this params are used to configure the columns that should be exposed; ; wildcard '*' can be used to speccify all columns. ; ; Example: ; ;[TABLE:MY_TABLE] ; ALLOW_METHODS = GET POST PUT DELETE ; EXPOSE_COLUMNS_GET = * ; EXPOSE_COLUMNS_POST = FIELD1 FIELD2 FIELD3 FIELD4 ; EXPOSE_COLUMNS_PUT = FIELD1 FIELD2 FIELD3 ; EOT; $configIniStr .= "\n\n"; foreach ($this->dbInfo as $table => $columns) { $strColumns = implode(' ', $columns['columns']); $configIniStr .= ";Table '$table' with ".count($columns['columns'])." columns.\n"; $configIniStr .= "[TABLE:$table]\n"; $configIniStr .= " ALLOW_METHODS = GET\n"; $configIniStr .= " EXPOSE_COLUMNS_GET = *\n"; $configIniStr .= " EXPOSE_COLUMNS_POST = ".$strColumns."\n"; $configIniStr .= " EXPOSE_COLUMNS_PUT = ".$strColumns."\n"; $configIniStr .= "\n"; } file_put_contents($configFile, $configIniStr); return $configFile; } /** * Build Rest Crud Api */ public function buildCrudApi() { /** * load configuration from /engine/config/rest-config.ini and * load database schemda from /engine/config/schema.xml */ $this->loadConfig(); $this->loadDbXmlSchema(); self::out('Output folder: ', 'info', false); echo $this->basePath . "services/rest/crud"; if (! is_dir($this->basePath . "services/rest/crud/")) { G::mk_dir($this->basePath . "services/rest/crud/"); echo ' (created)'; } echo "\n\n"; if (! is_writable($this->basePath . "services/rest/crud/")) { throw new Exception(fprintf( "Runtime Error: Output folder '%s' is not writable.", $this->basePath . "services/rest/crud/" )); } Haanga::configure(array( 'template_dir' => dirname(__FILE__) . '/templates/', 'cache_dir' => sys_get_temp_dir() . '/haanga_cache/', 'compiler' => array( 'compiler' => array( /* opts for the tpl compiler */ 'if_empty' => false, 'autoescape' => true, 'strip_whitespace' => true, 'allow_exec' => true ), ) )); //new feature adding columns types as commentary. $infoExtra = array(); foreach ($this->dbInfo as $tablename => $columns){ $maxArray = count($columns['columns']); for($ptr = 0; $ptr < $maxArray; $ptr++){ $columnName = $columns['columns'][$ptr]; $type = $columns['type']; $typeName = $type['name'][$ptr]; $typelength = $type['Length'][$ptr]; $infoExtra[$tablename][] = "Column: " . $columnName . " of type ". $typeName . (($typelength != '0')?("[" . $typelength . "]"):""); } } $c = 0; //foreach ($this->config['_tables'] as $table => $conf) { foreach ($this->config['_tables'] as $table => $conf) { $classname = self::camelize($table, 'class'); $allowedMethods = explode(' ', $conf['ALLOW_METHODS']); $methods = ''; // Getting data for every method. foreach ($allowedMethods as $method) { // validation for a valid method if (! in_array($method, array('GET', 'POST', 'PUT', 'DELETE'))) { throw new Exception("Invalid method '$method'."); } $method = strtoupper($method); $exposedColumns = array(); $params = array(); $paramsStr = array(); $primaryKeys = array(); // get columns to expose if (array_key_exists('EXPOSE_COLUMNS_'.$method, $conf)) { if ($conf['EXPOSE_COLUMNS_'.$method] == '*') { $exposedColumns = $this->dbInfo[$table]['columns']; } else { $exposedColumns = explode(' ', $conf['EXPOSE_COLUMNS_'.$method]); // validation for valid columns $tableColumns = $this->dbInfo[$table]['columns']; foreach ($exposedColumns as $column) { if (! in_array($column, $tableColumns)) { throw new Exception("Invalid column '$column' for table $table, it does not exist!."); } } // validate that all required columns are in exposed columns array if ($method == 'POST') { // intersect required columns with exposed columns // to verify is all required columns are exposed $intersect = array_intersect($this->dbInfo[$table]['required_columns'], $exposedColumns); // the diff should be empty $diff = array_diff($this->dbInfo[$table]['required_columns'], $intersect); if (! empty($diff)) { throw new Exception(sprintf( "Error: All required columns for table '%s' must be exposed for POST method.\n" . "PLease add all required columns on rule 'EXPOSE_COLUMNS_POST' or select all " . "with '*' selector.\n\n" . "Missing (%s) required fields for [%s] table:\n" . implode("\n", $diff), $table, count($diff), $table )); } } } } switch ($method) { case 'GET': foreach ($this->dbInfo[$table]['pKeys'] as $i => $pk) { $paramsStr[$i] = "\$".self::camelize($pk).'=null'; $params[$i] = self::camelize($pk); } break; case 'PUT': foreach ($exposedColumns as $i => $column) { $paramsStr[$i] = "\$".self::camelize($column); if (! in_array($column, $this->dbInfo[$table]['pKeys'])) { $params[$i] = self::camelize($column); } } break; case 'POST': foreach ($exposedColumns as $i => $column) { $paramsStr[$i] = "\$".self::camelize($column); $params[$i] = self::camelize($column); } break; } $paramsStr = implode(', ', $paramsStr); // formatting primary keys for template foreach ($this->dbInfo[$table]['pKeys'] as $i => $pk) { $primaryKeys[$i] = "\$".self::camelize($pk); } $primaryKeys = implode(', ', $primaryKeys); $methods .= Haanga::Load( 'method'.self::camelize($method, 'class').'.tpl', array( 'params' => $params, 'paramsStr' => $paramsStr, 'primaryKeys' => $primaryKeys, 'columns' => $exposedColumns, 'classname' => $classname, ), true ); $methods .= "\n"; } $classContent = Haanga::Load('class.tpl', array( 'classname' => $classname, 'type' => $infoExtra[$table], 'tablename' => $table, 'methods' => $methods ), true); //echo "File #$c - $classname.php saved!\n"; ++$c; file_put_contents($this->basePath . "services/rest/crud/$classname.php", $classContent); } printf("Done, generated %s Rest Class Files.\n\n", self::out("($c)", 'success', false, true)); } /** * Camilize a string * Example: some_underscored_string to SomeUnderscoredString * * @param string $str string to camelze * @param string $type if the type is 'var' do not capitalize the first character. * @return string camelized string */ protected static function camelize($str, $type = 'var') { if (is_array($str)) { foreach ($str as $i => $value) { $str[$i] = self::camelize($value); } } elseif (is_string($str)) { $str = str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($str)))); } if ($type == 'var') { $str = substr(strtolower($str), 0, 1) . substr($str, 1); } return $str; } /** * Try convert a string to its native variable type * Example: * for a string 'true' => true * for a string 'false' => false * for a string '1.0' => 1.0 * * @param string $value string to try cast to its native variable type * @return mixed value converted to its native type, if wasn't possible the same string will be returned */ protected static function cast($value) { if ($value === 'true') { return true; } elseif ($value === 'false') { return false; } elseif (is_numeric($value)) { return $value * 1; } return $value; } /** * colorize output */ static public function out($text, $color = null, $newLine = true, $ret = false) { if (DIRECTORY_SEPARATOR == '\\') { $hasColorSupport = false !== getenv('ANSICON'); } else { $hasColorSupport = true; } $styles = array( 'success' => "\033[0;32m%s\033[0m", 'error' => "\033[31;31m%s\033[0m", 'info' => "\033[33;33m%s\033[0m" ); $format = '%s'; if (isset($styles[$color]) && $hasColorSupport) { $format = $styles[$color]; } if ($newLine) { $format .= PHP_EOL; } if ($ret) { return sprintf($format, $text); } else { printf($format, $text); } } }