<?php
/**
 * Implements threading in PHP
 *
 * PHP version 5
 *
 * @category  Threading
 * @package   None
 * @author    Tudor Barbu <miau@motane.lu>
 * @copyright 2009 MIT
 * @license   http://en.wikipedia.org/wiki/MIT_License MIT License
 * @link      http://blog.motane.lu/2009/01/02/multithreading-in-php/
 */

/**
 * Thread class
 *
 * @category  Threading
 * @package   None
 * @author    Tudor Barbu <miau@motane.lu>
 * @copyright 2009 MIT
 * @license   http://en.wikipedia.org/wiki/MIT_License MIT License
 * @link      http://blog.motane.lu/2009/01/02/multithreading-in-php/
 */

class Thread
{
    const FUNCTION_NOT_CALLABLE = 10;
    const COULD_NOT_FORK        = 15;

    /**
     * Possible errors
     *
     * @var array
     */
    private $_errors = array(
        Thread::FUNCTION_NOT_CALLABLE => 'You must specify a valid function name that can be called from the current scope.',
        Thread::COULD_NOT_FORK        => 'pcntl_fork() returned a status of -1. No new process was created'
    );

    /**
     * Callback for the function that should run as a separate thread
     *
     * @var callback
     */
    protected $runnable;


    /**
     * Holds the current process id
     *
     * @var integer
     */
    private $_pid;
    
    
    /**
     * Exits with error
     *
     * @return void
    */
    
    private function fatalError($errorCode){
        throw new Exception( $this->getError($errorCode) );
    }

    /**
     * Checks if threading is supported by the current PHP configuration
     *
     * @return boolean
     */
    public static function isAvailable()
    {
        $required_functions = array(
            'pcntl_fork',
        );

        foreach ( $required_functions as $function ) {
            if ( !function_exists($function) ) {
                return false;
            }
        }

        return true;
    }

    /**
     * Class constructor - you can pass
     * the callback function as an argument
     *
     * @param callback $runnable Callback reference
     */
    public function __construct( $runnable = null )
    {
        if(!Thread::isAvailable() )throw new Exception("Threads not supported");
        if ( $runnable !== null ) {
            $this->setRunnable($runnable);
        }
    }

    /**
     * Sets the callback
     *
     * @param callback $runnable Callback reference
     *
     * @return callback
     */
    public function setRunnable( $runnable )
    {
        if ( self::isRunnableOk($runnable) ) {
            $this->runnable = $runnable;
        } else {
            $this->fatalError(Thread::FUNCTION_NOT_CALLABLE);
        }
    }

    /**
     * Gets the callback
     *
     * @return callback
     */
    public function getRunnable()
    {
        return $this->runnable;
    }

    /**
     * Checks if the callback is ok (the function/method
     * is runnable from the current context)
     *
     * can be called statically
     *
     * @param callback $runnable Callback
     *
     * @return boolean
     */
    public static function isRunnableOk( $runnable )
    {
        return ( is_callable($runnable) );
    }

    /**
     * Returns the process id (pid) of the simulated thread
     *
     * @return int
     */
    public function getPid()
    {
        return $this->_pid;
    }

    /**
     * Checks if the child thread is alive
     *
     * @return boolean
     */
    public function isAlive()
    {
        $pid = pcntl_waitpid($this->_pid, $status, WNOHANG);
        return ( $pid === 0 );

    }

    /**
     * Starts the thread, all the parameters are
     * passed to the callback function
     *
     * @return void
     */
    public function start()
    {
        $pid = @pcntl_fork();
        if ( $pid == -1 ) {
                $this->fatalError(Thread::COULD_NOT_FORK);
        }
        if ( $pid ) {
            // parent
            $this->_pid = $pid;
        } else {
            // child
            pcntl_signal(SIGTERM, array( $this, 'handleSignal' ));
            $arguments = func_get_args();
            if ( !empty($arguments) ) {
                call_user_func_array($this->runnable, $arguments);
            } else {
                call_user_func($this->runnable);
            }

            exit( 0 );
        }
    }

    /**
     * Attempts to stop the thread
     * returns true on success and false otherwise
     *
     * @param integer $signal SIGKILL or SIGTERM
     * @param boolean $wait   Wait until child has exited
     *
     * @return void
     */
    public function stop( $signal = SIGKILL, $wait = false )
    {
        if ( $this->isAlive() ) {
            posix_kill($this->_pid, $signal);
            if ( $wait ) {
                pcntl_waitpid($this->_pid, $status = 0);
            }
        }
    }

    /**
     * Alias of stop();
     *
     * @param integer $signal SIGKILL or SIGTERM
     * @param boolean $wait   Wait until child has exited
     *
     * @return void
     */
    public function kill( $signal = SIGKILL, $wait = false )
    {
        return $this->stop($signal, $wait);
    }

    /**
     * Gets the error's message based on its id
     *
     * @param integer $code The error code
     *
     * @return string
     */
    public function getError( $code )
    {
        if ( isset( $this->_errors[$code] ) ) {
            return $this->_errors[$code];
        } else {
            return "No such error code $code ! Quit inventing errors!!!";
        }
    }

    /**
     * Signal handler
     *
     * @param integer $signal Signal
     *
     * @return void
     */
    protected function handleSignal( $signal )
    {
        switch( $signal ) {
        case SIGTERM:
            exit( 0 );
            break;
        }
    }
}

