493 lines
17 KiB
PHP
Executable File
493 lines
17 KiB
PHP
Executable File
<?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);
|
|
}
|
|
}
|
|
}
|
|
|
|
|