jenkins-bot has submitted this change and it was merged.

Change subject: Replace EchoBackend with mappers and gateway
......................................................................


Replace EchoBackend with mappers and gateway

* Get rid of EchoBackend by separating responsibilities into smaller objects

* Move main fetchNotification logic from API to a more appropriate place

* Add more unit testing coverage

Change-Id: I42f4d7566543332588431c21c220c0d64d026b70
---
M Echo.php
M Hooks.php
M api/ApiEchoNotifications.php
M formatters/BasicFormatter.php
M formatters/PageLinkFormatter.php
A includes/DataOutputFormatter.php
D includes/DbEchoBackend.php
D includes/EchoBackend.php
M includes/EchoDbFactory.php
M includes/NotifUser.php
A includes/gateway/UserNotificationGateway.php
A includes/mapper/EventMapper.php
A includes/mapper/NotificationMapper.php
M model/Event.php
M model/Notification.php
M special/SpecialNotifications.php
A tests/includes/EchoDbFactoryTest.php
A tests/includes/gateway/UserNotificationGatewayTest.php
A tests/includes/mapper/EventMapperTest.php
A tests/includes/mapper/NotificationMapperTest.php
20 files changed, 1,363 insertions(+), 663 deletions(-)

Approvals:
  EBernhardson: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/Echo.php b/Echo.php
index 2568095..8fb2dad 100644
--- a/Echo.php
+++ b/Echo.php
@@ -47,6 +47,7 @@
 $wgExtensionMessagesFiles['Echo'] = $dir . 'Echo.i18n.php';
 $wgExtensionMessagesFiles['EchoAliases'] = $dir . 'Echo.alias.php';
 
+// Basic Echo classes
 $wgAutoloadClasses['EchoHooks'] = $dir . 'Hooks.php';
 $wgAutoloadClasses['EchoEvent'] = $dir . 'model/Event.php';
 $wgAutoloadClasses['EchoNotification'] = $dir . 'model/Notification.php';
@@ -56,7 +57,15 @@
 $wgAutoloadClasses['MWDbEchoEmailBundler'] = $dir . 
'includes/DbEmailBundler.php';
 $wgAutoloadClasses['MWEchoEventLogging'] = $dir . 'includes/EventLogging.php';
 
-// Formatters
+// Database mappers && gateways
+$wgAutoloadClasses['EchoEventMapper'] = $dir . 
'includes/mapper/EventMapper.php';
+$wgAutoloadClasses['EchoNotificationMapper'] = $dir . 
'includes/mapper/NotificationMapper.php';
+$wgAutoloadClasses['EchoUserNotificationGateway'] = $dir . 
'includes/gateway/UserNotificationGateway.php';
+
+// Output formatters
+$wgAutoloadClasses['EchoDataOutputFormatter'] = $dir . 
'includes/DataOutputFormatter.php';
+
+// Event formatters
 $wgAutoloadClasses['EchoNotificationFormatter'] = $dir . 
'formatters/NotificationFormatter.php';
 $wgAutoloadClasses['EchoBasicFormatter'] = $dir . 
'formatters/BasicFormatter.php';
 $wgAutoloadClasses['EchoEditFormatter'] = $dir . 
'formatters/EditFormatter.php';
@@ -101,8 +110,6 @@
 $wgSpecialPageGroups['Notifications'] = 'users';
 
 // Backend support
-$wgAutoloadClasses['MWEchoBackend'] = $dir . 'includes/EchoBackend.php';
-$wgAutoloadClasses['MWDbEchoBackend'] = $dir . 'includes/DbEchoBackend.php';
 $wgAutoloadClasses['MWEchoDbFactory'] = $dir . 'includes/EchoDbFactory.php';
 $wgAutoloadClasses['MWEchoNotifUser'] = $dir . 'includes/NotifUser.php';
 
@@ -248,14 +255,11 @@
 
 // Configuration
 
-// The name of the backend to use for Echo, eg, Db, Redis, Zeromq
+// The name of the backend to use for Echo email bundling and digest, it should
+// be always Db
+// @deprecated
+// @todo remove it from code base
 $wgEchoBackendName = 'Db';
-
-/**
- * The backend object
- * @var MWEchoBackend
- */
-$wgEchoBackend = null;
 
 // Whether to turn on email batch function
 $wgEchoEnableEmailBatch = true;
diff --git a/Hooks.php b/Hooks.php
index 68ab9e8..a4f703b 100644
--- a/Hooks.php
+++ b/Hooks.php
@@ -13,14 +13,11 @@
         * from $wgExtensionFunctions
         */
        public static function initEchoExtension() {
-               global $wgEchoBackend, $wgEchoBackendName, $wgEchoNotifications,
-                       $wgEchoNotificationCategories, 
$wgEchoNotificationIcons, $wgEchoConfig,
-                       $wgNotificationSenderName;
+               global $wgEchoNotifications, $wgEchoNotificationCategories, 
$wgEchoNotificationIcons,
+                       $wgEchoConfig, $wgNotificationSenderName;
 
                // allow extensions to define their own event
                wfRunHooks( 'BeforeCreateEchoEvent', array( 
&$wgEchoNotifications, &$wgEchoNotificationCategories, 
&$wgEchoNotificationIcons ) );
-
-               $wgEchoBackend = MWEchoBackend::factory( $wgEchoBackendName );
 
                // turn schema off if eventLogging is not enabled
                if ( !function_exists( 'efLogServerSideEvent' ) ) {
diff --git a/api/ApiEchoNotifications.php b/api/ApiEchoNotifications.php
index 91f0171..9e85e86 100644
--- a/api/ApiEchoNotifications.php
+++ b/api/ApiEchoNotifications.php
@@ -23,7 +23,12 @@
 
                $result = array();
                if ( in_array( 'list', $prop ) ) {
-                       $result['list'] = self::getNotifications( $user, 
$params['format'], $params['limit'] + 1, $params['continue'] );
+                       $result['list'] = array();
+                       $notifMapper = new EchoNotificationMapper( 
MWEchoDbFactory::newFromDefault() );
+                       $notifs = $notifMapper->fetchByUser( $user, 
$params['limit'] + 1, $params['continue'], 'web' );
+                       foreach ( $notifs as $notif ) {
+                               $result['list'][$notif->getEvent()->getID()] = 
EchoDataOutputFormatter::formatOutput( $notif, $params['format'], $user );
+                       }
 
                        // check if there is more elements than we request
                        if ( count( $result['list'] ) > $params['limit'] ) {
@@ -54,116 +59,6 @@
 
                $this->getResult()->setIndexedTagName( $result, 'notification' 
);
                $this->getResult()->addValue( 'query', $this->getModuleName(), 
$result );
-       }
-
-       /**
-        * Get a list of notifications based on the passed parameters
-        *
-        * @param $user User the user to get notifications for
-        * @param $format string|bool false to not format any notifications, 
string to a specific output format
-        * @param $limit int The maximum number of notifications to return
-        * @param $continue string Used for offset
-        *
-        * @return array
-        */
-       public static function getNotifications( $user, $format = false, $limit 
= 20, $continue = null ) {
-               global $wgEchoBackend;
-
-               $output = array();
-
-               // TODO: Make 'web' based on a new API param?
-               $res = $wgEchoBackend->loadNotifications( $user, $limit, 
$continue, 'web' );
-
-               foreach ( $res as $row ) {
-                       $event = EchoEvent::newFromRow( $row );
-                       if ( $row->notification_bundle_base && 
$row->notification_bundle_display_hash ) {
-                               $event->setBundleHash( 
$row->notification_bundle_display_hash );
-                       }
-
-                       $timestampMw = self::getUserLocalTime( $user, 
$row->notification_timestamp );
-
-                       // Start creating date section header
-                       $now = wfTimestamp();
-                       $dateFormat = substr( $timestampMw, 0, 8 );
-                       if ( substr( self::getUserLocalTime( $user, $now ), 0, 
8 ) === $dateFormat ) {
-                               // 'Today'
-                               $date = wfMessage( 'echo-date-today' 
)->escaped();
-                       } elseif ( substr( self::getUserLocalTime( $user, $now 
- 86400 ), 0, 8 ) === $dateFormat ) {
-                               // 'Yesterday'
-                               $date = wfMessage( 'echo-date-yesterday' 
)->escaped();
-                       } else {
-                               // 'May 10' or '10 May' (depending on user's 
date format preference)
-                               $lang = 
RequestContext::getMain()->getLanguage();
-                               $dateFormat = $lang->getDateFormatString( 
'pretty', $user->getDatePreference() ?: 'default' );
-                               $date = $lang->sprintfDate( $dateFormat, 
$timestampMw );
-                       }
-                       // End creating date section header
-
-                       $thisEvent = array(
-                               'id' => $event->getId(),
-                               'type' => $event->getType(),
-                               'category' => $event->getCategory(),
-                               'timestamp' => array(
-                                       // UTC timestamp in UNIX format used 
for loading more notification
-                                       'utcunix' => wfTimestamp( TS_UNIX, 
$row->notification_timestamp ),
-                                       'unix' => self::getUserLocalTime( 
$user, $row->notification_timestamp, TS_UNIX ),
-                                       'mw' => $timestampMw,
-                                       'date' => $date
-                               ),
-                       );
-
-                       if ( $event->getVariant() ) {
-                               $thisEvent['variant'] = $event->getVariant();
-                       }
-
-                       if ( $event->getTitle() ) {
-                               $thisEvent['title'] = array(
-                                       'full' => 
$event->getTitle()->getPrefixedText(),
-                                       'namespace' => 
$event->getTitle()->getNSText(),
-                                       'namespace-key' => 
$event->getTitle()->getNamespace(),
-                                       'text' => $event->getTitle()->getText(),
-                               );
-                       }
-
-                       if ( $event->getAgent() ) {
-                               if ( $event->userCan( Revision::DELETED_USER, 
$user ) ) {
-                                       $thisEvent['agent'] = array(
-                                               'id' => 
$event->getAgent()->getId(),
-                                               'name' => 
$event->getAgent()->getName(),
-                                       );
-                               } else {
-                                       $thisEvent['agent'] = array( 
'userhidden' => '' );
-                               }
-                       }
-
-                       if ( $row->notification_read_timestamp ) {
-                               $thisEvent['read'] = 
$row->notification_read_timestamp;
-                       }
-
-                       if ( $format ) {
-                               $thisEvent['*'] = 
EchoNotificationController::formatNotification(
-                                       $event, $user, $format );
-                       }
-
-                       $output[$event->getID()] = $thisEvent;
-               }
-
-               return $output;
-       }
-
-       /**
-        * Internal helper function for converting UTC timezone to a user's 
timezone
-        *
-        * @param $user User
-        * @param $ts string
-        * @param $format int output format
-        *
-        * @return string
-        */
-       private static function getUserLocalTime( $user, $ts, $format = TS_MW ) 
{
-               $timestamp = new MWTimestamp( $ts );
-               $timestamp->offsetForUser( $user );
-               return $timestamp->getTimestamp( $format );
        }
 
        public function getAllowedParams() {
diff --git a/formatters/BasicFormatter.php b/formatters/BasicFormatter.php
index d6a7f3c..53872e6 100644
--- a/formatters/BasicFormatter.php
+++ b/formatters/BasicFormatter.php
@@ -545,40 +545,44 @@
 
        /**
         * Get raw bundle data for an event so it can be manipulated
-        * @param $event EchoEvent
-        * @param $user User
-        * @param $type string deprecated
-        * @return ResultWrapper|bool
+        * @param EchoEvent
+        * @param User
+        * @param string deprecated
+        * @return EchoEvent[]|bool
         */
        protected function getRawBundleData( $event, $user, $type ) {
-               global $wgEchoBackend;
-
-               // We should keep bundling for events as long as it has bundle
-               // hash event for bundle-turned-off events as well, this is
-               // mainly for historical data
+               // We should keep bundling for events as long as it has bundle 
hash
+               // even for events with bundling switched to off, this is 
mainly for
+               // historical data
                if ( !$event->getBundleHash() ) {
                        return false;
                }
 
-               $data = $wgEchoBackend->getRawBundleData( $user, 
$event->getBundleHash(), $this->distributionType, 'DESC', 
self::$maxRawBundleData );
+               $eventMapper = new EchoEventMapper( 
MWEchoDbFactory::newFromDefault() );
+               $events = $eventMapper->fetchByUserBundleHash(
+                       $user, $event->getBundleHash(), 
$this->distributionType, 'DESC', self::$maxRawBundleData
+               );
 
-               if ( $data ) {
-                       $this->bundleData['raw-data-count'] += $data->numRows();
+               if ( $events ) {
+                       $this->bundleData['raw-data-count'] += count( $events );
+                       // Distribution types other than web include the base 
event
+                       // in the result already, decrement it by one
                        if ( $this->distributionType !== 'web' ) {
                                $this->bundleData['raw-data-count']--;
                        }
                }
 
-               return $data;
+               return $events;
        }
 
        /**
         * Construct the bundle data for an event, by default, the group 
iterator
         * is agent, eg, by user A and x others. custom formatter can overwrite
         * this function to use a differnt group iterator such as title, 
namespace
-        * @param $event EchoEvent
-        * @param $user User
-        * @param $type string deprecated
+        *
+        * @param EchoEvent
+        * @param User
+        * @param string deprecated
         * @throws MWException
         */
        protected function generateBundleData( $event, $user, $type ) {
@@ -606,13 +610,19 @@
 
                // Initialize with 1 for the agent of current event
                $count = 1;
-               foreach ( $data as $row ) {
-                       $key = $row->event_agent_id ? 'event_agent_id' : 
'event_agent_ip';
-                       if ( !isset( $agents[$row->$key] ) ) {
-                               $agents[$row->$key] = $row->$key;
-                               $count++;
+               foreach ( $data as $evt ) {
+                       if ( $evt->getAgent() ) {
+                               if ( $evt->getAgent()->isAnon() ) {
+                                       $key = $evt->getAgent()->getName();
+                               } else {
+                                       $key = $evt->getAgent()->getId();
+                               }
+                               if ( !isset( $agents[$key] ) ) {
+                                       $agents[$key] = $key;
+                                       $count++;
+                               }
                        }
-                       $this->bundleData['last-raw-data'] = $row;
+                       $this->bundleData['last-raw-data'] = $evt;
                }
 
                $this->bundleData['agent-other-count'] = $count - 1;
@@ -622,7 +632,7 @@
 
                // If there is more raw data than we requested, that means we 
have not
                // retrieved the very last raw record, set the key back to null
-               if ( $data->numRows() >= self::$maxRawBundleData ) {
+               if ( count( $data ) >= self::$maxRawBundleData ) {
                        $this->bundleData['last-raw-data'] = null;
                }
        }
@@ -753,7 +763,7 @@
 
                                        $data = $this->getBundleLastRawData( 
$event, $user );
                                        if ( $data ) {
-                                               $extra = $data->event_extra;
+                                               $extra = $data->getExtra();
                                                if ( isset( $extra['revid'] ) ) 
{
                                                        $oldId = 
$target->getPreviousRevisionID( $extra['revid'] );
                                                        // The diff engine 
doesn't provide a way to diff against a null revision.
@@ -773,31 +783,32 @@
        }
 
        /**
-        * Get the last bundle data in raw stdObject format. When bundling 
notifications,
+        * Get the last echo event in a set of bundling data. When bundling 
notifications,
         * we mostly only need the very first notification, which is the bundle 
base.
         * In some cases, like talk notification diff, Flow notificaiton first 
unread post,
         * we need data from the very last notification.
         *
         * @param EchoEvent
         * @param User
-        * @return stdObject|boolean false for none
+        * @return EchoEvent|boolean false for none
         */
-       protected function getBundleLastRawData( $event, $user ) {
+       protected function getBundleLastRawData( EchoEvent $event, User $user ) 
{
                if ( $event->getBundleHash() ) {
                        // First try cache data from preivous query
                        if ( isset( $this->bundleData['last-raw-data'] ) ) {
                                $data = $this->bundleData['last-raw-data'];
                        // Then try to query the storage
                        } else {
-                               global $wgEchoBackend;
-                               $data = $wgEchoBackend->getRawBundleData( 
$user, $event->getBundleHash(), $this->distributionType, 'ASC', 1 );
+                               $eventMapper = new EchoEventMapper( 
MWEchoDbFactory::newFromDefault() );
+                               $data = $eventMapper->fetchByUserBundleHash(
+                                       $user, $event->getBundleHash(), 
$this->distributionType, 'ASC', 1
+                               );
                                if ( $data ) {
-                                       $data = $data->current();
+                                       $data = reset( $data );
                                }
                        }
 
                        if ( $data ) {
-                               $data->event_extra = $data->event_extra ? 
unserialize( $data->event_extra ) : array();
                                return $data;
                        }
                }
diff --git a/formatters/PageLinkFormatter.php b/formatters/PageLinkFormatter.php
index f87f920..80fbc45 100644
--- a/formatters/PageLinkFormatter.php
+++ b/formatters/PageLinkFormatter.php
@@ -35,6 +35,7 @@
         * This method overwrite parent method and construct the bundle iterator
         * based on link from, it will be used in a message like this: Page A 
was
         * link from Page B and X other pages
+        *
         * @param $event EchoEvent
         * @param $user User
         * @param $type string deprecated
@@ -58,8 +59,8 @@
                $linkFrom = array(
                        $extra['link-from-page-id'] => true
                );
-               foreach ( $data as $row ) {
-                       $extra = $row->event_extra ? unserialize( 
$row->event_extra ) : null;
+               foreach ( $data as $bundledEvent ) {
+                       $extra = $bundledEvent->getExtra();
                        if ( !$extra ) {
                                continue;
                        }
diff --git a/includes/DataOutputFormatter.php b/includes/DataOutputFormatter.php
new file mode 100644
index 0000000..5224737
--- /dev/null
+++ b/includes/DataOutputFormatter.php
@@ -0,0 +1,108 @@
+<?php
+
+/**
+ * Utility class that formats a notification in the format specified
+ */
+class EchoDataOutputFormatter {
+
+       /**
+        * Format a notification for a user in the format specified
+        *
+        * @param string|bool specifify output format, false to not format any 
notifications
+        * @param User|null the target user viewing the notification
+        * @return array
+        */
+       public function formatOutput( EchoNotification $notification, $format = 
false, User $user = null ) {
+               $event = $notification->getEvent();
+               // Default to notification user if user is not specified
+               if ( !$user ) {
+                       $user = $notification->getUser();
+               }
+
+               if ( $notification->getBundleBase() && 
$notification->getBundleDisplayHash() ) {
+                       $event->setBundleHash( 
$notification->getBundleDisplayHash() );
+               }
+
+               $timestampMw = self::getUserLocalTime( $user, 
$notification->getTimestamp() );
+
+               // Start creating date section header
+               $now = wfTimestamp();
+               $dateFormat = substr( $timestampMw, 0, 8 );
+               if ( substr( self::getUserLocalTime( $user, $now ), 0, 8 ) === 
$dateFormat ) {
+                       // 'Today'
+                       $date = wfMessage( 'echo-date-today' )->escaped();
+               } elseif ( substr( self::getUserLocalTime( $user, $now - 86400 
), 0, 8 ) === $dateFormat ) {
+                       // 'Yesterday'
+                       $date = wfMessage( 'echo-date-yesterday' )->escaped();
+               } else {
+                       // 'May 10' or '10 May' (depending on user's date 
format preference)
+                       $lang = RequestContext::getMain()->getLanguage();
+                       $dateFormat = $lang->getDateFormatString( 'pretty', 
$user->getDatePreference() ?: 'default' );
+                       $date = $lang->sprintfDate( $dateFormat, $timestampMw );
+               }
+               // End creating date section header
+
+               $output = array(
+                       'id' => $event->getId(),
+                       'type' => $event->getType(),
+                       'category' => $event->getCategory(),
+                       'timestamp' => array(
+                               // UTC timestamp in UNIX format used for 
loading more notification
+                               'utcunix' => wfTimestamp( TS_UNIX, 
$notification->getTimestamp() ),
+                               'unix' => self::getUserLocalTime( $user, 
$notification->getTimestamp(), TS_UNIX ),
+                               'mw' => $timestampMw,
+                               'date' => $date
+                       ),
+               );
+
+               if ( $event->getVariant() ) {
+                       $output['variant'] = $event->getVariant();
+               }
+
+               if ( $event->getTitle() ) {
+                       $output['title'] = array(
+                               'full' => $event->getTitle()->getPrefixedText(),
+                               'namespace' => $event->getTitle()->getNSText(),
+                               'namespace-key' => 
$event->getTitle()->getNamespace(),
+                               'text' => $event->getTitle()->getText(),
+                       );
+               }
+
+               if ( $event->getAgent() ) {
+                       if ( $event->userCan( Revision::DELETED_USER, $user ) ) 
{
+                               $output['agent'] = array(
+                                       'id' => $event->getAgent()->getId(),
+                                       'name' => $event->getAgent()->getName(),
+                               );
+                       } else {
+                               $output['agent'] = array( 'userhidden' => '' );
+                       }
+               }
+
+               if ( $notification->getReadTimestamp() ) {
+                       $output['read'] = $notification->getReadTimestamp();
+               }
+
+               if ( $format ) {
+                       $output['*'] = 
EchoNotificationController::formatNotification( $event, $user, $format );
+               }
+
+               return $output;
+       }
+
+       /**
+        * Helper function for converting UTC timezone to a user's timezone
+        *
+        * @param User
+        * @param string
+        * @param int output format
+        *
+        * @return string
+        */
+       public static function getUserLocalTime( User $user, $ts, $format = 
TS_MW ) {
+               $timestamp = new MWTimestamp( $ts );
+               $timestamp->offsetForUser( $user );
+               return $timestamp->getTimestamp( $format );
+       }
+
+}
diff --git a/includes/DbEchoBackend.php b/includes/DbEchoBackend.php
deleted file mode 100644
index cf457e6..0000000
--- a/includes/DbEchoBackend.php
+++ /dev/null
@@ -1,312 +0,0 @@
-<?php
-
-/**
- * Database backend for echo notification
- */
-class MWDbEchoBackend extends MWEchoBackend {
-
-       /**
-        * @param $row array
-        */
-       public function createNotification( $row ) {
-               $dbw = MWEchoDbFactory::getDB( DB_MASTER );
-
-               $fname = __METHOD__;
-               $dbw->onTransactionIdle(
-                       function() use ( $dbw, $row, $fname ) {
-                               $dbw->startAtomic( $fname );
-                               // reset the base if this notification has a 
display hash
-                               if ( $row['notification_bundle_display_hash'] ) 
{
-                                       $dbw->update(
-                                               'echo_notification',
-                                               array( 
'notification_bundle_base' => 0 ),
-                                               array(
-                                                       'notification_user' => 
$row['notification_user'],
-                                                       
'notification_bundle_display_hash' => $row['notification_bundle_display_hash'],
-                                                       
'notification_bundle_base' => 1
-                                               ),
-                                               $fname
-                                       );
-                               }
-
-                               $row['notification_timestamp'] = 
$dbw->timestamp( $row['notification_timestamp'] );
-                               $dbw->insert( 'echo_notification', $row, $fname 
);
-                               $dbw->endAtomic( $fname );
-
-                               $user = User::newFromId( 
$row['notification_user'] );
-                               MWEchoNotifUser::newFromUser( $user 
)->resetNotificationCount( DB_MASTER );
-                       }
-               );
-       }
-
-       /**
-        * @param $user User the user to get notifications for
-        * @param $limit int The maximum number of notifications to return
-        * @param $continue string Used for offset
-        * @param $outputFormat string The output format of the notifications 
(web,
-        *    email, etc.)
-        * @return array
-        */
-       public function loadNotifications( $user, $limit, $continue, 
$outputFormat = 'web' ) {
-               $dbr = MWEchoDbFactory::getDB( DB_SLAVE );
-
-               $eventTypesToLoad = 
EchoNotificationController::getUserEnabledEvents( $user, $outputFormat );
-               if ( !$eventTypesToLoad ) {
-                       return array();
-               }
-
-               // Look for notifications with base = 1
-               $conds = array(
-                       'notification_user' => $user->getID(),
-                       'event_type' => $eventTypesToLoad,
-                       'notification_bundle_base' => 1
-               );
-
-               $offset = $this->extractQueryOffset( $continue );
-
-               // Start points are specified
-               if ( $offset['timestamp'] && $offset['offset'] ) {
-                       $ts = $dbr->addQuotes( $dbr->timestamp( 
$offset['timestamp'] ) );
-                       // The offset and timestamp are those of the first 
notification we want to return
-                       $conds[] = "notification_timestamp < $ts OR ( 
notification_timestamp = $ts AND notification_event <= " . $offset['offset'] . 
" )";
-               }
-
-               $res = $dbr->select(
-                       array( 'echo_notification', 'echo_event' ),
-                       '*',
-                       $conds,
-                       __METHOD__,
-                       array(
-                               'ORDER BY' => 'notification_timestamp DESC, 
notification_event DESC',
-                               'LIMIT' => $limit,
-                       ),
-                       array(
-                               'echo_event' => array( 'LEFT JOIN', 
'notification_event=event_id' ),
-                       )
-               );
-
-               return iterator_to_array( $res, false );
-       }
-
-       /**
-        * @param $user User
-        * @param $bundleHash string the bundle hash
-        * @param $type string
-        * @param $order string 'ASC'/'DESC'
-        * @param $limit int
-        * @return ResultWrapper|bool
-        */
-       public function getRawBundleData( $user, $bundleHash, $type = 'web', 
$order = 'DESC', $limit = 250 ) {
-               $dbr = MWEchoDbFactory::getDB( DB_SLAVE );
-
-               // We only display 99+ if the number is over 100, we can do 
limit 250, this should be sufficient
-               // to return 99 distinct group iterators, avoid select count( 
distinct ) for the following reason:
-               // 1. it will not scale for large volume data
-               // 2. notification may have random grouping iterator
-               // 3. agent may be anonymous, can't do distinct over two 
columns: event_agent_id and event_agent_ip
-               if ( $type == 'web' ) {
-                       $res = $dbr->select(
-                               array( 'echo_notification', 'echo_event' ),
-                               array( 'event_agent_id', 'event_agent_ip', 
'event_extra', 'event_page_id' ),
-                               array(
-                                       'notification_event=event_id',
-                                       'notification_user' => $user->getId(),
-                                       'notification_bundle_base' => 0,
-                                       'notification_bundle_display_hash' => 
$bundleHash
-                               ),
-                               __METHOD__,
-                               array( 'ORDER BY' => 'notification_timestamp ' 
. $order, 'LIMIT' => $limit )
-                       );
-               // this would be email for now
-               } else {
-                       $res = $dbr->select(
-                               array( 'echo_email_batch', 'echo_event' ),
-                               array( 'event_agent_id', 'event_agent_ip', 
'event_extra', 'event_page_id' ),
-                               array(
-                                       'eeb_event_id=event_id',
-                                       'eeb_user_id' => $user->getId(),
-                                       'eeb_event_hash' => $bundleHash
-                               ),
-                               __METHOD__,
-                               array( 'ORDER BY' => 'eeb_event_id ' . $order, 
'LIMIT' => $limit )
-                       );
-               }
-
-               return $res;
-       }
-
-       /**
-        * Get the last bundle stat - read_timestamp & bundle_display_hash
-        * @param $user User
-        * @param $bundleHash string The hash used to identify a set of 
bundle-able events
-        * @return ResultWrapper|bool
-        */
-       public function getLastBundleStat( $user, $bundleHash ) {
-               $dbr = MWEchoDbFactory::getDB( DB_SLAVE );
-
-               $res = $dbr->selectRow(
-                       array( 'echo_notification' ),
-                       array( 'notification_read_timestamp', 
'notification_bundle_display_hash' ),
-                       array(
-                               'notification_user' => $user->getId(),
-                               'notification_bundle_hash' => $bundleHash
-                       ),
-                       __METHOD__,
-                       array( 'ORDER BY' => 'notification_timestamp DESC', 
'LIMIT' => 1 )
-               );
-               return $res;
-       }
-
-       /**
-        * @param $row array
-        * @return int
-        */
-       public function createEvent( $row ) {
-               $dbw = MWEchoDbFactory::getDB( DB_MASTER );
-
-               $id = $dbw->nextSequenceValue( 'echo_event_id' );
-
-               if ( $id ) {
-                       $row['event_id'] = $id;
-               }
-
-               $dbw->insert( 'echo_event', $row, __METHOD__ );
-
-               if ( !$id ) {
-                       $id = $dbw->insertId();
-               }
-
-               return $id;
-       }
-
-       /**
-        * @param $id int
-        * @param $fromMaster bool
-        * @return ResultWrapper
-        * @throws MWException
-        */
-       public function loadEvent( $id, $fromMaster = false ) {
-               $db = $fromMaster ? MWEchoDbFactory::getDB( DB_MASTER ) : 
MWEchoDbFactory::getDB( DB_SLAVE );
-
-               $row = $db->selectRow( 'echo_event', '*', array( 'event_id' => 
$id ), __METHOD__ );
-
-               if ( !$row && !$fromMaster ) {
-                       return $this->loadEvent( $id, true );
-               } elseif ( !$row ) {
-                       throw new MWException( "No EchoEvent found with ID: 
$id" );
-               }
-
-               return $row;
-       }
-
-       /**
-        * @param $user User
-        * @param $eventIDs array
-        */
-       public function markRead( $user, $eventIDs ) {
-               if ( !$eventIDs ) {
-                       return;
-               }
-
-               $dbw = MWEchoDbFactory::getDB( DB_MASTER );
-
-               $dbw->update(
-                       'echo_notification',
-                       array( 'notification_read_timestamp' => 
$dbw->timestamp( wfTimestampNow() ) ),
-                       array(
-                               'notification_user' => $user->getId(),
-                               'notification_event' => $eventIDs,
-                               'notification_read_timestamp' => null,
-                       ),
-                       __METHOD__
-               );
-       }
-
-       /**
-        * @param $user User
-        */
-       public function markAllRead( $user ) {
-               $dbw = MWEchoDbFactory::getDB( DB_MASTER );
-
-               $dbw->update(
-                       'echo_notification',
-                       array( 'notification_read_timestamp' => 
$dbw->timestamp( wfTimestampNow() ) ),
-                       array(
-                               'notification_user' => $user->getId(),
-                               'notification_read_timestamp' => NULL,
-                               'notification_bundle_base' => 1,
-                       ),
-                       __METHOD__
-               );
-       }
-
-       /**
-        * @param $user User object to check notifications for
-        * @param $dbSource string use master or slave storage to pull count
-        * @return int
-        */
-       public function getNotificationCount( $user, $dbSource ) {
-               // double check
-               if ( !in_array( $dbSource, array( DB_SLAVE, DB_MASTER ) ) ) {
-                       $dbSource = DB_SLAVE;
-               }
-
-               $eventTypesToLoad = 
EchoNotificationController::getUserEnabledEvents( $user, 'web' );
-
-               if ( !$eventTypesToLoad ) {
-                       return 0;
-               }
-
-               global $wgEchoMaxNotificationCount;
-
-               $db = MWEchoDbFactory::getDB( $dbSource );
-               $res = $db->select(
-                       array( 'echo_notification', 'echo_event' ),
-                       array( 'notification_event' ),
-                       array(
-                               'notification_user' => $user->getId(),
-                               'notification_bundle_base' => 1,
-                               'notification_read_timestamp' => null,
-                               'event_type' => $eventTypesToLoad,
-                       ),
-                       __METHOD__,
-                       array( 'LIMIT' => $wgEchoMaxNotificationCount + 1 ),
-                       array(
-                               'echo_event' => array( 'LEFT JOIN', 
'notification_event=event_id' ),
-                       )
-               );
-               return $db->numRows( $res );
-       }
-
-       /**
-        * IMPORTANT: should only call this function if the number of unread 
notification
-        * is reasonable, for example, unread notification count is less than 
the max
-        * display defined in $wgEchoMaxNotificationCount
-        * @param $user User
-        * @param $type string
-        * @return array
-        */
-       public function getUnreadNotifications( $user, $type ) {
-               $dbr = MWEchoDbFactory::getDB( DB_SLAVE );
-               $res = $dbr->select(
-                       array( 'echo_notification', 'echo_event' ),
-                       array( 'notification_event' ),
-                       array(
-                               'notification_user' => $user->getId(),
-                               'notification_bundle_base' => 1,
-                               'notification_read_timestamp' => null,
-                               'event_type' => $type,
-                               'notification_event = event_id'
-                       ),
-                       __METHOD__
-               );
-
-               $eventIds = array();
-               foreach ( $res as $row ) {
-                       $eventIds[$row->notification_event] = 
$row->notification_event;
-               }
-
-               return $eventIds;
-       }
-
-}
diff --git a/includes/EchoBackend.php b/includes/EchoBackend.php
deleted file mode 100644
index cbfbbed..0000000
--- a/includes/EchoBackend.php
+++ /dev/null
@@ -1,132 +0,0 @@
-<?php
-
-/**
- * Base backend class for accessing and saving echo notification data,
- * this class should only provide all the necessary interfaces and
- * implementation should be provided in each child class
- */
-abstract class MWEchoBackend {
-
-       /**
-        * Factory to initialize a backend class
-        * @param $backend string
-        * @return MWEchoBackend
-        * @throws MWException
-        */
-       public static function factory( $backend ) {
-               $backend = strval( $backend );
-
-               $className = 'MW' . $backend . 'EchoBackend';
-
-               if ( !class_exists( $className ) ) {
-                       throw new MWException( "$backend backend is not 
supported" );
-               }
-
-               return new $className();
-       }
-
-       /**
-        * Extract the offset used for notification list
-        *
-        * @param $continue String Used for offset
-        *
-        * @throws MWException
-        * @return int[]
-        */
-       protected function extractQueryOffset( $continue ) {
-               $offset = array (
-                       'timestamp' => 0,
-                       'offset' => 0,
-               );
-               if ( $continue ) {
-                       $values = explode( '|', $continue, 3 );
-                       if ( count( $values ) !== 2 ) {
-                               throw new MWException( 'Invalid continue param: 
' . $continue );
-                       }
-                       $offset['timestamp'] = (int)$values[0];
-                       $offset['offset'] = (int)$values[1];
-               }
-
-               return $offset;
-       }
-
-       /**
-        * Create a new notification
-        * @param $row array
-        */
-       abstract public function createNotification( $row );
-
-       /**
-        * Load notifications based on the parameters
-        * @param $user User the user to get notifications for
-        * @param $limit int The maximum number of notifications to return
-        * @param $continue string Used for offset
-        * @param $outputFormat string The output format of the notifications 
(web, email, etc.)
-        * @return array
-        */
-       abstract public function loadNotifications( $user, $limit, $continue, 
$outputFormat = 'web' );
-
-       /**
-        * Get the bundle data for user/hash
-        * @param $user User
-        * @param $bundleHash string The hash used to identify a set of 
bundle-able events
-        * @param $type string 'web'/'email'
-        * @param $order 'ASC'/'DESC' Sort the result in ascending/descending 
order
-        * @param $limit int the number of records to retrieve
-        * @return ResultWrapper|bool
-        */
-       abstract public function getRawBundleData( $user, $bundleHash, $type = 
'web', $order = 'DESC', $limit = 250 );
-
-       /**
-        * Get the last bundle stat - read_timestamp & bundle_display_hash
-        * @param $user User
-        * @param $bundleHash string The hash used to identify a set of 
bundle-able events
-        * @return ResultWrapper|bool
-        */
-       abstract public function getLastBundleStat( $user, $bundleHash );
-
-       /**
-        * Create an Echo event
-        * @param $row array
-        * @return int
-        */
-       abstract public function createEvent( $row );
-
-       /**
-        * Load an Echo event
-        * @param $id int
-        * @param $fromMaster bool
-        */
-       abstract public function loadEvent( $id, $fromMaster );
-
-       /**
-        * Mark notifications as read for a user
-        * @param $user User
-        * @param $eventIDs array
-        */
-       abstract public function markRead( $user, $eventIDs );
-
-       /**
-        * Mark all unread notifications as read for a user
-        * @param $user User
-        */
-       abstract public function markAllRead( $user );
-
-       /**
-        * Retrieves number of unread notifications that a user has.
-        * @param $user User object to check notifications for
-        * @param $dbSource string use master or slave storage to pull count
-        * @return int
-        */
-       abstract public function getNotificationCount( $user, $dbSource );
-
-       /**
-        * Get the event ids for corresponding unread notifications for an
-        * event type
-        * @param $user User object to check notification for
-        * @param $type string event type
-        * @return array
-        */
-       abstract public function getUnreadNotifications( $user, $type );
-
-}
diff --git a/includes/EchoDbFactory.php b/includes/EchoDbFactory.php
index 24bac62..41372c4 100644
--- a/includes/EchoDbFactory.php
+++ b/includes/EchoDbFactory.php
@@ -7,8 +7,64 @@
 class MWEchoDbFactory {
 
        /**
+        * The wiki to access the database for
+        * @var string|bool
+        */
+       protected $wiki;
+
+       /**
+        * The cluster for the database
+        * @var string|bool
+        */
+       protected $cluster;
+
+       /**
+        * @param string|bool
+        * @param string|bool
+        */
+       public function __construct( $cluster = false, $wiki = false ) {
+               $this->cluster = $cluster;
+               $this->wiki = $wiki;
+       }
+
+       /**
+        * Create a db factory instance from default Echo configuration
+        * @return MWEchoDbFactory
+        */
+       public static function newFromDefault() {
+               global $wgEchoCluster;
+               return new self( $wgEchoCluster );
+       }
+
+       /**
+        * Get the database load balancer
+        * @param $wiki string|bool The wiki ID, or false for the current wiki
+        * @return LoadBalancer
+        */
+       protected function getLB() {
+               // Use the external db defined for Echo
+               if ( $this->cluster ) {
+                       $lb = wfGetLBFactory()->getExternalLB( $this->cluster, 
$this->wiki );
+               } else {
+                       $lb = wfGetLB( $this->wiki );
+               }
+
+               return $lb;
+       }
+
+       /**
+        * Get the database connection for Echo
+        * @param $db int Index of the connection to get
+        * @param $groups mixed Query groups.
+        * @return DatabaseBase
+        */
+       public function getEchoDb( $db, $groups = array() ) {
+               return $this->getLB()->getConnection( $db, $groups, $this->wiki 
);
+       }
+
+       /**
         * Wrapper function for wfGetDB
-        *
+        * @deprecated Use newFromDefault() instead to create a db factory
         * @param $db int Index of the connection to get
         * @param $groups mixed Query groups.
         * @param $wiki string|bool The wiki ID, or false for the current wiki
diff --git a/includes/NotifUser.php b/includes/NotifUser.php
index 38bc388..670ed85 100644
--- a/includes/NotifUser.php
+++ b/includes/NotifUser.php
@@ -18,20 +18,21 @@
        private $cache;
 
        /**
-        * Echo backend storage
-        * @var MWEchoBackend
+        * Database access gateway
+        * @var EchoUserNotificationGateway
         */
-       private $storage;
+       private $userNotifGateway;
 
        /**
         * Constructor for initialization
-        * @param $user User
+        * @param User
+        * @param BagOStuff
+        * @param EchoUserNotificationGateway
         */
-       private function __construct( User $user ) {
-               global $wgMemc, $wgEchoBackend;
+       private function __construct( User $user, BagOStuff $cache, 
EchoUserNotificationGateway $userNotifGateway  ) {
                $this->mUser = $user;
-               $this->storage = $wgEchoBackend;
-               $this->cache = $wgMemc;
+               $this->userNotifGateway = $userNotifGateway;
+               $this->cache = $cache;
        }
 
        /**
@@ -44,7 +45,11 @@
                if ( $user->isAnon() ) {
                        throw new MWException( 'User must be logged in to view 
notification!' );
                }
-               return new MWEchoNotifUser( $user );
+               global $wgMemc;
+               return new MWEchoNotifUser(
+                       $user, $wgMemc,
+                       new EchoUserNotificationGateway( $user, 
MWEchoDbFactory::newFromDefault() )
+               );
        }
 
        /**
@@ -66,8 +71,7 @@
 
                // Mark the talk page notification as read
                $this->markRead(
-                       $this->storage->getUnreadNotifications(
-                               $this->mUser,
+                       $this->userNotifGateway->getUnreadNotifications(
                                'edit-user-talk'
                        )
                );
@@ -133,7 +137,7 @@
                        return (int)$this->cache->get( $memcKey );
                }
 
-               $count = $this->storage->getNotificationCount( $this->mUser, 
$dbSource );
+               $count = $this->userNotifGateway->getNotificationCount( 
$dbSource );
 
                $this->cache->set( $memcKey, $count, 86400 );
 
@@ -150,7 +154,7 @@
                        return;
                }
 
-               $this->storage->markRead( $this->mUser, $eventIds );
+               $this->userNotifGateway->markRead( $eventIds );
                $this->resetNotificationCount( DB_MASTER );
        }
 
@@ -165,7 +169,7 @@
 
                // Only update all the unread notifications if it isn't a huge 
number.
                // TODO: Implement batched jobs it's over the maximum.
-               $this->storage->markAllRead( $this->mUser );
+               $this->userNotifGateway->markAllRead();
                $this->resetNotificationCount( DB_MASTER );
                $this->flagCacheWithNoTalkNotification();
                return true;
diff --git a/includes/gateway/UserNotificationGateway.php 
b/includes/gateway/UserNotificationGateway.php
new file mode 100644
index 0000000..b138a6c
--- /dev/null
+++ b/includes/gateway/UserNotificationGateway.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * Database gateway which handles direct database interaction with the
+ * echo_notification & echo_event for a user, that wouldn't require
+ * loading data into models
+ */
+class EchoUserNotificationGateway {
+
+       /**
+        * @var MWEchoDbFactory
+        */
+       protected $dbFactory;
+
+       /**
+        * @var User
+        */
+       protected $user;
+
+       /**
+        * The tables for this gateway
+        */
+       protected static $eventTable = 'echo_event';
+       protected static $notificationTable = 'echo_notification';
+
+       /**
+        * @param User
+        * @param MWEchoDbFactory
+        */
+       public function __construct( User $user, MWEchoDbFactory $dbFactory ) {
+               $this->user = $user;
+               $this->dbFactory = $dbFactory;
+       }
+
+       /**
+        * Mark notifications as read
+        * @param $eventIDs array
+        */
+       public function markRead( array $eventIDs ) {
+               if ( !$eventIDs ) {
+                       return;
+               }
+
+               $dbw = $this->dbFactory->getEchoDb( DB_MASTER );
+
+               return $dbw->update(
+                       self::$notificationTable,
+                       array( 'notification_read_timestamp' => 
$dbw->timestamp( wfTimestampNow() ) ),
+                       array(
+                               'notification_user' => $this->user->getId(),
+                               'notification_event' => $eventIDs,
+                               'notification_read_timestamp' => null,
+                       ),
+                       __METHOD__
+               );
+       }
+
+       /**
+        * Mark all notification as read
+        */
+       public function markAllRead() {
+               $dbw = $this->dbFactory->getEchoDb( DB_MASTER );
+
+               return $dbw->update(
+                       self::$notificationTable,
+                       array( 'notification_read_timestamp' => 
$dbw->timestamp( wfTimestampNow() ) ),
+                       array(
+                               'notification_user' => $this->user->getId(),
+                               'notification_read_timestamp' => NULL,
+                               'notification_bundle_base' => 1,
+                       ),
+                       __METHOD__
+               );
+       }
+
+       /**
+        * @param $dbSource string use master or slave storage to pull count
+        * @return int
+        */
+       public function getNotificationCount( $dbSource ) {
+               // double check
+               if ( !in_array( $dbSource, array( DB_SLAVE, DB_MASTER ) ) ) {
+                       $dbSource = DB_SLAVE;
+               }
+
+               $eventTypesToLoad = 
EchoNotificationController::getUserEnabledEvents( $this->user, 'web' );
+
+               if ( !$eventTypesToLoad ) {
+                       return 0;
+               }
+
+               global $wgEchoMaxNotificationCount;
+
+               $db = $this->dbFactory->getEchoDb( $dbSource );
+               $res = $db->select(
+                       array(
+                               self::$notificationTable,
+                               self::$eventTable
+                       ),
+                       array( 'notification_event' ),
+                       array(
+                               'notification_user' => $this->user->getId(),
+                               'notification_bundle_base' => 1,
+                               'notification_read_timestamp' => null,
+                               'event_type' => $eventTypesToLoad,
+                       ),
+                       __METHOD__,
+                       array( 'LIMIT' => $wgEchoMaxNotificationCount + 1 ),
+                       array(
+                               'echo_event' => array( 'LEFT JOIN', 
'notification_event=event_id' ),
+                       )
+               );
+               if ( $res ) {
+                       return $db->numRows( $res );
+               } else {
+                       return 0;
+               }
+       }
+
+       /**
+        * IMPORTANT: should only call this function if the number of unread 
notification
+        * is reasonable, for example, unread notification count is less than 
the max
+        * display defined in $wgEchoMaxNotificationCount
+        * @param string
+        * @return int[]
+        */
+       public function getUnreadNotifications( $type ) {
+               $dbr = $this->dbFactory->getEchoDb( DB_SLAVE );
+               $res = $dbr->select(
+                       array(
+                               self::$notificationTable,
+                               self::$eventTable
+                       ),
+                       array( 'notification_event' ),
+                       array(
+                               'notification_user' => $this->user->getId(),
+                               'notification_bundle_base' => 1,
+                               'notification_read_timestamp' => null,
+                               'event_type' => $type,
+                               'notification_event = event_id'
+                       ),
+                       __METHOD__
+               );
+
+               $eventIds = array();
+               if ( $res ) {
+                       foreach ( $res as $row ) {
+                               $eventIds[$row->notification_event] = 
$row->notification_event;
+                       }
+               }
+
+               return $eventIds;
+       }
+
+}
diff --git a/includes/mapper/EventMapper.php b/includes/mapper/EventMapper.php
new file mode 100644
index 0000000..a888a5d
--- /dev/null
+++ b/includes/mapper/EventMapper.php
@@ -0,0 +1,130 @@
+<?php
+
+/**
+ * Database mapper for EchoEvent model, which is an immutable class, there 
should
+ * not be any update to it
+ */
+class EchoEventMapper {
+
+       /**
+        * Echo database factory
+        * @param MWEchoDbFactory
+        */
+       protected $dbFactory;
+
+       /**
+        * @param MWEchoDbFactory
+        */
+       public function __construct( MWEchoDbFactory $dbFactory ) {
+               $this->dbFactory = $dbFactory;
+       }
+
+       /**
+        * Insert an event record
+        *
+        * @param EchoEvent
+        * @return int|bool
+        */
+       public function insert( EchoEvent $event ) {
+               $dbw = $this->dbFactory->getEchoDb( DB_MASTER );
+
+               $id = $dbw->nextSequenceValue( 'echo_event_id' );
+
+               $row = $event->toDbArray();
+               if ( $id ) {
+                       $row['event_id'] = $id;
+               }
+
+               $res = $dbw->insert( 'echo_event', $row, __METHOD__ );
+
+               if ( $res ) {
+                       if ( !$id ) {
+                               $id = $dbw->insertId();
+                       }
+                       return $id;
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Create an EchoEvent by id
+        *
+        * @param int
+        * @param boolean
+        * @return EchoEvent
+        * @throws MWException
+        */
+       public function fetchById( $id, $fromMaster = false ) {
+               $db = $fromMaster ? $this->dbFactory->getEchoDb( DB_MASTER ) : 
$this->dbFactory->getEchoDb( DB_SLAVE );
+
+               $row = $db->selectRow( 'echo_event', '*', array( 'event_id' => 
$id ), __METHOD__ );
+
+               if ( !$row && !$fromMaster ) {
+                       return $this->fetchById( $id, true );
+               } elseif ( !$row ) {
+                       throw new MWException( "No EchoEvent found with ID: 
$id" );
+               }
+
+               return EchoEvent::newFromRow( $row );
+       }
+
+       /**
+        * Get a list of echo events identified by user and bundle hash
+        *
+        * @param $user User
+        * @param $bundleHash string the bundle hash
+        * @param $type string distribution type
+        * @param $order string 'ASC'/'DESC'
+        * @param $limit int
+        * @return EchoEvent[]|bool
+        */
+       public function fetchByUserBundleHash( User $user, $bundleHash, $type = 
'web', $order = 'DESC', $limit = 250 ) {
+               $dbr = $this->dbFactory->getEchoDb( DB_SLAVE );
+
+               // We only display 99+ if the number is over 100, we can do 
limit 250, this should
+               // be sufficient to return 99 distinct group iterators, avoid 
select count( distinct )
+               // for the following reason:
+               // 1. it will not scale for large volume data
+               // 2. notification may have random grouping iterator
+               // 3. agent may be anonymous, can't do distinct over two 
columns: event_agent_id and event_agent_ip
+               if ( $type == 'web' ) {
+                       $res = $dbr->select(
+                               array( 'echo_notification', 'echo_event' ),
+                               array( 'echo_event.*' ),
+                               array(
+                                       'notification_event=event_id',
+                                       'notification_user' => $user->getId(),
+                                       'notification_bundle_base' => 0,
+                                       'notification_bundle_display_hash' => 
$bundleHash
+                               ),
+                               __METHOD__,
+                               array( 'ORDER BY' => 'notification_timestamp ' 
. $order, 'LIMIT' => $limit )
+                       );
+               // this would be email for now
+               } else {
+                       $res = $dbr->select(
+                               array( 'echo_email_batch', 'echo_event' ),
+                               array( 'echo_event.*' ),
+                               array(
+                                       'eeb_event_id=event_id',
+                                       'eeb_user_id' => $user->getId(),
+                                       'eeb_event_hash' => $bundleHash
+                               ),
+                               __METHOD__,
+                               array( 'ORDER BY' => 'eeb_event_id ' . $order, 
'LIMIT' => $limit )
+                       );
+               }
+
+               if ( $res ) {
+                       $data = array();
+                       foreach ( $res as $row ) {
+                               $data[] = EchoEvent::newFromRow( $row );
+                       }
+                       return $data;
+               } else {
+                       return false;
+               }
+       }
+
+}
diff --git a/includes/mapper/NotificationMapper.php 
b/includes/mapper/NotificationMapper.php
new file mode 100644
index 0000000..de91fbf
--- /dev/null
+++ b/includes/mapper/NotificationMapper.php
@@ -0,0 +1,172 @@
+<?php
+
+/**
+ * Database mapper for EchoNotification model
+ */
+class EchoNotificationMapper {
+
+       /**
+        * Echo database factory
+        * @param MWEchoDbFactory
+        */
+       protected $dbFactory;
+
+       /**
+        * @param MWEchoDbFactory
+        */
+       public function __construct( MWEchoDbFactory $dbFactory ) {
+               $this->dbFactory = $dbFactory;
+       }
+
+       /**
+        * Insert a notification record
+        * @param EchoNotification
+        * @return null
+        */
+       public function insert( EchoNotification $notification ) {
+               $dbw = $this->dbFactory->getEchoDb( DB_MASTER );
+
+               $fname = __METHOD__;
+               $row = $notification->toDbArray();
+
+               $dbw->onTransactionIdle( function() use ( $dbw, $row, $fname ) {
+                       $dbw->startAtomic( $fname );
+                       // reset the bundle base if this notification has a 
display hash
+                       // the result of this operation is that all previous 
notifications
+                       // with the same display hash are set to non-base 
because new record
+                       // is becoming the bundle base
+                       if ( $row['notification_bundle_display_hash'] ) {
+                               $dbw->update(
+                                       'echo_notification',
+                                       array( 'notification_bundle_base' => 0 
),
+                                       array(
+                                               'notification_user' => 
$row['notification_user'],
+                                               
'notification_bundle_display_hash' => $row['notification_bundle_display_hash'],
+                                               'notification_bundle_base' => 1
+                                       ),
+                                       $fname
+                               );
+                       }
+
+                       $row['notification_timestamp'] = $dbw->timestamp( 
$row['notification_timestamp'] );
+                       $res = $dbw->insert( 'echo_notification', $row, $fname 
);
+                       $dbw->endAtomic( $fname );
+
+                       // @todo - This is simple and easy but the proper way 
is to build a notification
+                       // observer to notify all listeners on creating a new 
notification
+                       if ( $res ) {
+                               $user = User::newFromId( 
$row['notification_user'] );
+                               MWEchoNotifUser::newFromUser( $user 
)->resetNotificationCount( DB_MASTER );
+                       }
+               } );
+       }
+
+       /**
+        * Extract the offset used for notification list
+        * @param $continue String Used for offset
+        * @throws MWException
+        * @return int[]
+        */
+       protected function extractQueryOffset( $continue ) {
+               $offset = array (
+                       'timestamp' => 0,
+                       'offset' => 0,
+               );
+               if ( $continue ) {
+                       $values = explode( '|', $continue, 3 );
+                       if ( count( $values ) !== 2 ) {
+                               throw new MWException( 'Invalid continue param: 
' . $continue );
+                       }
+                       $offset['timestamp'] = (int)$values[0];
+                       $offset['offset'] = (int)$values[1];
+               }
+
+               return $offset;
+       }
+
+       /**
+        * Get Notification by user in batch along with limit, offset etc
+        * @param User the user to get notifications for
+        * @param int The maximum number of notifications to return
+        * @param string Used for offset
+        * @param string Notification distribution type ( web, email, etc.)
+        * @return EchoNotification[]
+        */
+       public function fetchByUser( User $user, $limit, $continue, 
$distributionType = 'web' ) {
+               $dbr = $this->dbFactory->getEchoDb( DB_SLAVE );
+
+               $eventTypesToLoad = 
EchoNotificationController::getUserEnabledEvents( $user, $distributionType );
+               if ( !$eventTypesToLoad ) {
+                       return array();
+               }
+
+               // Look for notifications with base = 1
+               $conds = array(
+                       'notification_user' => $user->getID(),
+                       'event_type' => $eventTypesToLoad,
+                       'notification_bundle_base' => 1
+               );
+
+               $offset = $this->extractQueryOffset( $continue );
+
+               // Start points are specified
+               if ( $offset['timestamp'] && $offset['offset'] ) {
+                       $ts = $dbr->addQuotes( $dbr->timestamp( 
$offset['timestamp'] ) );
+                       // The offset and timestamp are those of the first 
notification we want to return
+                       $conds[] = "notification_timestamp < $ts OR ( 
notification_timestamp = $ts AND notification_event <= " . $offset['offset'] . 
" )";
+               }
+
+               $res = $dbr->select(
+                       array( 'echo_notification', 'echo_event' ),
+                       '*',
+                       $conds,
+                       __METHOD__,
+                       array(
+                               'ORDER BY' => 'notification_timestamp DESC, 
notification_event DESC',
+                               'LIMIT' => $limit,
+                       ),
+                       array(
+                               'echo_event' => array( 'LEFT JOIN', 
'notification_event=event_id' ),
+                       )
+               );
+
+               $data = array();
+
+               if ( $res ) {
+                       foreach ( $res as $row ) {
+                               $data[] = EchoNotification::newFromRow( $row );
+                       }
+               }
+               return $data;
+       }
+
+       /**
+        * Get the last notification in a set of bundle-able notifications by a 
bundle hash
+        * @param User
+        * @param string The hash used to identify a set of bundle-able 
notifications
+        * @return EchoNotification|bool
+        */
+       public function fetchNewestByUserBundleHash( User $user, $bundleHash ) {
+               $dbr = $this->dbFactory->getEchoDb( DB_SLAVE );
+
+               $row = $dbr->selectRow(
+                       array( 'echo_notification', 'echo_event' ),
+                       array( '*' ),
+                       array(
+                               'notification_user' => $user->getId(),
+                               'notification_bundle_hash' => $bundleHash
+                       ),
+                       __METHOD__,
+                       array( 'ORDER BY' => 'notification_timestamp DESC', 
'LIMIT' => 1 ),
+                       array(
+                               'echo_event' => array( 'LEFT JOIN', 
'notification_event=event_id' ),
+                       )
+               );
+               if ( $row ) {
+                       return EchoNotification::newFromRow( $row );
+               } else {
+                       return false;
+               }
+       }
+
+}
diff --git a/model/Event.php b/model/Event.php
index 34b9e46..6fae194 100644
--- a/model/Event.php
+++ b/model/Event.php
@@ -147,6 +147,37 @@
        }
 
        /**
+        * Convert the object's database property to array
+        * @return array
+        */
+       public function toDbArray() {
+               $data = array (
+                       'event_type' => $this->type,
+                       'event_variant' => $this->variant,
+                       'event_extra' => $this->serializeExtra()
+               );
+               if ( $this->id ) {
+                       $data['event_id'] = $this->id;
+               }
+               if ( $this->agent ) {
+                       if ( $this->agent->isAnon() ) {
+                               $data['event_agent_ip'] = 
$this->agent->getName();
+                       } else {
+                               $data['event_agent_id'] = $this->agent->getId();
+                       }
+               }
+               if ( $this->title ) {
+                       $pageId = $this->title->getArticleId();
+                       // Don't need any special handling for title with no id
+                       // as they are already stored in extra data array
+                       if ( $pageId ) {
+                               $data['event_page_id'] = $pageId;
+                       }
+               }
+               return $data;
+       }
+
+       /**
         * Check whether the echo event is an enabled event
         * @return bool
         */
@@ -163,32 +194,8 @@
         * Inserts the object into the database.
         */
        protected function insert() {
-               global $wgEchoBackend;
-
-               if ( $this->id ) {
-                       throw new MWException( "Attempt to insert() an existing 
event" );
-               }
-
-               $row = array(
-                       'event_type' => $this->type,
-                       'event_variant' => $this->variant,
-               );
-
-               if ( $this->agent ) {
-                       if ( $this->agent->isAnon() ) {
-                               $row['event_agent_ip'] = 
$this->agent->getName();
-                       } else {
-                               $row['event_agent_id'] = $this->agent->getId();
-                       }
-               }
-
-               if ( $this->pageId ) {
-                       $row['event_page_id'] = $this->pageId;
-               }
-
-               $row['event_extra'] = $this->serializeExtra();
-
-               $this->id = $wgEchoBackend->createEvent( $row );
+               $eventMapper = new EchoEventMapper( 
MWEchoDbFactory::newFromDefault() );
+               $this->id = $eventMapper->insert( $this );
        }
 
        /**
@@ -235,9 +242,21 @@
         * @param $fromMaster bool
         */
        public function loadFromID( $id, $fromMaster = false ) {
-               global $wgEchoBackend;
+               $eventMapper = new EchoEventMapper( 
MWEchoDbFactory::newFromDefault() );
+               $event = $eventMapper->fetchById( $id, $fromMaster );
 
-               $this->loadFromRow( $wgEchoBackend->loadEvent( $id, $fromMaster 
) );
+               // Copy over the attribute
+               $this->id = $event->id;
+               $this->type = $event->type;
+               $this->variant = $event->variant;
+               $this->extra = $event->extra;
+               $this->pageId = $event->pageId;
+               $this->agent = $event->agent;
+               $this->title = $event->title;
+               // Don't overwrite timestamp if it exists already
+               if ( !$this->timestamp ) {
+                       $this->timestamp = $event->timestamp;
+               }
        }
 
        /**
diff --git a/model/Notification.php b/model/Notification.php
index d0a6abc..8f93ad2 100644
--- a/model/Notification.php
+++ b/model/Notification.php
@@ -23,6 +23,25 @@
        protected $readTimestamp;
 
        /**
+        * Determine whether this is a bundle base.  Default is 1,
+        * which means it's a bundle base
+        * @var int
+        */
+       protected $bundleBase = 1;
+
+       /**
+        * The hash used to determine if a set of event could be bundled
+        * @var string
+        */
+       protected $bundleHash = '';
+
+       /**
+        * The hash used to bundle events to display
+        * @var string
+        */
+       protected $bundleDisplayHash = '';
+
+       /**
         * Do not use this constructor.
         */
        protected function __construct() {}
@@ -71,16 +90,9 @@
         * Adds this new notification object to the backend storage.
         */
        protected function insert() {
-               global $wgEchoBackend, $wgEchoNotifications;
+               global $wgEchoNotifications;
 
-               $row = array(
-                       'notification_event' => $this->event->getId(),
-                       'notification_user' => $this->user->getId(),
-                       'notification_timestamp' => $this->timestamp,
-                       'notification_read_timestamp' => $this->readTimestamp,
-                       'notification_bundle_hash' => '',
-                       'notification_bundle_display_hash' => ''
-               );
+               $notifMapper = new EchoNotificationMapper( 
MWEchoDbFactory::newFromDefault() );
 
                // Get the bundle key for this event if web bundling is enabled
                $bundleKey = '';
@@ -89,23 +101,60 @@
                }
                if ( $bundleKey ) {
                        $hash = md5( $bundleKey );
-                       $row['notification_bundle_hash'] = $hash;
-
-                       $lastStat = $wgEchoBackend->getLastBundleStat( 
$this->user, $hash );
+                       $this->bundleHash = $hash;
+                       $lastNotif = $notifMapper->fetchNewestByUserBundleHash( 
$this->user, $hash );
 
                        // Use a new display hash if:
                        // 1. there was no last bundle notification
                        // 2. last bundle notification with the same hash was 
read
-                       if ( $lastStat && 
!$lastStat->notification_read_timestamp ) {
-                               $row['notification_bundle_display_hash'] = 
$lastStat->notification_bundle_display_hash;
+                       if ( $lastNotif && !$lastNotif->getReadTimestamp() ) {
+                               $this->bundleDisplayHash = 
$lastNotif->getBundleDisplayHash();
                        } else {
-                               $row['notification_bundle_display_hash'] = md5( 
$bundleKey . '-display-hash-' . wfTimestampNow() );
+                               $this->bundleDisplayHash = md5( $bundleKey . 
'-display-hash-' . wfTimestampNow() );
                        }
                }
 
-               $wgEchoBackend->createNotification( $row );
+               $notifMapper->insert( $this );
 
                wfRunHooks( 'EchoCreateNotificationComplete', array( $this ) );
+       }
+
+       /**
+        * Load a notification record from std class
+        * @param stdClass
+        * @return EchoNotification
+        */
+       public static function newFromRow( $row ) {
+               $notification = new EchoNotification();
+
+               if ( property_exists( $row, 'event_type' ) ) {
+                       $notification->event = EchoEvent::newFromRow( $row );
+               } else {
+                       $notification->event = EchoEvent::newFromID( 
$row->notification_event );
+               }
+               $notification->user = User::newFromId( $row->notification_user 
);
+               $notification->timestamp = $row->notification_timestamp;
+               $notification->readTimestamp = 
$row->notification_read_timestamp;
+               $notification->bundleBase = $row->notification_bundle_base;
+               $notification->bundleHash = $row->notification_bundle_hash;
+               $notification->bundleDisplayHash = 
$row->notification_bundle_display_hash;
+               return $notification;
+       }
+
+       /**
+        * Convert object property to database row array
+        * @return array
+        */
+       public function toDbArray() {
+               return array(
+                       'notification_event' => $this->event->getId(),
+                       'notification_user' => $this->user->getId(),
+                       'notification_timestamp' => $this->timestamp,
+                       'notification_read_timestamp' => $this->readTimestamp,
+                       'notification_bundle_base' => $this->bundleBase,
+                       'notification_bundle_hash' => $this->bundleHash,
+                       'notification_bundle_display_hash' => 
$this->bundleDisplayHash
+               );
        }
 
        /**
@@ -139,4 +188,28 @@
        public function getReadTimestamp() {
                return $this->readTimestamp;
        }
+
+       /**
+        * Getter method
+        * @return int Notification bundle base
+        */
+       public function getBundleBase() {
+               return $this->bundleBase;
+       }
+
+       /**
+        * Getter method
+        * @return string|null Notification bundle hash
+        */
+       public function getBundleHash() {
+               return $this->bundleHash;
+       }
+
+       /**
+        * Getter method
+        * @return string|null Notification bundle display hash
+        */
+       public function getBundleDisplayHash() {
+               return $this->bundleDisplayHash;
+       }
 }
diff --git a/special/SpecialNotifications.php b/special/SpecialNotifications.php
index 067d090..4ca78bd 100644
--- a/special/SpecialNotifications.php
+++ b/special/SpecialNotifications.php
@@ -38,7 +38,12 @@
                $continue = $this->getRequest()->getVal( 'continue', null );
 
                // Pull the notifications
-               $notif = ApiEchoNotifications::getNotifications( $user, 'html', 
self::$displayNum + 1, $continue );
+               $notif = array();
+               $notificationMapper = new EchoNotificationMapper( 
MWEchoDbFactory::newFromDefault() );
+               $notifications = $notificationMapper->fetchByUser( $user, 
self::$displayNum + 1, $continue, 'web' );
+               foreach ( $notifications as $notification ) {
+                       $notif[] = EchoDataOutputFormatter::formatOutput( 
$notification, 'html', $user );
+               }
 
                // If there are no notifications, display a message saying so
                if ( !$notif ) {
diff --git a/tests/includes/EchoDbFactoryTest.php 
b/tests/includes/EchoDbFactoryTest.php
new file mode 100644
index 0000000..61cffab
--- /dev/null
+++ b/tests/includes/EchoDbFactoryTest.php
@@ -0,0 +1,29 @@
+<?php
+
+class MWEchoDbFactoryTest extends MediaWikiTestCase {
+
+       public function testNewFromDefault() {
+               $db = MWEchoDbFactory::newFromDefault();
+               $this->assertInstanceOf( 'MWEchoDbFactory', $db );
+               return $db;
+       }
+
+       /**
+        * @depends testNewFromDefault
+        */
+       public function testGetEchoDb( MWEchoDbFactory $db ) {
+               $this->assertInstanceOf( 'DatabaseBase', $db->getEchoDb( 
DB_MASTER ) );
+               $this->assertInstanceOf( 'DatabaseBase', $db->getEchoDb( 
DB_SLAVE ) );
+       }
+
+       /**
+        * @depends testNewFromDefault
+        */
+       public function testGetLB( MWEchoDbFactory $db ) {
+               $reflection = new ReflectionClass( 'MWEchoDbFactory' );
+               $method = $reflection->getMethod( 'getLB' );
+               $method->setAccessible( true );
+               $this->assertInstanceOf( 'LoadBalancer', $method->invoke( $db ) 
);
+       }
+
+}
diff --git a/tests/includes/gateway/UserNotificationGatewayTest.php 
b/tests/includes/gateway/UserNotificationGatewayTest.php
new file mode 100644
index 0000000..5f27a9c
--- /dev/null
+++ b/tests/includes/gateway/UserNotificationGatewayTest.php
@@ -0,0 +1,134 @@
+<?php
+
+class EchoUserNotificationGatewayTest extends MediaWikiTestCase {
+
+       public function testMarkRead() {
+               // no event ids to mark
+               $gateway = new EchoUserNotificationGateway( User::newFromId( 1 
), $this->mockMWEchoDbFactory() );
+               $this->assertNull( $gateway->markRead( array() ) );
+
+               // successful update
+               $gateway = new EchoUserNotificationGateway( User::newFromId( 1 
), $this->mockMWEchoDbFactory( array( 'update' => true ) ) );
+               $this->assertTrue( $gateway->markRead( array( 2 ) ) );
+
+               // unsuccessful update
+               $gateway = new EchoUserNotificationGateway( User::newFromId( 1 
), $this->mockMWEchoDbFactory( array( 'update' => false ) ) );
+               $this->assertFalse( $gateway->markRead( array( 2 ) ) );
+       }
+
+       public function testMarkAllRead() {
+               // successful update
+               $gateway = new EchoUserNotificationGateway( User::newFromId( 1 
), $this->mockMWEchoDbFactory( array( 'update' => true ) ) );
+               $this->assertTrue( $gateway->markAllRead( array( 2 ) ) );
+
+               // unsuccessful update
+               $gateway = new EchoUserNotificationGateway( User::newFromId( 1 
), $this->mockMWEchoDbFactory( array( 'update' => false ) ) );
+               $this->assertFalse( $gateway->markAllRead( array( 2 ) ) );
+       }
+
+       public function testGetNotificationCount() {
+               global $wgEchoNotificationCategories;
+               $previous = $wgEchoNotificationCategories;
+
+               // Alter the category group so the user is always elegible to
+               // view some notification types.
+               foreach ( $wgEchoNotificationCategories as &$value ) {
+                       $value['usergroups'] = array( 'echo_group' );
+               }
+               unset( $value );
+
+               // successful select
+               $gateway = new EchoUserNotificationGateway( $this->mockUser(), 
$this->mockMWEchoDbFactory( array( 'select' => false ) ) );
+               $this->assertEquals( 0, $gateway->getNotificationCount( 
DB_SLAVE ) );
+
+               // successful select
+               $gateway = new EchoUserNotificationGateway( $this->mockUser(), 
$this->mockMWEchoDbFactory( array( 'select' => array( 1, 2, 3 ) ) ) );
+               $this->assertEquals( 3, $gateway->getNotificationCount( 
DB_SLAVE ) );
+
+               // Alter the category group so the user is not elegible to
+               // view any notification types.
+               foreach ( $wgEchoNotificationCategories as &$value ) {
+                       $value['usergroups'] = array( 'sysop' );
+               }
+               unset( $value );
+
+               $gateway = new EchoUserNotificationGateway( $this->mockUser(), 
$this->mockMWEchoDbFactory( array( 'select' => array( 1, 2, 3 ) ) ) );
+               $this->assertEquals( 0, $gateway->getNotificationCount( 
DB_SLAVE ) );
+
+               $wgEchoNotificationCategories = $previous;
+       }
+
+       public function testGetUnreadNotifications() {
+               $gateway = new EchoUserNotificationGateway( $this->mockUser(), 
$this->mockMWEchoDbFactory( array( 'select' => false ) ) );
+               $this->assertEmpty( $gateway->getUnreadNotifications( 
'user_talk' ) );
+
+               $dbResult = array(
+                       (object)array( 'notification_event' => 1 ),
+                       (object)array( 'notification_event' => 2 ),
+                       (object)array( 'notification_event' => 3 ),
+               );
+               $gateway = new EchoUserNotificationGateway( $this->mockUser(), 
$this->mockMWEchoDbFactory( array( 'select' => $dbResult ) ) );
+               $res = $gateway->getUnreadNotifications( 'user_talk' );
+               $this->assertEquals( $res, array( 1 => 1, 2 => 2, 3 => 3 ) );
+       }
+
+       /**
+        * Mock object of User
+        */
+       protected function mockUser() {
+               $user = $this->getMockBuilder( 'User' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $user->expects( $this->any() )
+                       ->method( 'getID' )
+                       ->will( $this->returnValue( 1 ) );
+               $user->expects( $this->any() )
+                       ->method( 'getOption' )
+                       ->will( $this->returnValue( true ) );
+               $user->expects( $this->any() )
+                       ->method( 'getGroups' )
+                       ->will( $this->returnValue( array( 'echo_group' ) ) );
+               return $user;
+       }
+
+       /**
+        * Mock object of MWEchoDbFactory
+        */
+       protected function mockMWEchoDbFactory( array $dbResult = array() ) {
+               $dbFactory = $this->getMockBuilder( 'MWEchoDbFactory' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $dbFactory->expects( $this->any() )
+                       ->method( 'getEchoDb' )
+                       ->will( $this->returnValue( $this->mockDb( $dbResult ) 
) );
+               return $dbFactory;
+       }
+
+       /**
+        * Mock object of DatabaseMysql ( DatabaseBase )
+        */
+       protected function mockDb( array $dbResult = array() ) {
+               $dbResult += array(
+                       'update' => '',
+                       'select' => '',
+                       'selectRow' => '',
+               );
+               $db = $this->getMockBuilder( 'DatabaseMysql' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $db->expects( $this->any() )
+                       ->method( 'update' )
+                       ->will( $this->returnValue( $dbResult['update'] ) );
+               $db->expects( $this->any() )
+                       ->method( 'select' )
+                       ->will( $this->returnValue( $dbResult['select'] ) );
+               $db->expects( $this->any() )
+                       ->method( 'selectRow' )
+                       ->will( $this->returnValue( $dbResult['selectRow'] ) );
+               $db->expects( $this->any() )
+                       ->method( 'numRows' )
+                       ->will( $this->returnValue( count( $dbResult['select'] 
) ) );
+               return $db;
+       }
+
+}
diff --git a/tests/includes/mapper/EventMapperTest.php 
b/tests/includes/mapper/EventMapperTest.php
new file mode 100644
index 0000000..834df68
--- /dev/null
+++ b/tests/includes/mapper/EventMapperTest.php
@@ -0,0 +1,167 @@
+<?php
+
+class EchoEventMapperTest extends MediaWikiTestCase {
+
+       public function provideDataTestInsert() {
+               return array (
+                       array (
+                               'successful insert with next sequence = 1',
+                               array ( 'nextSequenceValue' => 1, 'insert' => 
true, 'insertId' => 2 ),
+                               1
+                       ),
+                       array (
+                               'successful insert with insert id = 2',
+                               array ( 'nextSequenceValue' => null, 'insert' 
=> true, 'insertId' => 2 ),
+                               2
+                       ),
+                       array (
+                               'unsuccessful insert',
+                               array ( 'nextSequenceValue' => null, 'insert' 
=> false, 'insertId' => 2 ),
+                               false
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider provideDataTestInsert
+        */
+       public function testInsert( $message, $dbResult, $result ) {
+               $event = $this->mockEchoEvent();
+               $eventMapper = new EchoEventMapper( $this->mockMWEchoDbFactory( 
$dbResult ) );
+               $this->assertEquals( $result, $eventMapper->insert( $event ), 
$message );
+       }
+
+       /**
+        * Successful fetchById()
+        */
+       public function testSuccessfulFetchById() {
+               $eventMapper = new EchoEventMapper(
+                       $this->mockMWEchoDbFactory(
+                               array(
+                                       'selectRow' => (object)array (
+                                               'event_id' => 1,
+                                               'event_type' => 'test',
+                                               'event_variant' => '',
+                                               'event_extra' => '',
+                                               'event_page_id' => '',
+                                               'event_agent_id' => '',
+                                               'event_agent_ip' => ''
+                                       )
+                               )
+                       )
+               );
+               $res = $eventMapper->fetchById( 1 );
+               $this->assertInstanceOf( 'EchoEvent', $res );
+       }
+
+       /**
+        * @expectedException MWException
+        */
+       public function testUnsuccessfulFetchById() {
+               $eventMapper = new EchoEventMapper(
+                       $this->mockMWEchoDbFactory(
+                               array(
+                                       'selectRow' => false
+                               )
+                       )
+               );
+               $res = $eventMapper->fetchById( 1 );
+               $this->assertInstanceOf( 'EchoEvent', $res );
+       }
+
+       public function testFetchByUserBundleHash() {
+               // Unsuccessful select
+               $event = $this->mockEchoEvent();
+               $eventMapper = new EchoEventMapper( $this->mockMWEchoDbFactory( 
array ( 'select' => false ) ) );
+               $res = $eventMapper->fetchByUserBundleHash( User::newFromId( 1 
), 'testhash', 'web', 'DESC', 250 );
+               $this->assertFalse( $res );
+
+               // Successful select
+               $event = $this->mockEchoEvent();
+               $dbResult = array (
+                       (object)array (
+                               'event_id' => 1,
+                               'event_type' => 'test',
+                               'event_variant' => '',
+                               'event_extra' => '',
+                               'event_page_id' => '',
+                               'event_agent_id' => '',
+                               'event_agent_ip' => ''
+                       ),
+                       (object)array (
+                               'event_id' => 2,
+                               'event_type' => 'test2',
+                               'event_variant' => '',
+                               'event_extra' => '',
+                               'event_page_id' => '',
+                               'event_agent_id' => '',
+                               'event_agent_ip' => ''
+                       )
+               );
+               $eventMapper = new EchoEventMapper( $this->mockMWEchoDbFactory( 
array ( 'select' => $dbResult ) ) );
+               $res = $eventMapper->fetchByUserBundleHash( User::newFromId( 1 
), 'testhash', 'web', 'DESC', 250 );
+               $this->assertTrue( is_array( $res ) );
+               foreach ( $res as $row ) {
+                       $this->assertInstanceOf( 'EchoEvent', $row );
+               }
+       }
+
+       /**
+        * Mock object of EchoEvent
+        */
+       protected function mockEchoEvent() {
+               $event = $this->getMockBuilder( 'EchoEvent' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $event->expects( $this->any() )
+                       ->method( 'toDbArray' )
+                       ->will( $this->returnValue( array() ) );
+               return $event;
+       }
+
+       /**
+        * Mock object of MWEchoDbFactory
+        */
+       protected function mockMWEchoDbFactory( $dbResult ) {
+               $dbFactory = $this->getMockBuilder( 'MWEchoDbFactory' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $dbFactory->expects( $this->any() )
+                       ->method( 'getEchoDb' )
+                       ->will( $this->returnValue( $this->mockDb( $dbResult ) 
) );
+               return $dbFactory;
+       }
+
+       /**
+        * Mock object of DatabaseMysql ( DatabaseBase )
+        */
+       protected function mockDb( array $dbResult ) {
+               $dbResult += array(
+                       'nextSequenceValue' => '',
+                       'insert' => '',
+                       'insertId' => '',
+                       'select' => '',
+                       'selectRow' => ''
+               );
+               $db = $this->getMockBuilder( 'DatabaseMysql' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $db->expects( $this->any() )
+                       ->method( 'nextSequenceValue' )
+                       ->will( $this->returnValue( 
$dbResult['nextSequenceValue'] ) );
+               $db->expects( $this->any() )
+                       ->method( 'insert' )
+                       ->will( $this->returnValue( $dbResult['insert'] ) );
+               $db->expects( $this->any() )
+                       ->method( 'insertId' )
+                       ->will( $this->returnValue( $dbResult['insertId'] ) );
+               $db->expects( $this->any() )
+                       ->method( 'select' )
+                       ->will( $this->returnValue( $dbResult['select'] ) );
+               $db->expects( $this->any() )
+                       ->method( 'selectRow' )
+                       ->will( $this->returnValue( $dbResult['selectRow'] ) );
+               return $db;
+       }
+
+}
diff --git a/tests/includes/mapper/NotificationMapperTest.php 
b/tests/includes/mapper/NotificationMapperTest.php
new file mode 100644
index 0000000..2de2583
--- /dev/null
+++ b/tests/includes/mapper/NotificationMapperTest.php
@@ -0,0 +1,184 @@
+<?php
+
+class EchoNotificationMapperTest extends MediaWikiTestCase {
+
+       /**
+        * @todo write this test
+        */
+       public function testInsert() {
+               $this->assertTrue( true );
+       }
+
+       public function testFetchByUser() {
+               global $wgEchoNotificationCategories;
+               $previous = $wgEchoNotificationCategories;
+
+               // Alter the category group so the user is always elegible to
+               // view some notification types.
+               foreach ( $wgEchoNotificationCategories as &$value ) {
+                       $value['usergroups'] = array( 'echo_group' );
+               }
+               unset( $value );
+
+               // Unsuccessful select
+               $notifMapper = new EchoNotificationMapper( 
$this->mockMWEchoDbFactory( array ( 'select' => false ) ) );
+               $res = $notifMapper->fetchByUser( $this->mockUser(), 10, '' );
+               $this->assertEmpty( $res );
+
+               // Successful select
+               $dbResult = array(
+                       (object)array (
+                               'event_id' => 1,
+                               'event_type' => 'test',
+                               'event_variant' => '',
+                               'event_extra' => '',
+                               'event_page_id' => '',
+                               'event_agent_id' => '',
+                               'event_agent_ip' => '',
+                               'notification_user' => 1,
+                               'notification_timestamp' => '20140615101010',
+                               'notification_read_timestamp' => 
'20140616101010',
+                               'notification_bundle_base' => 1,
+                               'notification_bundle_hash' => 'testhash',
+                               'notification_bundle_display_hash' => 
'testdisplayhash'
+                       )
+               );
+               $notifMapper = new EchoNotificationMapper( 
$this->mockMWEchoDbFactory( array ( 'select' => $dbResult ) ) );
+               $res = $notifMapper->fetchByUser( $this->mockUser(), 10, '' );
+               $this->assertTrue( is_array( $res ) );
+               foreach ( $res as $row ) {
+                       $this->assertInstanceOf( 'EchoNotification', $row  );
+               }
+
+               // Alter the category group so the user is not elegible to
+               // view any notification types.
+               foreach ( $wgEchoNotificationCategories as &$value ) {
+                       $value['usergroups'] = array( 'sysop' );
+               }
+               unset( $value );
+
+               $notifMapper = new EchoNotificationMapper( 
$this->mockMWEchoDbFactory( array() ) );
+               $res = $notifMapper->fetchByUser( $this->mockUser(), 10, '' );
+               $this->assertEmpty( $res );
+
+               // Restore the default setting
+               $wgEchoNotificationCategories = $previous;
+       }
+
+       public function testFetchNewestByUserBundleHash() {
+               // Unsuccessful select
+               $notifMapper = new EchoNotificationMapper( 
$this->mockMWEchoDbFactory( array ( 'selectRow' => false ) ) );
+               $res = $notifMapper->fetchNewestByUserBundleHash( 
User::newFromId( 1 ), 'testhash' );
+               $this->assertFalse( $res );
+
+               // Successful select
+               $dbResult = (object)array (
+                       'event_id' => 1,
+                       'event_type' => 'test',
+                       'event_variant' => '',
+                       'event_extra' => '',
+                       'event_page_id' => '',
+                       'event_agent_id' => '',
+                       'event_agent_ip' => '',
+                       'notification_user' => 1,
+                       'notification_timestamp' => '20140615101010',
+                       'notification_read_timestamp' => '20140616101010',
+                       'notification_bundle_base' => 1,
+                       'notification_bundle_hash' => 'testhash',
+                       'notification_bundle_display_hash' => 'testdisplayhash'
+               );
+               $notifMapper = new EchoNotificationMapper( 
$this->mockMWEchoDbFactory( array ( 'selectRow' => $dbResult ) ) );
+               $row = $notifMapper->fetchNewestByUserBundleHash( 
User::newFromId( 1 ), 'testdisplayhash' );
+               $this->assertInstanceOf( 'EchoNotification', $row );
+       }
+
+       /**
+        * Mock object of User
+        */
+       protected function mockUser() {
+               $user = $this->getMockBuilder( 'User' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $user->expects( $this->any() )
+                       ->method( 'getID' )
+                       ->will( $this->returnValue( 1 ) );
+               $user->expects( $this->any() )
+                       ->method( 'getOption' )
+                       ->will( $this->returnValue( true ) );
+               $user->expects( $this->any() )
+                       ->method( 'getGroups' )
+                       ->will( $this->returnValue( array( 'echo_group' ) ) );
+               return $user;
+       }
+
+       /**
+        * Mock object of EchoNotification
+        */
+       protected function mockEchoNotification() {
+               $event = $this->getMockBuilder( 'EchoNotification' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $event->expects( $this->any() )
+                       ->method( 'toDbArray' )
+                       ->will( $this->returnValue( array() ) );
+               return $event;
+       }
+
+       /**
+        * Mock object of MWEchoDbFactory
+        */
+       protected function mockMWEchoDbFactory( $dbResult ) {
+               $dbFactory = $this->getMockBuilder( 'MWEchoDbFactory' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $dbFactory->expects( $this->any() )
+                       ->method( 'getEchoDb' )
+                       ->will( $this->returnValue( $this->mockDb( $dbResult ) 
) );
+               return $dbFactory;
+       }
+
+       /**
+        * Mock object of DatabaseMysql ( DatabaseBase )
+        */
+       protected function mockDb( array $dbResult ) {
+               $dbResult += array(
+                       'insert' => '',
+                       'select' => '',
+                       'selectRow' => ''
+               );
+               $db = $this->getMockBuilder( 'DatabaseMysql' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $db->expects( $this->any() )
+                       ->method( 'insert' )
+                       ->will( $this->returnValue( $dbResult['insert'] ) );
+               $db->expects( $this->any() )
+                       ->method( 'select' )
+                       ->will( $this->returnValue( $dbResult['select'] ) );
+               $db->expects( $this->any() )
+                       ->method( 'selectRow' )
+                       ->will( $this->returnValue( $dbResult['selectRow'] ) );
+               $db->expects ( $this->any() )
+                       ->method( 'onTransactionIdle' )
+                       ->will( new EchoExecuteFirstArgumentStub );
+
+               return $db;
+       }
+
+}
+
+class EchoExecuteFirstArgumentStub implements 
PHPUnit_Framework_MockObject_Stub {
+       public function invoke( PHPUnit_Framework_MockObject_Invocation 
$invocation ) {
+               if ( !$invocation instanceof 
PHPUnit_Framework_MockObject_Invocation_Static ) {
+                       throw new PHPUnit_Framework_Exception( 'wrong 
invocation type' );
+               }
+               if ( !$invocation->arguments ) {
+                       throw new PHPUnit_Framework_Exception( 'Method call 
must have an argument' );
+               }
+               return call_user_func( reset( $invocation->arguments ) );
+       }
+       
+       public function toString() {
+               return 'return result of call_user_func on first invocation 
argument';
+       }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/147369
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I42f4d7566543332588431c21c220c0d64d026b70
Gerrit-PatchSet: 12
Gerrit-Project: mediawiki/extensions/Echo
Gerrit-Branch: master
Gerrit-Owner: Bsitu <[email protected]>
Gerrit-Reviewer: Bsitu <[email protected]>
Gerrit-Reviewer: EBernhardson <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to