PROBLEM No structure info of the table that gives enough data to handle the interface table. SOLUTION Table structure was added in order to provide info requiered.
493 lines
17 KiB
PHP
493 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* Class Service_Rest_RestTool
|
|
*
|
|
* This tool generates a rest-config.ini file and build rest crud api for 'Restler' lib.
|
|
* Class since: Aug 22, 2012
|
|
*
|
|
* @author Erik Amaru Ortiz <aortiz.erik@gmail.com>
|
|
*/
|
|
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 = <<<EOT
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
; -= ProcessMaker RestFul services configuration =- ;
|
|
; ;
|
|
; On this configuration file you can customize the Processmaker Rest Service. ;
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
; Rest Service Configuration ;
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
enable_service = true
|
|
|
|
; add headers to rest server responses
|
|
[HEADERS]
|
|
; Enable this header to allow "Cross Domain AJAX" requests;
|
|
; This works because processmaker is handling correctly requests with method 'OPTIONS'
|
|
; that automatically is sent by a client using XmlHttpRequest or similar.
|
|
;Access-Control-Allow-Origin = *
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
; DB Tables Crud generation Config ;
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;
|
|
; This configuration section is used by ./rest-gen command to build the Rest Crud Api
|
|
;
|
|
; Configure what methods GET, POST, PUT, DELETE should be exposed.
|
|
; Configure for each table/method what columns sould be exposed.
|
|
;
|
|
; Configuration for each table must starts with a section like:
|
|
; [TABLE:<table-name>]
|
|
;
|
|
; 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
|