Tuesday, January 10, 2012

A process manager in PHP

namespace shell;

/**
 * pcntl is a process manager for spawning new child processes
 * either through php closures or shell commands. pcntl will
 * make sure zombies dont start invading your systems
 * @author shean massey
 * @since Jan 7th 2012
 */
class pcntl {
    protected $_child_pids = array();
    protected $_max_wait = 0;
    protected $_debug_mode = false;

    /**
     * register the wait function as a shutdown procedure
     * and register the signal handlers for SIGCHLD and SIGTERM
     */
    public function __construct() {
        foreach ( array( SIGCHLD, SIGTERM ) as $signal )
        if ( false === ( pcntl_signal( $signal, array( $this, 'signal_handler') ) ) ) {
            throw new \exception('failed tp attach signal: ' . $signal . ' handler');
        }
        # on shutdown, wait() for all/any forked child processes 
        register_shutdown_function( array( $this, 'wait' ) );
    }

    /**
     * on destruction wait() for all/any forked child processes
     */
    public function __destruct() {
        $this->wait();
    }

    /**
     * enable/disable debug messages
     */
    public function use_debugger( $bool = true ) {
        $this->_debug_mode = (bool)$bool;
        return $this;
    }

    /**
     * this is the signal handler registered for cleaning the dead children
     * processes and remove them from the pid collection.
     */
    public function signal_handler( $signum ) {
        switch ( $signum ) {
            # this event is sent to processes when one of their child processes
            # passed away
            case SIGCHLD:
                while ( $pid = pcntl_wait( $status, WNOHANG) ) {
                    $this->debug( function() use ($pid, $status){
                        echo 'caught pid by signal handler: ', $pid, ' ';
                        echo 'return status: ', $status, ' ';
                        echo '[my pid = ', posix_getpid(), ']', PHP_EOL;
                    });
                    # there are no more children to handle:
                    if ( empty( $this->_child_pids ) ) return;
                    # pcntl_wait failed:
                    if ( $pid === -1 ) {
                        pcntl_signal_dispatch();
                        break;
                    }
                    # how could this even happen ?
                    if ( ! array_key_exists( $pid, $this->_child_pids ) ) {
                        throw new \appcore\exception('caught someone elses dead baby');
                    }
                    # remove the child pid from the pid collection
                    unset( $this->_child_pids[ $pid ] );
                }
            break;
            # this process is sent from a kill -1:
            case SIGTERM:
                
            break;
        }
    }

    /**
     * fork a closure as a new process
     */
    public function fork( \closure $function ) {
        switch ( $pid = pcntl_fork() ) {
            # error:
            case -1:
                throw new \appcore\exception('failed to fork()');
            break;
            # child proc:
            case 0:
                # empty the array of children:
                $this->_child_pids = array();
                $function();
                exit(0);
            break;
            # parent proc:
            default:
                $this->_child_pids[ $pid ] = $pid;
                $this->debug( function() use ($pid) {
                    echo 'new child: ', $pid, PHP_EOL;
                });
                pcntl_signal_dispatch();
                return $this;
            break;
        }
    }

    /**
     * fork a shell command
     */
    public function shell_fork( $cmd_line = '' ) {
        $this->fork( function() use ( $cmd_line ) {
            $cmd_line .= ' 2>&1 > /tmp/proc_manager.lck.'.posix_getpid().' &';
            shell_exec( $cmd_line );
        });
        pcntl_signal_dispatch();
        return $this;
    }

    /**
     * if the debug_mode is set, the execute the function being
     * passed as a closure. Note that this function should never
     * attempt to change the current state.
     */
    public function debug( \closure $function ) {
        if ( ! $this->_debug_mode ) return false;
        $function();
        return true;
    }

    /**
     * this will loop with short sleeps and dispatch any lingering
     * signals to the signal handler (to reap the dead children)
     */
    public function wait() {
        $this->debug( function(){
            echo 'dispatch loop', PHP_EOL;
        });
        while ( true ) {
            if ( ! $this->_child_pids ) break;
            pcntl_signal_dispatch();
            usleep(10);
        }
        return $this;
    }
}
Using it is VERY simple, you fork a new process either as a closures or as shell commands:
#!/usr/bin/env php
use_debugger( true );

$proc_manager->fork( function( ) {
  $i = 0;
  while ( $i++ < 5 ) {
    sleep( 1 );
    file_put_contents('/tmp/test', 'i = '.$i.PHP_EOL, FILE_APPEND);
  }
});

$proc_manager->fork( function( ) {
  $m = 0; while ( $m++ < 6 ) {
    sleep( 1 ); 
    file_put_contents('/tmp/test', 'm = '.$m.PHP_EOL, FILE_APPEND);
  }
});

$proc_manager->shell_fork('sleep 1 && ls -la');

$proc_manager->shell_fork('rsync # ...');

Wednesday, January 4, 2012

PHP: errors/warnings/notices to exceptions

Something to append to all your bootstraps:
set_error_handler( function( $num, $msg, $file, $line ) {
  # take into account the '@' operators ( or remove this line and ignore them ):
  if ( error_reporting() === 0 ) return false;
  throw new \ErrorException( $msg, $num, 0, $file, $line );
});
This will transform all user catchable internal php errors (not parse errors) into \ErrorException exceptions (a subclass of \Exception ).
Cheers

Wednesday, November 9, 2011

Lazy loading with PHP magic methods

Using some php magic for a dead simple lazy loading framework:
class DemoContext {
    protected $_lazy_vars = array(
        'cache' => null,
        'request' => null,
    );

    # the magic happens here ;)
    public function __get( $varname ) {
        if ( ! array_key_exists( $varname, $this->_lazy_vars ) )
            return null;
        if ( $this->_lazy_vars[ $varname ] !== null )
            return $this->_lazy_vars[ $varname ];

        $value = null;
        switch ( $varname ) {
            case 'cache':
                $value = new \appcore\cache();
            break;
            case 'request':
                $value = new \appcore\request();
            break;
        }
        $this->_lazy_vars[ $varname ] = $value;
        return $value;
    }
}

# using it:
$demo = new DemoContext();
if ( $demo->cache->check() ) {
    # ...
}

if ( $demo->request->hasHeader('Location') ) {
    # ...
}
Using lazy loading is great for setting back of any object creation until you're sure you actually need it. This could be a huge memory improvement in some cases, anywhere where there might be useless object creation.

Saturday, October 22, 2011

FCache Revisited

After some refactoring I found a way to use a closure for my content cacher instead of fake "code blocks". Now using it is much easier to read and understand, the code that needs to be cached is passed as the implementation of an anonymous function. The new cache_block() method:
/**
 * cache a block of code
 * @param string $key the cache key
 * @param int $seconds the number of seconds before a cache file is outdated
 * @param function the closure to cache the contents of
 */
public static function cache_block( $key, $seconds = null, $function ) {
  if ( self::is_cached( $key, $seconds ) ) {
    echo file_get_contents( self::get_path( $key ) );
    return true;
  }
  self::init_buffer();
  call_user_func( $function );
  echo self::save_buffer( $key );
  return true;
}
And an example usage of the new method:
public function demo() {
  $self = $this; # php5.3 cant pass $this with use(), but 5.4 will be able to!

  \appcore\fcache::cache_block('demo_cache_1', 2, function() use ($self) {
    $self->show('header');
    $self->show('index');
    \appcore\fcache::cache_block('demo_cache_2', 4, function() use ($self) {
      $self->show('footer');
    });
  });
}
This creates 2 cached elements, 'demo_cache_1' and 'demo_cache_2'. The demo_cache_1 element expires after 2 seconds and the demo_cache_2 after 4 seconds. The means when the contents of the demo_cache_1 expire and the block is re-executed, the contents of demo_cache_2 may be re-used if they havn't expired yet in the creation of the outer block cache. Here's the entire fcache class refactored:
namespace appcore;

/**
 * file caching methods. this is a key/value implementation.
 * @author smassey
 * @since may 5th 2011
 * may 22nd 2011 - added events
 * oct 16th 2011 - refactored + added the cache_block() method
 */
class fcache extends namespace\base\object {
  private function __construct() {}
  private function __clone() {}

  /**
   * check if a file is cached and optionaly non expired
   * @param mixed $key
   * @param int $seconds
   * @return false if a cached file for the given key isnt found
   * or if the file exits and is outdated. return true otherwise. 
   */
  public static function is_cached( $key, $seconds = null ) {
    if ( ! file_exists( self::get_path( $key ) ) ) return false;
    return ( $seconds ) ? ( ! self::is_outdated( $key, $seconds ) ) : true;
  }

  /**
   * get a cached file
   * @param mixed $key the key of the cached file
   * @param int $exp_seconds the expire time of the cached file in seconds
   */
  public static function get_cache( $key ) {
    if (self::is_cached( $key )) {
      \appcore\events::send('trace', 'found cache ' . md5($key));
      return file_get_contents( self::get_path( $key ) );
    }
    return false;
  }

  /**
   * save contents into a cached file
   * @param $key the cache key
   * @param $value the contents to cache
   */
  public static function cache( $key, $value ) {
    $path = self::get_path( $key );
    if ( file_put_contents( $path, $value ) === false ) {
      throw new \Exception("failed to write file: $path");
    }
  }

  /**
   * cache a block of code
   * @param string $key the cache key
   * @param int $seconds the number of seconds before a cache file is outdated
   * @param function the closure to cache the contents of
   */
  public static function cache_block( $key, $seconds = null, $function ) {
    if ( self::is_cached( $key, $seconds ) ) {
      echo file_get_contents( self::get_path( $key ) );
      return true;
    }
    self::init_buffer();
    call_user_func( $function );
    echo self::save_buffer( $key );
    return true;
  }

  /**
   * start the buffers
   */
  public static function init_buffer() {
    ob_start();
  }

  /**
   * save a buffer
   * @param $key the cache key
   */
  public static function save_buffer( $key ) {
    \appcore\events::send('trace', 'saving buffer to file cache');
    $contents = ob_get_contents();
    ob_end_clean();
    self::cache( $key, $contents );
    return $contents;
  }

  /**
   * determine the real full path + filename for a cache file
   * @param string $key the cache key
   * @returns the full path + filename
   */
  protected static function get_path( $key ) {
    return CACHE_PATH . md5($key) . '.cache.php';
  }
 
  /**
   * deterine if a file is outdated
   * @param string $key the cached key
   * @param int $seconds number of seconds to check the caches age against
   */
  protected static function is_outdated( $key, $seconds = false ) {
    if ( ! $seconds ) return false;
    $file_stats = stat( self::get_path( $key ) );
    if ( ( time() - $file_stats['mtime'] ) > $seconds ) return true;
    return false;
  }
}

Tuesday, August 16, 2011

An expression parser in PHP

This is a little mathematical expression parser I wrote out of sheer boredom. After finding my "calculator assignment" from highschool (which worked, but had absolutly no style, just a couple hundred lines of sequential handling) I decided I really needed to take another shot at it.. And this time, to do thing right and build an expression tree from the input then evaluate the tree.

First a brief walkthrough:
- create a stack of context handlers (a handler must react on passed tokens)
- first step is to tokenize the content into a list of tokens
- push the first (global) context onto the context stack
- iterate through the token list and send the token to the last context handler on the stack
( the context handlers are responsible for pushing/poping contexts onto the stack )
- pop off the last context, this should be the first pushed, the global (the trunk of the tree)

Example input: 3 + ( 4 * 2.3 ) - 1
the tokenized list would be: ['3', '+', '(', '4', '*', '2.3', ')', '-', '1']
- push a global scope onto the context handler stack (context 0)
looping through the tokens:
on '3': the context handler adds 3 to its expression list (context 1)
on '+': the context handler adds + to its expression list (context 1)
on '(': the context handler pushes a new context onto the the context handler stack (context 2)
on '4': the context handler adds 4 to its expression list (context 2)
on '*': the context handler adds * to its expression list (context 2)
on '2.3': the context handler adds 2.3 to its expression list (context 2)
on ')': the context handler pops off 'context 2', adds it to the expression list of context 1
on '-': the context handler adds - to the expression list (context 1)
on '1': the context handler adds 1 to the expression list (context 1)

context 1 looks like this: 3 + (context 2) - 1
context 2 looks like this: 4 * 2.3

Now that the data is parsed into the tree structure, some recursive evaluation and we've got the result.

/exprlib/Parser.php
<?php
namespace exprlib;

/**
 * this model handles the tokenizing, the context stack functions, and
 * the parsing (token list to tree trans).
 * as well as an evaluate method which delegates to the global scopes evaluate.
 */

class Parser {
    protected $_content = null;
    protected $_context_stack = array();
    protected $_tree = null;
    protected $_tokens = array();

    public function __construct($content = null) {
        if ( $content ) {
        	$this->set_content( $content );
        }
    }

    /**
     * this function does some simple syntax cleaning:
     * - removes all spaces
     * - replaces '**' by '^'
     * then it runs a regex to split the contents into tokens. the set
     * of possible tokens in this case is predefined to numbers (ints of floats)
     * math operators (*, -, +, /, **, ^) and parentheses.
     */
    public function tokenize() {
        $this->_content = str_replace(array("\n","\r","\t"," "), '', $this->_content);
        $this->_content = str_replace('**', '^', $this->_content);
        $this->_content = str_replace('PI', (string)PI(), $this->_content);
        $this->_tokens = preg_split(
        	'@([\d\.]+)|(sin\(|cos\(|tan\(|sqrt\(|\+|\-|\*|/|\^|\(|\))@',
        	$this->_content,
        	null,
        	PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
        );
        return $this;
    }

    /**
     * this is the the loop that transforms the tokens array into
     * a tree structure.
     */
    public function parse() {
        # this is the global scope which will contain the entire tree
        $this->push_context( new \exprlib\contexts\Scope() );
        foreach ( $this->_tokens as $token ) {
        	# get the last context model from the context stack,
        	# and have it handle the next token	 
        	$this->get_context()->handle_token( $token );
        }
        $this->_tree = $this->pop_context();

        return $this;
    }

    public function evaluate() {
        if ( ! $this->_tree ) {
        	throw new \exprlib\exceptions\ParseTreeNotFoundException();
        }
        return $this->_tree->evaluate();
    }

    /*** accessors and mutators ***/

    public function get_tree() {
        return $this->_tree;
    }
    
    public function set_content($content = null) {
        $this->_content = $content;
        return $this;
    }

    public function get_tokens() {
        return $this->_tokens;
    }


    /*******************************************************
     * the context stack functions. for the stack im using 
     * an array with the functions array_push, array_pop,
     * and end to push, pop, and get the current element
     * from the stack.
     *******************************************************/

    public function push_context( \exprlib\contexts\IfContext $context ) {
        array_push( $this->_context_stack, $context );
        $this->get_context()->set_builder( $this );
    }

    public function pop_context() {
        return array_pop( $this->_context_stack );
    }

    public function get_context() {
        return end( $this->_context_stack );
    }
}


The global scope in this case is also the parent scope of all other scopes:
/exprlib/contexts/Scope.php
<?php
namespace exprlib\contexts;

class Scope implements namespace\IfContext {
    protected $_builder = null;
    protected $_children_contexts = array();
    protected $_raw_content = array();
    protected $_operations = array();

    const T_NUMBER = 1;
    const T_OPERATOR = 2;
    const T_SCOPE_OPEN = 3;
    const T_SCOPE_CLOSE = 4;
    const T_SIN_SCOPE_OPEN = 5;
    const T_COS_SCOPE_OPEN = 6;
    const T_TAN_SCOPE_OPEN = 7;
    const T_SQRT_SCOPE_OPEN = 8;

    public function set_builder( \exprlib\Parser $builder ) {
        $this->_builder = $builder;
    }

    public function __toString() {
        return implode('', $this->_raw_content);
    }

    public function add_operation( $operation ) {
        $this->_operations[] = $operation;
    }

    /**
     * handle the next token from the tokenized list. example actions
     * on a token would be to add it to the current context expression list,
     * to push a new context on the the context stack, or pop a context off the
     * stack.
     */
    public function handle_token( $token ) {
        $type = null;
    
        if ( in_array( $token, array('*','/','+','-','^') ) ) $type = self::T_OPERATOR;
        if ( $token === ')' ) $type = self::T_SCOPE_CLOSE;
        if ( $token === '(' ) $type = self::T_SCOPE_OPEN;
        if ( $token === 'sin(' ) $type = self::T_SIN_SCOPE_OPEN;
        if ( $token === 'cos(' ) $type = self::T_COS_SCOPE_OPEN;
        if ( $token === 'tan(' ) $type = self::T_TAN_SCOPE_OPEN;
        if ( $token === 'sqrt(' ) $type = self::T_SQRT_SCOPE_OPEN;

        if ( is_null( $type ) ) {
            if ( is_numeric( $token ) ) {
                $type = self::T_NUMBER;
                $token = (float)$token;
            }
        }

        switch ( $type ) {
            case self::T_NUMBER:
            case self::T_OPERATOR:
                $this->_operations[] = $token;
            break;
            case self::T_SCOPE_OPEN:
                $this->_builder->push_context( new namespace\Scope() );
            break;
            case self::T_SIN_SCOPE_OPEN:
                $this->_builder->push_context( new namespace\SineScope() );
            break;
            case self::T_COS_SCOPE_OPEN:
                $this->_builder->push_context( new namespace\CosineScope() );
            break;
            case self::T_TAN_SCOPE_OPEN:
                $this->_builder->push_context( new namespace\TangentScope() );
            break;
            case self::T_SQRT_SCOPE_OPEN:
                $this->_builder->push_context( new namespace\SqrtScope() );
            break;
            case self::T_SCOPE_CLOSE:
                $scope_operation = $this->_builder->pop_context();
                $new_context = $this->_builder->get_context();
                if ( is_null( $scope_operation ) || ( ! $new_context ) ) {
                    # this means there are more closing parentheses than openning
                    throw new \exprlib\exceptions\OutOfScopeException();
                }
                $new_context->add_operation( $scope_operation );
            break;
            default:
                throw new \exprlib\exceptions\UnknownTokenException($token);
            break;
        }
    }

    /**
     * order of operations:
     * - parentheses, these should all ready be executed before this method is called
     * - exponents, first order
     * - mult/divi, second order
     * - addi/subt, third order
     */
    protected function _expression_loop( & $operation_list ) {
        while ( list( $i, $operation ) = each ( $operation_list ) ) {
            if ( ! in_array( $operation, array('^','*','/','+','-') ) ) continue;

            $left =  isset( $operation_list[ $i - 1 ] ) ? (float)$operation_list[ $i - 1 ] : null;
            $right = isset( $operation_list[ $i + 1 ] ) ? (float)$operation_list[ $i + 1 ] : null;

            # if ( is_null( $left ) || is_null( $right ) ) throw new \Exception('syntax error');

            $first_order = ( in_array('^', $operation_list) );
            $second_order = ( in_array('*', $operation_list ) || in_array('/', $operation_list ) );
            $third_order = ( in_array('-', $operation_list ) || in_array('+', $operation_list ) );

            $remove_sides = true;
            if ( $first_order ) {
                switch( $operation ) {
                    case '^': $operation_list[ $i ] = pow( (float)$left, (float)$right ); break;
                    default: $remove_sides = false; break;
                }
            } elseif ( $second_order ) {
                switch ( $operation ) {
                    case '*': $operation_list[ $i ] = (float)($left * $right); break;
                    case '/': $operation_list[ $i ] = (float)($left / $right); break;
                    default: $remove_sides = false; break;
                }
            } elseif ( $third_order ) {
                switch ( $operation ) {
                    case '+': $operation_list[ $i ] = (float)($left + $right); break;
                    case '-': $operation_list[ $i ] = (float)($left - $right); break;
                    default: $remove_sides = false; break;
                }
            }

            if ( $remove_sides ) {
                unset( $operation_list[ $i + 1 ], $operation_list[ $i - 1 ] );
                reset( $operation_list = array_values( $operation_list ) );
            }
        }
        if ( count( $operation_list ) === 1 ) return end( $operation_list );
        return false;
    }

    # order of operations:
    # - sub scopes first
    # - multiplication, division
    # - addition, subtraction
    # evaluating all the sub scopes (recursivly):
    public function evaluate() {
        foreach ( $this->_operations as $i => $operation ) {
            if ( is_object( $operation ) ) {
                $this->_operations[ $i ] = $operation->evaluate();
            }
        }

        $operation_list = $this->_operations;

        while ( true ) {
            $operation_check = $operation_list;
            $result = $this->_expression_loop( $operation_list );
            if ( $result !== false ) return $result;
            if ( $operation_check === $operation_list ) {
                break;
            } else {
                reset( $operation_list = array_values( $operation_list ) );
            }
        }
        throw new \Exception('failed... here');
    }
}


Here are 2 of the scopes that extends the global (parent) scope: the sine and squared root scopes

/exprlib/contexts/SineScope.php
<?php
namespace exprlib\contexts;

class SineScope extends namespace\Scope {
    public function evaluate() {
        return sin( deg2rad( parent::evaluate() ) );
    }
}

\exprlib\contexts\SqrtScope.php

<?php
namespace exprlib\contexts;

class SqrtScope extends namespace\Scope {
	public function evaluate() {
		return sqrt( parent::evaluate() );
	}
}

To conclude, here is a user interface I built for the testing. It's a command line shell which inputs expressions and outputs the evaluated expressions or thrown exception messages in case of error:

#!/usr/bin/php
<?php
include('exprlib/loaders.php');

$builder = new \exprlib\Parser();

while ( (fputs(STDOUT,'math > ')) && $e = fgets(STDIN) ) {
	if ( ! ($e = trim($e)) ) continue;
	if ( in_array( $e, array('quit','exit',':q') ) ) break;

	try {
		$result = $builder->set_content($e)->tokenize()->parse()->evaluate();
	} catch ( \exprlib\exceptions\UnknownTokenException $exception ) {
		echo 'unknown token exception thrown in expression: ', $e, PHP_EOL;
		echo 'token: "',$exception->getMessage(),'"',PHP_EOL;
		continue;
	} catch ( \exprlib\exceptions\ParseTreeNotFoundException $exception ) {
		echo 'parse tree not found (missing content): ', $e, PHP_EOL;
		continue;
	} catch ( \exprlib\exceptions\OutOfScopeException $exception ) {
		echo 'out of scope exception thrown in: ', $e, PHP_EOL;
		echo 'you should probably count your parentheses', PHP_EOL;
		continue;
	} catch ( \Exception $exception ) {
		echo 'unknown exception thrown: ', $e, PHP_EOL;
		echo $exception->getMessage(), PHP_EOL;
		continue;
	}

	echo $result, PHP_EOL;
}