2014-08-08 15:06:11 -05:00
< ? php namespace DCarbone ;
use DCarbone\Helpers\FileHelper ;
/**
* Class UglyQueue
* @ package DCarbone
2014-09-29 16:26:53 -05:00
*
* @ property string name
* @ property string path
* @ property bool locked
2014-08-08 15:06:11 -05:00
*/
2014-09-29 16:26:53 -05:00
class UglyQueue implements \Serializable , \SplSubject
2014-08-08 15:06:11 -05:00
{
2014-09-29 16:26:53 -05:00
const NOTIFY_QUEUE_INITIALIZED = 0 ;
const NOTIFY_QUEUE_LOCKED = 1 ;
const NOTIFY_QUEUE_FAILED_TO_LOCK = 2 ;
const NOTIFY_QUEUE_LOCKED_BY_OTHER_PROCESS = 3 ;
const NOTIFY_QUEUE_UNLOCKED = 4 ;
const NOTIFY_QUEUE_PROCESSING = 5 ;
const NOTIFY_QUEUE_REACHED_END = 6 ;
/** @var int */
public $notifyStatus ;
const QUEUE_READONLY = 0 ;
const QUEUE_READWRITE = 1 ;
2014-08-08 15:06:11 -05:00
/** @var array */
2014-09-29 16:26:53 -05:00
private $observers = array ();
2014-08-08 15:06:11 -05:00
2014-09-29 16:26:53 -05:00
/** @var int */
protected $mode = null ;
2014-08-08 15:06:11 -05:00
/** @var string */
2014-09-29 16:26:53 -05:00
protected $_name ;
2014-08-08 15:06:11 -05:00
/** @var string */
2014-09-29 16:26:53 -05:00
protected $_path ;
2014-08-08 15:06:11 -05:00
/** @var bool */
2014-09-29 16:26:53 -05:00
protected $_locked = false ;
2014-08-08 15:06:11 -05:00
/** @var resource */
protected $_tmpHandle ;
/**
2014-09-29 16:26:53 -05:00
* @ param string $directoryPath
* @ param array $observers
2014-08-08 15:06:11 -05:00
* @ throws \RuntimeException
* @ throws \InvalidArgumentException
2014-09-29 16:26:53 -05:00
* @ return UglyQueue
2014-08-08 15:06:11 -05:00
*/
2014-09-29 17:02:28 -05:00
public static function queueWithDirectoryPathAndObservers ( $directoryPath , array $observers = array ())
2014-08-08 15:06:11 -05:00
{
2014-09-29 16:26:53 -05:00
if ( ! is_string ( $directoryPath ))
throw new \InvalidArgumentException ( 'Argument 1 expected to be string, ' . gettype ( $directoryPath ) . ' seen' );
if (( $directoryPath = trim ( $directoryPath )) === '' )
throw new \InvalidArgumentException ( 'Empty string passed for argument 1' );
if ( file_exists ( $directoryPath ))
{
if ( ! is_dir ( $directoryPath ))
throw new \RuntimeException ( 'Argument 1 expected to be path to directory, path to non-directory seen' );
}
else if ( !@ mkdir ( $directoryPath ))
{
throw new \RuntimeException ( 'Unable to create queue directory at path: "' . $directoryPath . '".' );
}
$uglyQueue = new UglyQueue ();
$uglyQueue -> observers = $observers ;
2014-09-29 17:02:28 -05:00
$split = preg_split ( '#[/\\\]+#' , $directoryPath );
2014-08-08 15:06:11 -05:00
2014-09-29 16:26:53 -05:00
$uglyQueue -> _name = end ( $split );
$uglyQueue -> _path = rtrim ( realpath ( implode ( DIRECTORY_SEPARATOR , $split )), " / \\ " ) . DIRECTORY_SEPARATOR ;
2014-08-08 15:06:11 -05:00
2014-09-29 16:26:53 -05:00
if ( is_writable ( $uglyQueue -> _path ))
$uglyQueue -> mode = self :: QUEUE_READWRITE ;
else if ( is_readable ( $uglyQueue -> _path ))
$uglyQueue -> mode = self :: QUEUE_READONLY ;
2014-08-10 10:52:59 -05:00
2014-09-29 16:26:53 -05:00
// Insert "don't look here" index.html file
if ( ! file_exists ( $uglyQueue -> _path . 'index.html' ))
{
if ( $uglyQueue -> mode === self :: QUEUE_READONLY )
throw new \RuntimeException ( 'Cannot initialize queue with name "' . $uglyQueue -> _name . '", the user lacks permission to create files.' );
$html = <<< HTML
< html >
< head >
< title > 403 Forbidden </ title >
</ head >
< body >
< p > Directory access is forbidden .</ p >
</ body >
</ html >
HTML ;
file_put_contents ( $uglyQueue -> _path . 'index.html' , $html );
}
if ( ! file_exists ( $uglyQueue -> _path . 'queue.txt' ))
{
if ( $uglyQueue -> mode === self :: QUEUE_READONLY )
throw new \RuntimeException ( 'Cannot initialize queue with name "' . $uglyQueue -> _name . '", the user lacks permission to create files.' );
file_put_contents ( $uglyQueue -> _path . 'queue.txt' , '' );
}
$uglyQueue -> notifyStatus = self :: NOTIFY_QUEUE_INITIALIZED ;
$uglyQueue -> notify ();
return $uglyQueue ;
}
/**
* @ param string $param
* @ return string
* @ throws \OutOfBoundsException
*/
public function __get ( $param )
{
switch ( $param )
{
case 'name' :
return $this -> _name ;
case 'path' :
return $this -> _path ;
case 'locked' :
return $this -> _locked ;
default :
throw new \OutOfBoundsException ( get_class ( $this ) . ' does not have a property named "' . $param . '".' );
}
2014-08-08 15:06:11 -05:00
}
/**
* Destructor
*/
public function __destruct ()
{
$this -> _populateQueue ();
2014-09-29 16:26:53 -05:00
$this -> unlock ();
file_put_contents ( $this -> _path . UglyQueueManager :: UGLY_QUEUE_SERIALIZED_NAME , serialize ( $this ));
2014-08-08 15:06:11 -05:00
}
/**
* @ param int $ttl Time to live in seconds
2014-08-10 11:46:06 -05:00
* @ throws \InvalidArgumentException
2014-08-08 15:06:11 -05:00
* @ return bool
*/
public function lock ( $ttl = 250 )
{
2014-08-10 11:46:06 -05:00
if ( ! is_int ( $ttl ))
2014-09-29 16:26:53 -05:00
throw new \InvalidArgumentException ( 'Argument 1 expected to be integer, "' . gettype ( $ttl ) . '" seen' );
2014-08-10 11:46:06 -05:00
if ( $ttl < 0 )
2014-09-29 16:26:53 -05:00
throw new \InvalidArgumentException ( 'Argument 1 expected to be positive integer, "' . $ttl . '" seen' );
2014-08-10 11:46:06 -05:00
2014-09-29 16:26:53 -05:00
$alreadyLocked = $this -> isLocked ();
2014-08-08 15:06:11 -05:00
2014-09-29 16:26:53 -05:00
// If there is currently no lock
if ( $alreadyLocked === false )
return $this -> createLockFile ( $ttl );
2014-08-08 15:06:11 -05:00
// If we make it this far, there is already a lock in place.
2014-09-29 16:26:53 -05:00
$this -> _locked = false ;
$this -> notifyStatus = self :: NOTIFY_QUEUE_LOCKED_BY_OTHER_PROCESS ;
$this -> notify ();
return false ;
2014-08-08 15:06:11 -05:00
}
/**
* @ param int $ttl seconds to live
* @ return bool
*/
2014-09-29 16:26:53 -05:00
protected function createLockFile ( $ttl )
2014-08-08 15:06:11 -05:00
{
$ok = ( bool ) @ file_put_contents (
2014-09-29 16:26:53 -05:00
$this -> _path . 'queue.lock' ,
2014-08-08 15:06:11 -05:00
json_encode ( array ( 'ttl' => $ttl , 'born' => time ())));
if ( $ok !== true )
2014-09-29 16:26:53 -05:00
{
$this -> notifyStatus = self :: NOTIFY_QUEUE_FAILED_TO_LOCK ;
$this -> notify ();
return $this -> _locked = false ;
}
$this -> _locked = true ;
$this -> notifyStatus = self :: NOTIFY_QUEUE_LOCKED ;
$this -> notify ();
2014-08-08 15:06:11 -05:00
return true ;
}
/**
2014-08-08 17:27:21 -05:00
* Close this ugly queue , writing out contents to file .
2014-08-08 15:06:11 -05:00
*/
public function unlock ()
{
2014-09-29 16:26:53 -05:00
if ( $this -> _locked === true )
2014-08-08 15:06:11 -05:00
{
2014-09-29 16:26:53 -05:00
unlink ( $this -> _path . 'queue.lock' );
$this -> _locked = false ;
$this -> notifyStatus = self :: NOTIFY_QUEUE_UNLOCKED ;
$this -> notify ();
2014-08-08 15:06:11 -05:00
}
}
/**
2014-08-10 11:46:06 -05:00
* @ throws \RuntimeException
2014-08-08 15:06:11 -05:00
* @ return bool
*/
public function isLocked ()
{
// First check for lock file
2014-09-29 16:26:53 -05:00
if ( is_file ( $this -> _path . 'queue.lock' ))
2014-08-08 15:06:11 -05:00
{
2014-09-29 16:26:53 -05:00
$lock = json_decode ( file_get_contents ( $this -> _path . 'queue.lock' ), true );
2014-08-08 15:06:11 -05:00
2014-09-29 17:02:28 -05:00
// If we have an invalid lock structure.
2014-08-08 15:06:11 -05:00
if ( ! isset ( $lock [ 'ttl' ]) || ! isset ( $lock [ 'born' ]))
2014-09-29 17:02:28 -05:00
throw new \RuntimeException ( 'Invalid "queue.lock" file structure seen at "' . $this -> _path . 'queue.lock".' );
2014-08-08 15:06:11 -05:00
2014-08-10 12:28:26 -05:00
// Otherwise...
2014-08-08 15:06:11 -05:00
$lock_ttl = (( int ) $lock [ 'born' ] + ( int ) $lock [ 'ttl' ]);
// If we're within the TTL of the lock, assume another thread is already processing.
// We'll pick it up on the next go around.
if ( $lock_ttl > time ())
return true ;
// Else, remove lock file and assume we're good to go!
2014-09-29 16:26:53 -05:00
unlink ( $this -> _path . 'queue.lock' );
2014-08-08 15:06:11 -05:00
return false ;
}
// If no file, assume not locked.
return false ;
}
/**
* @ param int $count
* @ throws \RuntimeException
2014-08-10 14:27:00 -05:00
* @ throws \InvalidArgumentException
2014-08-08 15:06:11 -05:00
* @ return bool | array
*/
public function processQueue ( $count = 1 )
{
2014-09-29 16:26:53 -05:00
if ( $this -> mode === self :: QUEUE_READONLY )
2014-09-29 17:18:14 -05:00
throw new \RuntimeException ( 'Queue "' . $this -> _name . '" cannot be processed. It was started in Read-Only mode (the user running this process does not have permission to write to the queue directory).' );
2014-08-08 15:06:11 -05:00
// If we don't have a lock, assume issue and move on.
2014-09-29 16:26:53 -05:00
if ( $this -> _locked === false )
throw new \RuntimeException ( 'Cannot process queue named "' . $this -> _name . '". It is locked by another process.' );
2014-08-10 14:27:00 -05:00
// If non-int valid is passed
if ( ! is_int ( $count ))
2014-09-29 16:26:53 -05:00
throw new \InvalidArgumentException ( 'Argument 1 expected to be integer greater than 0, "' . gettype ( $count ) . '" seen' );
2014-08-10 14:27:00 -05:00
// If negative integer passed
if ( $count <= 0 )
2014-09-29 16:26:53 -05:00
throw new \InvalidArgumentException ( 'Argument 1 expected to be integer greater than 0, "' . $count . '" seen' );
if ( $this -> notifyStatus !== self :: NOTIFY_QUEUE_PROCESSING )
{
$this -> notifyStatus = self :: NOTIFY_QUEUE_PROCESSING ;
$this -> notify ();
}
2014-08-08 15:06:11 -05:00
// Find number of lines in the queue file
2014-09-29 16:26:53 -05:00
$lineCount = FileHelper :: getLineCount ( $this -> _path . 'queue.txt' );
2014-08-08 15:06:11 -05:00
// If queue line count is 0, assume empty
2014-08-10 13:56:15 -05:00
if ( $lineCount === 0 )
2014-08-08 15:06:11 -05:00
return false ;
// Try to open the file for reading / writing.
2014-09-29 16:26:53 -05:00
$queueFileHandle = fopen ( $this -> _path . 'queue.txt' , 'r+' );
2014-08-10 13:56:15 -05:00
if ( $queueFileHandle === false )
2014-08-08 15:06:11 -05:00
$this -> unlock ();
// Get an array of the oldest $count data in the queue
$data = array ();
2014-08-10 13:56:15 -05:00
$start_line = $lineCount - $count ;
2014-08-08 15:06:11 -05:00
$i = 0 ;
2014-08-10 13:56:15 -05:00
while (( $line = fscanf ( $queueFileHandle , " %s \t %s \n " )) !== false && $i < $lineCount )
2014-08-08 15:06:11 -05:00
{
if ( $i ++ >= $start_line )
{
list ( $key , $value ) = $line ;
$data [ $key ] = $value ;
}
}
// If we have consumed the rest of the file
2014-08-10 13:56:15 -05:00
if ( $count >= $lineCount )
2014-08-08 15:06:11 -05:00
{
2014-08-10 13:56:15 -05:00
rewind ( $queueFileHandle );
ftruncate ( $queueFileHandle , 0 );
fclose ( $queueFileHandle );
2014-09-29 16:26:53 -05:00
$this -> notifyStatus = self :: NOTIFY_QUEUE_REACHED_END ;
$this -> notify ();
2014-08-08 15:06:11 -05:00
}
// Otherwise, create new queue file minus the processed lines.
else
{
2014-09-29 16:26:53 -05:00
$tmp = fopen ( $this -> _path . 'queue.tmp' , 'w+' );
2014-08-10 13:56:15 -05:00
rewind ( $queueFileHandle );
2014-08-08 15:06:11 -05:00
$i = 0 ;
2014-08-10 13:56:15 -05:00
while (( $line = fgets ( $queueFileHandle )) !== false && $i < $start_line )
2014-08-08 15:06:11 -05:00
{
if ( $line !== " \n " || $line !== " " )
fwrite ( $tmp , $line );
$i ++ ;
}
2014-08-10 13:56:15 -05:00
fclose ( $queueFileHandle );
2014-08-08 15:06:11 -05:00
fclose ( $tmp );
2014-09-29 16:26:53 -05:00
unlink ( $this -> _path . 'queue.txt' );
rename ( $this -> _path . 'queue.tmp' , $this -> _path . 'queue.txt' );
2014-08-08 15:06:11 -05:00
}
return $data ;
}
/**
* @ param string $key
* @ param string | array $value
* @ return bool
* @ throws \RuntimeException
*/
public function addToQueue ( $key , $value )
{
2014-09-29 16:26:53 -05:00
if ( $this -> mode === self :: QUEUE_READONLY )
throw new \RuntimeException ( 'Cannot add items to queue "' . $this -> _name . '" as it is in read-only mode' );
2014-08-08 15:06:11 -05:00
// If we don't have a lock, assume issue and move on.
2014-09-29 16:26:53 -05:00
if ( $this -> _locked === false )
throw new \RuntimeException ( 'Cannot add items to queue "' . $this -> _name . '". Queue is already locked by another process' );
2014-08-08 15:06:11 -05:00
if ( ! is_resource ( $this -> _tmpHandle ))
{
2014-09-29 16:26:53 -05:00
$this -> _tmpHandle = fopen ( $this -> _path . 'queue.tmp' , 'w+' );
2014-08-08 15:06:11 -05:00
if ( $this -> _tmpHandle === false )
2014-09-29 16:26:53 -05:00
throw new \RuntimeException ( 'Unable to create "queue.tmp" file.' );
2014-08-08 15:06:11 -05:00
}
if ( is_array ( $value ) || $value instanceof \stdClass )
$value = json_encode ( $value );
return ( bool ) fwrite (
$this -> _tmpHandle ,
$key . " \t " . str_replace ( array ( " \r \n " , " \n " ), ' ' , $value )
. " \n " );
}
/**
* If there is a tmp queue file , add it ' s contents to the beginning of a new queue file
*
* @ return void
*/
2014-08-10 13:56:15 -05:00
public function _populateQueue ()
2014-08-08 15:06:11 -05:00
{
if ( is_resource ( $this -> _tmpHandle ))
{
2014-09-29 16:26:53 -05:00
if ( file_exists ( $this -> _path . 'queue.txt' ))
2014-08-08 15:06:11 -05:00
{
2014-09-29 16:26:53 -05:00
$queueFileHandle = fopen ( $this -> _path . 'queue.txt' , 'r+' );
2014-08-10 13:56:15 -05:00
while (( $line = fgets ( $queueFileHandle )) !== false )
2014-08-08 15:06:11 -05:00
{
if ( $line !== " \n " && $line !== " " )
fwrite ( $this -> _tmpHandle , $line );
}
2014-08-10 13:56:15 -05:00
fclose ( $queueFileHandle );
2014-09-29 16:26:53 -05:00
unlink ( $this -> _path . 'queue.txt' );
2014-08-08 15:06:11 -05:00
}
fclose ( $this -> _tmpHandle );
2014-09-29 16:26:53 -05:00
rename ( $this -> _path . 'queue.tmp' , $this -> _path . 'queue.txt' );
2014-08-08 15:06:11 -05:00
}
}
2014-08-08 18:09:45 -05:00
/**
2014-08-10 13:56:15 -05:00
* @ return int
2014-08-08 18:09:45 -05:00
* @ throws \RuntimeException
*/
public function getQueueItemCount ()
{
2014-09-29 16:26:53 -05:00
return FileHelper :: getLineCount ( $this -> _path . 'queue.txt' );
2014-08-08 18:09:45 -05:00
}
/**
* @ param string $key
* @ return bool
* @ throws \RuntimeException
*/
public function keyExistsInQueue ( $key )
{
$key = ( string ) $key ;
// Try to open the file for reading / writing.
2014-09-29 16:26:53 -05:00
$queueFileHandle = fopen ( $this -> _path . 'queue.txt' , 'r' );
2014-08-08 18:09:45 -05:00
2014-09-29 17:02:28 -05:00
while (( $line = fscanf ( $queueFileHandle , " %s \t %s \n " )) !== false )
2014-08-08 18:09:45 -05:00
{
2014-09-29 16:26:53 -05:00
list ( $lineKey , $lineValue ) = $line ;
2014-08-10 13:56:15 -05:00
2014-09-29 16:26:53 -05:00
if ( $key === $lineKey )
2014-08-08 18:09:45 -05:00
{
2014-08-10 13:56:15 -05:00
fclose ( $queueFileHandle );
2014-08-08 18:09:45 -05:00
return true ;
}
}
2014-08-10 13:56:15 -05:00
fclose ( $queueFileHandle );
2014-08-08 18:09:45 -05:00
return false ;
}
2014-08-10 10:52:59 -05:00
2014-08-11 12:24:05 -05:00
/**
2014-09-29 16:26:53 -05:00
* ( PHP 5 >= 5.1 . 0 )
* String representation of object
* @ link http :// php . net / manual / en / serializable . serialize . php
*
* @ return string the string representation of the object or null
2014-08-11 12:42:17 -05:00
*/
2014-09-29 16:26:53 -05:00
public function serialize ()
2014-08-11 12:42:17 -05:00
{
2014-09-29 16:26:53 -05:00
return serialize ( array ( $this -> _name , $this -> _path ));
2014-08-11 12:24:05 -05:00
}
2014-08-10 10:52:59 -05:00
/**
2014-09-29 16:26:53 -05:00
* ( PHP 5 >= 5.1 . 0 )
* Constructs the object
* @ link http :// php . net / manual / en / serializable . unserialize . php
*
* @ param string $serialized The string representation of the object .
* @ return void
2014-08-10 10:52:59 -05:00
*/
2014-09-29 16:26:53 -05:00
public function unserialize ( $serialized )
2014-08-10 10:52:59 -05:00
{
2014-09-29 16:26:53 -05:00
/** @var \DCarbone\UglyQueue $uglyQueue */
$data = unserialize ( $serialized );
$this -> _name = $data [ 0 ];
$this -> _path = $data [ 1 ];
2014-08-10 10:52:59 -05:00
}
/**
2014-09-29 16:26:53 -05:00
* ( PHP 5 >= 5.1 . 0 )
* Attach an SplObserver
* @ link http :// php . net / manual / en / splsubject . attach . php
*
* @ param \SplObserver $observer The SplObserver to attach .
* @ return void
2014-08-10 10:52:59 -05:00
*/
2014-09-29 16:26:53 -05:00
public function attach ( \SplObserver $observer )
2014-08-10 10:52:59 -05:00
{
2014-09-29 16:26:53 -05:00
if ( ! in_array ( $observer , $this -> observers ))
$this -> observers [] = $observer ;
2014-08-10 10:52:59 -05:00
}
/**
2014-09-29 16:26:53 -05:00
* ( PHP 5 >= 5.1 . 0 )
* Detach an observer
* @ link http :// php . net / manual / en / splsubject . detach . php
*
* @ param \SplObserver $observer The SplObserver to detach .
* @ return void
2014-08-10 10:52:59 -05:00
*/
2014-09-29 16:26:53 -05:00
public function detach ( \SplObserver $observer )
2014-08-10 10:52:59 -05:00
{
2014-09-29 16:26:53 -05:00
$idx = array_search ( $observer , $this -> observers , true );
if ( $idx !== false )
unset ( $this -> observers [ $idx ]);
2014-08-10 10:52:59 -05:00
}
/**
2014-09-29 16:26:53 -05:00
* ( PHP 5 >= 5.1 . 0 )
* Notify an observer
* @ link http :// php . net / manual / en / splsubject . notify . php
*
* @ return void
2014-08-10 10:52:59 -05:00
*/
2014-09-29 16:26:53 -05:00
public function notify ()
2014-08-10 10:52:59 -05:00
{
2014-09-29 16:26:53 -05:00
for ( $i = 0 , $count = count ( $this -> observers ); $i < $count ; $i ++ )
{
$this -> observers [ $i ] -> notify ( $this );
}
2014-08-10 10:52:59 -05:00
}
2014-08-08 15:06:11 -05:00
}