Created
March 16, 2012 16:53
-
-
Save peterdm/2051081 to your computer and use it in GitHub Desktop.
Implementing Observer pattern in PHP for an unkempt social network -- refactored hundreds of inconsistant ad-hoc calls into a handful of concrete classes bound declaratively via a config file. Net effect: Consistency and easy centralized maintenance.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
uses_model('top/user_object'); | |
uses_model('message/message'); | |
uses_model('notifications/notification_queue'); | |
uses_model('notifications/subscription'); | |
/** | |
* Notifier is one of several Observers. Each Observer takes action across a particular channel. | |
* Notifier queues messages for delivery to users' mailboxes within the social network. | |
* | |
* (Other observers: Twitterer, ActivityLogger, Flasher) | |
*/ | |
class Notifier extends Observer { | |
/* | |
* Update - Implementation of the abstract method defined by the Observer class | |
*/ | |
public function update($object, $event, &$data) { | |
trace('Observer', 'Notifier called with ' . get_class($object) . '/' . $event); | |
$subscription = $this->get_subscription($object); | |
$data['subscription'] = $subscription; | |
if ((!$subscription || $subscription->notification_type_id == 1) && method_exists($this, $event)) { | |
return $this->{$event}($object, $data); | |
} | |
} | |
/* | |
* Subscriptions are unique to Notifier -- allows users to opt-in/out of various event notifications | |
*/ | |
protected function get_subscription($object) { | |
if (!isset($this->params['subscription'])) | |
return null; // no restriction, no worries | |
if ($object instanceof Profile) | |
$recipient = $object; | |
elseif ($object->owner instanceof Profile) | |
$recipient = $object->owner; | |
else | |
return null; // we only store these notification settings for users at this point | |
// Check to see what this recipient has chosen for the preference named in the eventmap | |
return $recipient->get_subscription_for_text_id($this->params['subscription']); | |
} | |
/* --------------- CUSTOM NOTIFICATION HANDLERS --------------- */ | |
/* | |
* Construct/queue a message alerting user to the arrival of a new message | |
*/ | |
private function new_mail($object, &$data) { | |
if ($object instanceof MailboxMessage) { | |
$recipient_id = $object->to_object_id; | |
$sender_id = $object->message->profile_id; | |
$param_1 = $object->id; // the mailboxmessage id | |
NotificationQueue::AddNotification($this->params['subscription'], $recipient_id, $sender_id, $param_1); | |
} | |
} | |
/* | |
* Construct/queue a message alerting user that another user wants to connect | |
*/ | |
private function connection_invite($object, &$data) { | |
if ($object instanceof MailboxMessage) { | |
$recipient_id = $object->to_object_id; | |
$sender_id = $object->message->profile_id; | |
$param_1 = $object->id; // the mailboxmessage id | |
NotificationQueue::AddNotification($this->params['subscription'], $recipient_id, $sender_id, $param_1); | |
} | |
} | |
/* | |
* Construct/queue a message alerting user that another user has invited them to collaborate on a project | |
*/ | |
private function project_invite($object, &$data) { | |
if ($object instanceof MailboxMessage) { | |
$recipient_id = $object->to_object_id; | |
$sender_id = $object->message->profile_id; | |
$param_1 = $object->id; // the mailboxmessage id | |
NotificationQueue::AddNotification($this->params['subscription'], $recipient_id, $sender_id, $param_1); | |
} | |
} | |
/* | |
* Construct/queue a message alerting user that someone has applied to their job posting | |
*/ | |
private function job_application($object, &$data) { | |
if ($object instanceof MailboxMessage) { | |
$recipient_id = $object->to_object_id; | |
$sender_id = $object->message->profile_id; | |
$param_1 = $object->mailbox->object_id; // the cast or crew listing | |
$param_2 = $object->id; | |
NotificationQueue::AddNotification($this->params['subscription'], $recipient_id, $sender_id, $param_1, $param_2); | |
} | |
} | |
/* | |
* Construct/queue a message alerting user another user has commented on an item of theirs | |
*/ | |
private function comment($object, &$data) { | |
$comment = $data['comment']; | |
$recipient_id = $object->profile_id; // the owner of the object being commented on | |
$sender_id = $comment->profile_id; | |
$param_1 = $comment->id; | |
$param_2 = $object->id; // the id of the object being commented on | |
NotificationQueue::AddNotification($this->params['subscription'], $recipient_id, $sender_id, $param_1, $param_2); | |
} | |
/* | |
* Construct/queue a message alerting user another user has reviewed an item of theirs | |
*/ | |
private function review($object, &$data) { | |
$review = $data['review']; | |
$recipient_id = $object->profile_id; // the owner of the object being reviewed | |
$sender_id = $review->profile_id; | |
$param_1 = $review->id; | |
$param_2 = $object->id; // the id of the object being reviewed | |
NotificationQueue::AddNotification($this->params['subscription'], $recipient_id, $sender_id, $param_1, $param_2); | |
} | |
/* | |
* Construct/queue a message alerting user another user has accepted their invitation | |
*/ | |
private function accepted($object, &$data) { | |
if ($object instanceof Invite) { | |
$invitee = $data['invitee']; | |
$invitor = $data['invitor']; | |
$recipient_id = $invitor->id; // we're actually notifying the sender of the invite that it was accepted | |
$sender_id = $invitee->id; | |
$param_1 = $object->id; | |
NotificationQueue::AddNotification($this->params['subscription'], $recipient_id, $sender_id, $param_1); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Observer class - An Observer receives update messages from ObservableModel instances | |
* via the ->notify($event, $data) method | |
*/ | |
abstract class Observer { | |
protected $params = array(); | |
public function __construct($params = null) { | |
if ($params) | |
$this->params = $params; | |
} | |
abstract public function update($observable, $event, &$data); | |
} | |
/** | |
* Abstract observable superclass (observers attach to this) | |
* | |
* 1) Each ObservableModel subclass maintains a list of Observers listening for specific events. | |
* | |
* 2) When an instance of ObservableModel calls it's ->notify($event) method, all Observers | |
* registered to listen for this particular event receive a callback to their ->update() method. | |
* | |
*/ | |
class ObservableModel extends Model { | |
/** | |
* An array of Observer objects to notify | |
* | |
* @access private | |
* @var array | |
*/ | |
var $_observers = array(); | |
/** | |
* Constructor | |
*/ | |
function __construct($id = null, $fields = null, $db = null, $row = null, $cacheable = false) { | |
$this->_observers = array(); | |
// Load observers from observers.conf (e.g. ActivityLogger, Notifier, Messenger) | |
$config = Config::Get('observers/eventmap'); | |
/* Eventmap format | |
* --------------- | |
* | |
* <class_name>: (use instanceof to check so we can include superclasses here) | |
* - event: <e.g. post_create, accept, ignore, decline> | |
* observers: | |
* - class: <e.g. ActivityLogger, Notifier, Twitterer> | |
* params: {<optional map of any parameters we want to pass>} | |
* | |
* | |
* Sample mapping | |
* -------------- | |
* Project: | |
* - event: rate | |
* observers: | |
* - class: ActivityLogger | |
* | |
* - event: comment | |
* observers: | |
* - class: Notifier | |
* params: {subscription: itemcomment} | |
* | |
* - class: ActivityLogger | |
* | |
* # | |
* # This sample config will: | |
* # - Trigger a post on the Project owner's "activity log" (wall) whenever | |
* # someone rates or comments on their project. | |
* # | |
* # - Send a message to the Project owner's mailbox with the comment | |
* # provided they're opted into this type of alert. | |
* # | |
*/ | |
// autoload observers | |
$conf = Config::Get('observers'); | |
if ($conf->auto) { | |
foreach ($conf->auto->items as $obs) | |
uses("observer.$obs"); | |
foreach ($config->items as $observable_class => $event_observers) { | |
if ($this instanceof $observable_class) { | |
foreach ($event_observers->items as $event) { | |
foreach ($event->observers->items as $item) { | |
$params = $item->params; | |
$parameters = ($params) ? $params->items : null; | |
$instance = new $item->class($parameters); | |
$this->attach($instance, $event->event); | |
} | |
} | |
} | |
} | |
} | |
parent::__construct($id, $fields, $db, $row, $cacheable); | |
} | |
/** | |
* Update each attached observer object and return an array of their return values | |
* | |
* @access public | |
* @return array Array of return values from the observers | |
*/ | |
function notify($event, $data = null) { | |
$return = array(); | |
$data = ($data) ? $data : array(); | |
// Iterate through the _observers array | |
if (isset($this->_observers[$event])) { | |
foreach ($this->_observers[$event] as $observer) { | |
$return[] = $observer->update($this, $event, $data); | |
} | |
} | |
return $return; | |
} | |
/** | |
* Attach an observer object | |
* | |
* @access public | |
* @param object $observer An observer object to attach | |
* @return void | |
*/ | |
function attach(&$observer, $event) { | |
trace(get_class($this), 'Attached observer ' . get_class($observer) . ' for event ' . $event); | |
// Make sure we haven't already attached this object as an observer | |
if (is_object($observer)) { | |
$class = get_class($observer); | |
if (isset($this->_observers[$event])) | |
foreach ($this->_observers[$event] as $check) | |
if (is_a($check, $class)) | |
return; | |
if (!isset($this->_observers[$event])) | |
$this->_observers[$event] = array(); | |
$this->_observers[$event][] = $observer; | |
} | |
} | |
/** | |
* Detach an observer object | |
* | |
* @access public | |
* @param object $observer An observer object to detach | |
* @return boolean True if the observer object was detached | |
*/ | |
function detach($observer, $event) { | |
// Initialize variables | |
$retval = false; | |
if (isset($this->_observers[$event])) { | |
$key = array_search($observer, $this->_observers[$event]); | |
if ($key !== false) { | |
unset($this->_observers[$event][$key]); | |
$retval = true; | |
} | |
} | |
return $retval; | |
} | |
/* | |
* The following methods are all events raised during the persistance lifecycle of a Model instance. | |
* | |
* They are being overridden here in ObservableModel to expose these lifecycle events to Observers | |
* that want to listen for them. | |
*/ | |
protected function pre_create(&$fields) { | |
parent::pre_create($fields); | |
$this->notify(__FUNCTION__); | |
} | |
protected function post_create() { | |
parent::post_create(); | |
$this->notify(__FUNCTION__); | |
} | |
protected function pre_read() { | |
parent::pre_read(); | |
$this->notify(__FUNCTION__); | |
} | |
protected function post_read() { | |
parent::post_read(); | |
$this->notify(__FUNCTION__); | |
} | |
protected function pre_update(&$fields) { | |
parent::pre_update($fields); | |
$this->notify(__FUNCTION__); | |
} | |
protected function post_update($fields) { | |
parent::post_update($fields); | |
$this->notify(__FUNCTION__); | |
} | |
protected function pre_delete() { | |
parent::pre_delete(); | |
$this->notify(__FUNCTION__); | |
} | |
protected function post_delete() { | |
parent::post_delete(); | |
$this->notify(__FUNCTION__); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment