Thursday, March 22, 2012

Organizing your libraries like a boss

Recently I've been creating a lot of small PHP libs/modules that I would like to be able to reuses in other projects. Issues always come about when I need to include the files from other modules and how to keep them path independent and organized without wrapping everything into a parent namespace. Namespaces, autoloaders, and a bootloader has been the best solution I've come up with until now. So for example, I have a message queue module (group of classes) which I keep in a directory /hacklabs/modules/mq/. Everything in this directory is namespaced in 'mq', for example the class "queue" ( /hacklabs/modules/mq/queue.php ):
namespace mq;

class queue {
    # ...
}
/hacklabs/modules/mq/exceptions/empty_queue_exception.php:
namespace mq\exceptions;

class empty_queue_exception {
    # ...
}
In another project, I would like to use this mq module. Now comes the bootloader of the mq module ( /hacklabs/modules/mq/bootloader.php )

define('MQ_PATH', dirname(__FILE__));

# add the parent directory to the include path:
set_include_path( get_include_path() . PATH_SEPARATOR . realpath( dirname( __FILE__ ) . '/../' ) );

spl_autoload_register();
At this point, if I want to use the module 'mq' from a project in /hacklabs/projects/big_project/ all I need to do is include the bootloader from my modules and the default spl autoloader will take care of the rest:
namespace big_project;

require_once('/hacklabs/modules/mq/bootloader.php');
# also load a forums module bootloader:
require_once('/hacklabs/modules/forums/bootloader.php');

$queue = new \mq\queue();
$msg = new \mq\message();

try {
    $queue->send( $msg );
} catch ( \mq\exceptions\empty_queue_exception $e ) {
    # ...
} catch ( \exception $e ) {
    # ...
}

$forum = new \forums\forum();
# ... 
Using this technique, I can make module specific defines in the modules bootloader while the actual autoload include path is anything in /hacklabs/modules/*. Include just the modules bootloader, then the rest is handled via spl_autoload_register() and namespaced code.

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