Files
luos/workflow/engine/lib/Service/Rest/RestTool.php
2012-11-30 10:47:17 -04:00

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);
}
}
}