diff --git a/include/class.signal.php b/include/class.signal.php new file mode 100644 index 0000000000000000000000000000000000000000..86540054ed6d4d703b6f5e9175067298c51fa428 --- /dev/null +++ b/include/class.signal.php @@ -0,0 +1,93 @@ +<?php +/********************************************************************* + class.signal.php + + Simple interface for a publish and subscribe signal model + + Jared Hancock <jared@osticket.com> + Copyright (c) 2006-2013 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +/** + * Signals implement a simple publish/subscribe event model. To keep things + * simplistic between classes and to maintain compatible with PHP version 4, + * signals will not be explicitly defined or registered. Instead, signals + * are connected to callbacks via a string signal name. + * + * The system is proofed with a static inspection test which will ensure + * that for every given Signal::connect() function call, somewhere else in + * the codebase there exists a Signal::send() for the same named signal. + */ +class Signal { + /** + * Subscribe to a signal. + * + * Signal::connect('user.auth', 'function'); + * + * The subscribed function should receive a two arguments and will have + * this signature: + * + * function callback($object, $data); + * + * Where the $object argument is the object originating the signal, and + * the $options is a hash-array of other information originating from- + * and pertaining to the signal. + * + * The value of the $data argument is not defined. It is signal + * specific. It should be a hash-array of data; however, no runtime + * checks are made to ensure such an interface. + * + * Optionally, if $object is a class and is passed into the ::connect() + * method, only instances of the named class or subclass will actually + * be connected to the callable function. + */ + /*static*/ function connect($signal, $callable, $object=null) { + global $_subscribers; + if (!isset($_subscribers[$signal])) $_subscribers[$signal] = array(); + // XXX: Ensure $object if set is a class + if ($object && !is_string($object)) + trigger_error("Invalid object: $object: Expected class"); + $_subscribers[$signal][] = array($object, $callable); + } + + /** + * Publish a signal. + * + * Signal::send('user.login', $this, array('username'=>'blah')); + * + * All subscribers to the signal will be called in the order they + * connect()ed to the signal. Subscribers do not have the opportunity to + * interrupt or discontinue delivery of the signal to other subscribers. + * The $object argument is required and should almost always be ($this). + * Its interpretation is the object originating or sending the signal. + * It could also be interpreted as the context of the signal. + * + * $data if sent should be a hash-array of data included with the signal + * event. There is otherwise no definition for what should or could be + * included in the $data array. The received data is received by + * reference and can be passed to the callable by reference if the + * callable is defined to receive it by reference. Therefore, it is + * possible to propogate changes in the signal handlers back to the + * originating context. + */ + /*static*/ function send($signal, $object, &$data=null) { + global $_subscribers; + if (!isset($_subscribers[$signal])) + return; + foreach ($_subscribers[$signal] as $sub) { + list($s, $callable) = $sub; + if ($s && !is_a($object, $s)) + continue; + call_user_func($callable, $data); + } + } +} + +$_subscribers = array(); +?> diff --git a/include/class.staff.php b/include/class.staff.php index 414c835cccd07522626eef9ab0f2f9fa573da32d..4dc2f6bf4b1724d16b067be77a3244baad387d49 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -501,6 +501,8 @@ class Staff { $this->updateTeams($vars['teams']); $this->reload(); + + Signal::send('model.modified', $this); return true; } @@ -520,6 +522,8 @@ class Staff { db_query('DELETE FROM '.TEAM_MEMBER_TABLE.' WHERE staff_id='.db_input($id)); } + Signal::send('model.deleted', $this); + return $num; } @@ -613,9 +617,14 @@ class Staff { //Destroy old session ID - needed for PHP version < 5.1.0 TODO: remove when we move to php 5.3 as min. requirement. if(($session=$ost->getSession()) && is_object($session) && $sid!=session_id()) $session->destroy($sid); + + Signal::send('auth.login.succeeded', $user); return $user; } + + Signal::send('auth.login.failed', null, array('username'=>$username, + 'password'=>$passwd)); //If we get to this point we know the login failed. $_SESSION['_staff']['strikes']+=1; @@ -637,8 +646,10 @@ class Staff { } function create($vars, &$errors) { - if(($id=self::save(0, $vars, $errors)) && $vars['teams'] && ($staff=Staff::lookup($id))) + if(($id=self::save(0, $vars, $errors)) && $vars['teams'] && ($staff=Staff::lookup($id))) { $staff->updateTeams($vars['teams']); + Signal::send('model.created', $staff); + } return $id; } diff --git a/setup/test/tests/test.signals.php b/setup/test/tests/test.signals.php new file mode 100644 index 0000000000000000000000000000000000000000..7ce888383ab0aee781450f3f908d8c4a36d96ae1 --- /dev/null +++ b/setup/test/tests/test.signals.php @@ -0,0 +1,47 @@ +<?php +require_once "class.test.php"; + +class SignalsTest extends Test { + var $name = "Signals checks"; + + /** + * Ensures that each signal subscribed to has a sender somewhere else + */ + function testFindSignalPublisher() { + $scripts = $this->getAllScripts(); + $matches = $published_signals = array(); + foreach ($scripts as $s) + if (preg_match_all("/^ *Signal::send\('([^']+)'/m", + file_get_contents($s), $matches, PREG_SET_ORDER)) + foreach ($matches as $match) + $published_signals[] = $match[1]; + foreach ($scripts as $s) { + if (preg_match_all("/^ *Signal::connect\('([^']+)'/m", + file_get_contents($s), $matches, + PREG_OFFSET_CAPTURE|PREG_SET_ORDER) > 0) { + foreach ($matches as $match) { + $match = $match[1]; + if (!in_array($match[0], $published_signals)) + $this->fail( + $s, self::line_number_for_offset($s, $match[1]), + "Signal '{$match[0]}' is never sent"); + else + $this->pass(); + } + } + } + } + + function line_number_for_offset($filename, $offset) { + $lines = file($filename); + $bytes = $line = 0; + while ($bytes < $offset) { + $bytes += strlen(array_shift($lines)); + $line += 1; + } + return $line; + } +} + +return 'SignalsTest'; +?>