Skip to content

Instantly share code, notes, and snippets.

@peterdm
Created March 16, 2012 16:53
Show Gist options
  • Save peterdm/2051081 to your computer and use it in GitHub Desktop.
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.
<?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);
}
}
}
<?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