* appendCustomCode( "function fibonacci( $number )" ); * $generator->appendCustomCode( "{" ); * * $generator->appendVariable( "lo", 1 ); * $generator->appendVariable( "hi", 1 ); * $generator->appendVariable( "i", 2 ); * * generator->appendWhile( "$2 < $number" ); * $generator->appendCustomCode( '$hi = $lo + $hi;' ); * $generator->appendCustomCode( '$lo = $hi - $lo;' ); * $generator->appendEndWhile(); * $generator->appendCustomCode( "}" ); * ?> * *The above code will fill the file "~/file.php" with the following contents: * * * * * property indentLevel int The level of indentation. Increase or decrease by one * if you want the indentation level to change. * property lineBreak string The characters to use for linebreaks. Defaults to "\r\n". * property niceIndentation boolean Whether to output the PHP nicely indented or not. * the default is false. * property indentString string The characters that are indented per indentation level. * the default is ' '. * * @package PhpGenerator * @version 1.0beta1 */ class ezcPhpGenerator { // assignment types const ASSIGN_NORMAL = 1; // = const ASSIGN_APPEND_TEXT = 2; // .= const ASSIGN_ADD = 3; // += const ASSIGN_SUBTRACT = 4; // -= const ASSIGN_ARRAY_APPEND = 5; // [] = // method control structures const FLOW_IF = 'if'; const FLOW_FOREACH = 'foreach'; const FLOW_FOR = 'for'; const FLOW_DO = 'do'; const FLOW_WHILE = 'while'; // the file resource of tmpFilename. This is used to write to during execution. private $fileResource; private $tmpFilename; // the name of the final result file. We use cp to move tmpFilename to resultFilename private $resultFilename; // wheter to include < ?php and ? > to the file. This is useful if you plan to use eval() on the result. private $includeStartEndTags; // Stack of assignment types. Used to control if append control and appendEnd control are called // in the right order. private $flowStack = array(); private $properties = array(); /** * Constructs a new PhpGenerator which that writes to the file $fileName. If $includeStartEndTags * is set to true the start and end php tags will be included. It is useful to ommit these if you * want to run the generated code using eval() later. $niceIndentation controls if the PHP output * should be indented correctly. This option is useful if you want to debug the generated code. * * @param string $fileName * @param boolean $includeStartEndTags * @param boolean niceIndentation * @throws PhpGeneratorException If the PhpGenerator can't open a temporary file for writing. */ public function __construct( $filename, $includeStartEndTags = true, $niceIndentation = false ) { // properties defaults $this->indentLevel = 0; $this->lineBreak = "\r\n"; $this->niceIndentation = $niceIndentation; $this->indentString = ' '; // other initialization $this->resultFilename = $filename; $this->includeStartEndTags = $includeStartEndTags; //// setup file write resource $dir = dirname( $filename ); $file = basename( $filename ); // generate a temporary name $id = md5( uniqid( "ezp". getmypid(), true ) ); $this->tmpFilename = $filename . $id; // open the file, and make it ready for writing $this->fileResource = fopen( $this->tmpFilename, 'w' ); if ( $this->fileResource == false ) { throw new ezcPhpGeneratorException( 'ezcPhpGenerator could not open the file $fileName for writing.', ezcPhpGeneratorException::FILE_OPEN_FAILED ); } if ( $this->includeStartEndTags ) { $this->write( 'lineBreak ); } } /** * Property set */ public function __set( $name, $value ) { switch ( $name ) { case 'lineBreak': $this->properties['lineBreak'] = $value; break; case 'indentLevel': $this->properties['indentLevel'] = $value; break; case 'indentString': $this->properties['indentString'] = $value; break; case 'niceIndentation': $this->properties['niceIndentation'] = $value; break; default: throw new ezcBasePropertyNotFoundException( $name ); break; } } /** * Property get */ public function __get( $name ) { switch ( $name ) { case 'lineBreak': return $this->properties['lineBreak']; break; case 'indentLevel': return $this->properties['indentLevel']; break; case 'indentString': return $this->properties['indentString']; break; case 'niceIndentation': return $this->properties['niceIndentation']; break; default: throw new ezcBasePropertyNotFoundException( $name ); break; } } /** * Destructs the object. * Makes sure any temporary files are removed. * * @returns void */ public function __destruct() { $this->abort(); } /** * Sets end of line characters that are used when generating code. * The default is "\r\n". * * @param string $characters * @returns void */ public function setLineBreak( $characters ) { $this->lineBreak = $characters; } /** * Returns the end of line characters that are used when generating code. * * @return string */ public function lineBreak( ) { return $this->lineBreak; } /** * Completes the code generation, moves the temporary writing file to the end result file * and releases used resources. * * This method must be called when you have finished generating a file. Subsequent calls to * any methods generating code will fail. * * @throws PhpGeneratorException if it was not possible to write to the output file or if * there are any control structures (if/foreach etc.) still open. * @returns void */ public function finish() { if ( count( $this->flowStack ) != 0 ) { throw new ezcPhpGeneratorException( 'finish() called while still inside a flow control structure.', ezcPhpGeneratorException::NESTING_ERROR ); } if ( $this->fileResource ) { if ( $this->includeStartEndTags ) { $this->write( '?>' ); } fclose( $this->fileResource ); $this->fileResource = null; if ( rename( $this->tmpFilename, $this->resultFilename ) === false ) { throw new ezcPhpGeneratorException( "ezcPhpGenerator could not open the file <{$fileName}> for writing.", ezcPhpGeneratorException::FILE_RENAME_FAILED ); } } } /** * Aborts the PHP genarating. Cleans up the file handler and the temporary file. * Subsequent calls to any methods generating code will fail. * * @returns void */ public function abort() { if ( file_exists( $this->tmpFilename ) ) { unlink( $this->tmpFilename ); $this->tmpFilename = ''; } } /** * Defines the variable $name with the value $value. * * The parameter $caseSensitive determines if the defined variable is case sensitive or not. * Note that $name must start with a letter or underscore, followed * by any number of letters, numbers, or underscores. * @see http://php.net/manual/en/language.constants.php for more information. * @see http://php.net/manual/en/function.define.php * * Example: * * $php->addDefine( 'MY_CONSTANT', 5 ); * * * Produces: * * * define( 'MY_CONSTANT', 5 ); * * * * @param string $name * @param string $value * @param boolean $caseSensitive * @returns void * @throws PhpGeneratorException if it was not possible to write the define to the output file. */ public function appendDefine( $name, $value, $caseInsensitive = false ) { $valueData = var_export( $value, true ); $case = ''; if ( $caseInsensitive == true ) { $case = ', true'; } $this->write( $this->indentCode( "define( '$name', $valueData". $case . ' );' . $this->lineBreak ) ); } /** * Assigns $value to the variable $name. $value is exported using var_export() * which allows you to use complex structures for $value. If you want to append an assignment * to a variable in the generated code use appendVariableAssignment. * * You can control the assignement type with the $assignmentType parameter. * * Example: * * $array = array( 1, 2, 3 ); * $php->addValueAssignment( 'ProducedArray', $array ); * * * Produces the PHP code * * * $ProducedArray = array( 1, 2, 3 ); * * * @param string $name * @param mixed $value; * @param $assignmentType Controls the way the value is assigned, choose one of the following: * - PHPCREATOR_VARIABLE_ASSIGNMENT, assign using \c = (default) * - PHPCREATOR_VARIABLE_APPEND_TEXT, append using text concat operator \c . * - PHPCREATOR_VARIABLE_APPEND_ELEMENT, append element to array using append operator \c [] * @returns void * @throws PhpGeneratorException if it was not possible to write the assignment to the output file. */ public function appendValueAssignment( $name, $value, $assignmentType = self::ASSIGN_NORMAL ) { switch ( $assignmentType ) { case self::ASSIGN_NORMAL: $this->write( $this->indentCode( "\${$name} = ". var_export( $value, true). ";{$this->lineBreak}" ) ); break; case self::ASSIGN_APPEND_TEXT: $this->write( $this->indentCode( "\${$name} .= ". var_export( $value, true). ";{$this->lineBreak}" ) ); break; case self::ASSIGN_ADD: $this->write( $this->indentCode( "\${$name} += ". var_export( $value, true). ";{$this->lineBreak}" ) ); break; case self::ASSIGN_SUBTRACT: $this->write( $this->indentCode( "\${$name} -= ". var_export( $value, true). ";{$this->lineBreak}" ) ); break; case self::ASSIGN_ARRAY_APPEND: $this->write( $this->indentCode( "\${$name}[] = ". var_export( $value, true). ";{$this->lineBreak}" ) ); break; default: // default to ASSIGN_NORMAL $this->write( $this->indentCode( "\${$name} = ". var_export( $value, true). ";{$this->lineBreak}" ) ); break; } } /** * Assigns the variable named $variable to the variable $name. * You can control the assignement type with the $assignmentType parameter. * * Example: * * $php->addVariableAssignment( 'ProducedArray', 'otherVar' ); * * * Produces the PHP code * * * $ProducedArray = $otherVar; * * * @param string $name * @param mixed $value; * @param $assignmentType Controls the way the value is assigned, choose one of the following: * - PHPCREATOR_VARIABLE_ASSIGNMENT, assign using \c = (default) * - PHPCREATOR_VARIABLE_APPEND_TEXT, append using text concat operator \c . * - PHPCREATOR_VARIABLE_APPEND_ELEMENT, append element to array using append operator \c [] * @returns void * @throws PhpGeneratorException if it was not possible to write the assignment to the output file. */ public function appendVariableAssignment( $name, $variable, $assignmentType = self::ASSIGN_NORMAL ) { switch ( $assignmentType ) { case self::ASSIGN_NORMAL: $this->write( $this->indentCode( "\${$name} = ". '$' . $variable. ";{$this->lineBreak}" ) ); break; case self::ASSIGN_APPEND_TEXT: $this->write( $this->indentCode( "\${$name} .= ". '$' .$variable . ";{$this->lineBreak}" ) ); break; case self::ASSIGN_ADD: $this->write( $this->indentCode( "\${$name} += ". '$' .$variable . ";{$this->lineBreak}" ) ); break; case self::ASSIGN_SUBTRACT: $this->write( $this->indentCode( "\${$name} -= ". '$' .$variable . ";{$this->lineBreak}" ) ); break; case self::ASSIGN_ARRAY_APPEND: $this->write( $this->indentCode( "\${$name}[] = ". '$' .$variable . ";{$this->lineBreak}" ) ); break; default: // default to ASSIGN_NORMAL $this->write( $this->indentCode( "\${$name} = ". '$' .$variable . ";{$this->lineBreak}" ) ); break; } } /** * Unsets the variable $name. * * Example: * * $php->addVariableUnset( 'offset' ); * * * Produces the PHP code: * * * unset( $offset ); * * * @param string $name * @returns void * @throws PhpGeneratorException if it was not possible to write the unset to the output file. */ public function appendUnset( $name ) { $this->write( $this->indentCode( "unset( \${$name} );{$this->lineBreak}" ) ); } /** * Inserts code to unset a list of variables with name from \a $list. * * Example: * * $php->addVariableUnsetList( array ( 'var1', 'var2' ) ); * * * Produces the PHP code: * * * unset( $var1, $var2 ); * * * @see http://php.net/manual/en/function.unset.php * @param array $list Array of variable names. * @returns void * @throws PhpGeneratorException if it was not possible to write the unset to the output file. */ public function appendUnsetList( array $list ) { $first = true; $variables = ''; foreach ( $list as $item ) { if ( !$first ) { $variables .= ', '; } else { $first = false; } $variables .= "\${$item}"; } $this->write( $this->indentCode( "unset( $variables );{$this->lineBreak}" ) ); } /** * Inserts $lines number of empty lines to the generated PHP code. * * Example: * * $php->addSpace( 1 ); * * * @returns void * @throws PhpGeneratorException if it was not possible to write the empty lines to the output file. */ public function appendEmptyLines( $lines = 1 ) { $this->write( str_repeat( $this->lineBreak, $lines ) ); } /** * Inserts a function call in the generated code. * * Set the $returnData parameter if you want to catch the return value. * * Example: * * $php->appendFunctionCall( 'str_repeat', array( array( 'variable' => 'repeat' ), array( 'value' => 4 ) ); * * * Produces the PHP code: * * * str_repeat( 'repeat', 4 ); * * * @param string $methodName * @param array(ezcPhpGeneratorParameter) $methodParameters * @param ezcPhpGeneratorReturnData $returnValue * @returns void * @throws PhpGeneratorException if it was not possible to write the method call to the output file. */ public function appendFunctionCall( $functionName, array $parameters, $returnData = false ) { $this->appendMethodOrFunctionCall( $functionName, $parameters, $returnData ); } /** * Inserts a method call on an object in the generated code. * * Adds a call to method $methodName on the object $objectName with parameters $parameters. * Set the $returnData parameter if you want to catch the return value. * * Example: * * $php->appendMethodCall( 'node', 'name', array(), array( 'name' => 'result' ) ); * * * Produces the PHP code: * * * $result = $node->name(); * * * @param string $objectName * @param string $methodName * @param array(ezcPhpGeneratorParameter) $methodParameters * @param ezcPhpGeneratorReturnData $returnValue * @returns void * @throws PhpGeneratorException if it was not possible to write the method call to the output file. */ public function appendMethodCall( $objectName, $methodName, array $parameters = array(), $returnData = false ) { $this->appendMethodOrFunctionCall( $methodName, $parameters, $returnData, $objectName ); } /** * Inserts a method or function call in the generated code. * * A method call is inserted if $objectName is provided. If not a function call is inserted. This * method is a helper method for appendFunctionCall and appendMethodCall. See their description for * further description of the parameters and examples. * * @param string $methodName * @param array(ezcPhpGeneratorParameter) $methodParameters * @param ezcPhpGeneratorReturnData $returnValue * @returns void * @throws PhpGeneratorException if it was not possible to write the method call to the output file. */ protected function appendMethodOrFunctionCall( $functionName, array $parameters, $returnData = false, $objectName = false ) { // prepare the return part $returnString = ''; if ( $returnData != false ) { switch ( $returnData->type ) { case self::ASSIGN_NORMAL: $returnString = "\${$returnData->variable} ="; break; case self::ASSIGN_APPEND_TEXT: $returnString = "\${$returnData->variable} .="; break; case self::ASSIGN_ADD: $returnString = "\${$returnData->variable} +="; break; case self::ASSIGN_SUBTRACT: $returnString = "\${$returnData->variable} -="; break; case self::ASSIGN_ARRAY_APPEND: $returnString = "\${$returnData->variable}[] ="; break; default: // default to ASSIGN_NORMAL $returnString = "\${$returnData->variable} ="; break; } $returnString .= ' '; // append trailing space } // prepare the object string if this is a call to an object $objectString = ''; if ( $objectName !== false ) { $objectString = "\${$objectName}->"; } // prepare the parameters $parameterString = ''; if ( is_array( $parameters ) && count( $parameters ) > 0 ) { $firstParam = true; foreach ( $parameters as $parameter ) { if ( $parameter->type == ezcPhpGeneratorParameter::VALUE ) { $parameterString .= $firstParam ? '' : ', '; $parameterString .= var_export( $parameter->variable, true ); } else if ( $parameter->type == ezcPhpGeneratorParameter::VARIABLE ) { $parameterString .= $firstParam ? '' : ', '; $parameterString .= "\${$parameter->variable}"; } // else <-- we could have thrown an exception, but we simply ignore this $firstParam = false; } } $this->write( $this->indentCode( "$returnString$objectString$functionName( $parameterString );" . $this->lineBreak ) ); } /** * Inserts a custom code into the generated code. * * The $code will be indented and inserted directly into the generated * code. An additional linebreak at the end of your code will be inserted automatically. * * Example: * * $php->addCodePiece( "if ( \$value > 2 )" . $php->lineBreak() * . '{' . $php->lineBreak() . "\$value = 2;" ); * * * Produces the PHP code: * * * if ( $value > 2 ) * { * $value = 2; * } * * * @param string $code * @returns void * @throws PhpGeneratorException if it was not possible to write the custom codeto the output file. */ public function appendCustomCode( $code ) { $this->write( $this->indentCode( $code . $this->lineBreak ) ); } /** * Inserts a comment into the code. * * The comment will be display using an end-of-line * comment (//). * * Example: * * $php->addComment( "This file is auto generated. Do not edit!" ); * * * Produces the PHP code: * * * // This file is auto generated. Do not edit! * * * @param string $comment * @returns void * @throws PhpGeneratorException if it was not possible to write the comment to the output file. */ public function appendComment( $comment ) { $this->write( $this->indentCode( '//' . $comment . $this->lineBreak ) ); } /** * Inserts an if statement. * * The complete condition of the if statement is provided through $condition. * The if statement must be closed properly with a call to appendEndIf(). * * Example: * * $php->appendIf( '$myVar === 0 ' ); * $php->appendEndIf(); * * * Produces the PHP code: * * * if ( $myVar === 0 ) * { * } * * * @see $ezcPhpGenerator::appendElse() * @see $ezcPhpGenerator::appendEndIf() * @param string $condition * @returns void * @throws PhpGeneratorException if it was not possible to write the if statement to the output file. */ public function appendIf( $condition ) { $this->write( $this->indentCode( "if ( $condition )" . $this->lineBreak . '{' . $this->lineBreak ) ); $this->indentLevel++; $this->flowStack[] = self::FLOW_IF; } /** * Ends an if statement. * * @see $ezcPhpGenerator::appendIf() * @returns void * @throws PhpGeneratorException if it was not possible to write to the output file * or if the method was not properly nested with an appendIf. */ public function appendEndIf() { $this->appendEnd( self::FLOW_IF ); } /** * Inserts an else or an else if statement. * * If a $condition is provided an else if statement is generated. * If not, an else statement is generated. You can only call this method * after calling appendIf first. * * Example: * * $php->appendIf( '$myVar === 0 ' ); * $php->appendElse( '$myVar2 === 0 ' ); * $php->appendElse(); * $php->appendEndIf(); * * * Produces the PHP code: * * * if ( $myVar === 0 ) * { * } * else if ( $myVar ) * { * } * else * { * } * * * @see $ezcPhpGenerator::appendIf() * @see $ezcPhpGenerator::appendEndIf() * @param string $condition * @returns void * @throws PhpGeneratorException if it was not possible to write the if statement to the output file. */ public function appendElse( $condition = '' ) { // check that we are in the correct flow type $pop = array_pop( $this->flowStack ); if ( $pop == self::FLOW_IF ) { $this->flowStack[] = self::FLOW_IF; // push it back $this->indentLevel--; if ( $condition != '' ) { $condition = 'if ( ' . $condition . ' )'; } $this->write( $this->indentCode( '}' . $this->lineBreak ) . "else $condition" . $this->lineBreak . '{' . $this->lineBreak ); $this->indentLevel++; } else { $this->abort(); $message = $pop ? "Expected end of <{$pop}>, but <{$type}> was requested" : 'Currently not in a flow control structure'; throw new ezcPhpGeneratorException( $message, ezcPhpGeneratorException::FLOW_ERROR ); } } /** * Inserts a foreach statement. * * The complete condition of the foreach statement is provided through $condition. * The foreach statement must be closed properly with a call to appendEndForeach(). * * Example: * * $php->appendForeach( '$myArray as $item ' ); * $php->appendEndForeach(); * * * Produces the PHP code: * * * foreach ( $myArray as $item ) * { * } * * * @see $ezcPhpGenerator::appendEndForeach() * @param string $condition * @returns void * @throws PhpGeneratorException if it was not possible to write the foreach statement to the output file. */ public function appendForeach( $condition ) { $this->write( $this->indentCode( "foreach ( $condition )" . $this->lineBreak . '{' . $this->lineBreak ) ); $this->indentLevel++; $this->flowStack[] = self::FLOW_FOREACH; } /** * Ends a foreach statement. * * @see $ezcPhpGenerator::appendForeach() * @returns void * @throws PhpGeneratorException if it was not possible to write to the output file * or if the method was not properly nested with an appendForeach. */ public function appendEndForeach() { $this->appendEnd( self::FLOW_FOREACH ); } /** * Inserts a while statement. * * The complete condition of the while statement is provided through $condition. * The while statement must be closed properly with a call to appendEndWhile(). * * Example: * * $php->appendWhile( '$myVar > 0' ); * $php->appendEndWhile(); * * * Produces the PHP code: * * * while ( $myVar > 0 ) * { * } * * * @see $ezcPhpGenerator::appendEndWhile() * @param string $condition * @returns void * @throws PhpGeneratorException if it was not possible to write the while statement to the output file. */ public function appendWhile( $condition ) { $this->write( $this->indentCode( "while ( $condition )" . $this->lineBreak . '{' . $this->lineBreak ) ); $this->indentLevel++; $this->flowStack[] = self::FLOW_WHILE; } /** * Ends a while statement. * * @see $ezcPhpGenerator::appendWhile() * @returns void * @throws PhpGeneratorException if it was not possible to write to the output file * or if the method was not properly nested with an appendWhile. */ public function appendEndWhile() { $this->appendEnd( self::FLOW_WHILE ); } /** * Inserts a do statement. * The do statement must be closed properly with a call to appendEndDo(). * * Example: * * $php->appendDo(); * $php->appendEndDo( '$myVar > 0' ); * * * Produces the PHP code: * * * do * { * } while ( $myVar > 0 ); * * * @see $ezcPhpGenerator::appendEndDo() * @returns void * @throws PhpGeneratorException if it was not possible to write to the output file. */ public function appendDo() { $this->write( $this->indentCode( 'do' . $this->lineBreak . '{' . $this->lineBreak ) ); $this->indentLevel++; $this->flowStack[] = self::FLOW_DO; } /** * Ends a do statement. * * The complete condition of the do statement is provided through $condition. * * @see $ezcPhpGenerator::appendDo() * @param string $condition * @returns void * @throws PhpGeneratorException if it was not possible to write the do statement to the output file * or if the method was not properly nested with an appendDo. */ public function appendEndDo( $condition ) { $pop = array_pop( $this->flowStack ); if ( $pop == self::FLOW_DO ) { $this->indentLevel--; $this->write( $this->indentCode( '} while ( ' . $condition . ' );'. $this->lineBreak ) ); } else { $this->abort(); $message = $pop ? "Expected end of <{$pop}>, but <{$type}> was requested" : 'Currently not in a flow control structure'; throw new ezcPhpGeneratorException( $message, ezcPhpGeneratorException::FLOW_ERROR ); } } /** * Checks that the end call is properly nested using $type and the flow stack. * * @param type One of the flow types FLOW_IF, FLOW_FOREACH, FLOW_WHILE or FLOW_DO. * @returns void * @throws PhpGeneratorException if it was not possible to write to the output file or if a nesting * error was detected. */ private function appendEnd( $type ) { $pop = array_pop( $this->flowStack ); if ( $pop == $type ) { $this->indentLevel--; $this->write( $this->indentCode( '}' . $this->lineBreak ) ); } else { $this->abort(); $message = $pop ? "Expected end of <{$pop}>, but <{$type}> was requested" : 'Currently not in a flow control structure'; throw new ezcPhpGeneratorException( $message, ezcPhpGeneratorException::FLOW_ERROR ); } } /** * Writes $data to $this->fileResource * * @throws PhpGeneratorException if it was not possible to write to the file. * @returns void */ protected function write( $data ) { if ( !$this->fileResource ) { throw new ezcPhpGeneratorException( 'ezcPhpGenerator could not write to the temporary file. ' . 'It has already been closed', ezcPhpGeneratorException::FILE_WRITE_FAILED ); } if ( fwrite( $this->fileResource, $data ) === false ) { throw new ezcPhpGeneratorException( 'ezcPhpGenerator could not write to the temporary file: ' . $this->tmpFilename, ezcPhpGeneratorException::FILE_WRITE_FAILED ); } } /** * Returns each line in $text indented correctly if indenting is turned on. * If indenting is turned off it will return $text unmodified. * * @param string $text * @return string */ protected function indentCode( $text ) { if ( $this->niceIndentation == false || $this->indentLevel == 0 ) return $text; $textArray = explode( $this->lineBreak, $text ); $newTextArray = array(); foreach ( $textArray as $text ) { if ( trim( $text ) != '' ) { $textLine = str_repeat( $this->indentString, $this->indentLevel ) . $text; } else { $textLine = $text; } $newTextArray[] = $textLine; } return implode( $this->lineBreak, $newTextArray ); } } ?>