diff --git a/app/commands/StatementMigrateCommand.php b/app/commands/StatementMigrateCommand.php
index fc421d49ec..2af1bab5fb 100644
--- a/app/commands/StatementMigrateCommand.php
+++ b/app/commands/StatementMigrateCommand.php
@@ -55,7 +55,7 @@ public function fire() {
         });
 
         // Uses the repository to migrate the statements.
-        $repo = App::make('Locker\Repository\Statement\EloquentStatementRepository');
+        $repo = App::make('Locker\Repository\Statement\EloquentVoider');
         $repo->updateReferences($statements_array, $lrs);
         $repo->voidStatements($statements_array, $lrs);
 
diff --git a/app/controllers/LrsController.php b/app/controllers/LrsController.php
index 3f7bb07034..9281f93318 100644
--- a/app/controllers/LrsController.php
+++ b/app/controllers/LrsController.php
@@ -1,7 +1,9 @@
 <?php
 
 use \Locker\Repository\Lrs\Repository as LrsRepo;
-use \Locker\Repository\Statement\StatementRepository as StatementRepo;
+use \Locker\Repository\Statement\Repository as StatementRepo;
+use \Locker\Repository\Statement\EloquentIndexer as StatementIndexer;
+use \Locker\Repository\Statement\IndexOptions as IndexOptions;
 
 class LrsController extends BaseController {
 
@@ -66,7 +68,7 @@ public function store() {
 
     //lrs input validation
     $rules['title']        = 'required';
-    $rules['description']  = '';       
+    $rules['description']  = '';
     $validator = \Validator::make($data, $rules);
     if ($validator->fails()) return \Redirect::back()->withErrors($validator);
 
@@ -103,7 +105,7 @@ public function update($lrs_id){
     $data = \Input::all();
 
     //lrs input validation
-    $rules['title'] = 'required';      
+    $rules['title'] = 'required';
     $validator = \Validator::make($data, $rules);
     if ($validator->fails()) {
       return \Redirect::back()->withErrors($validator);
@@ -123,7 +125,7 @@ public function update($lrs_id){
 
   /**
    * Display the specified resource.
-   * This is a temp hack until the single page app for 
+   * This is a temp hack until the single page app for
    * analytics is ready. v1.0 stable.
    * @param String $lrs_id
    * @return View
@@ -187,10 +189,13 @@ public function destroy($lrs_id){
    * @return View
    */
   public function statements($lrs_id){
-    $statements = $this->statement->index($lrs_id, [], [
+    $statements = (new StatementIndexer)->index(new IndexOptions([
+      'lrs_id' => $lrs_id,
       'ascending' => false,
-      'limit' => $this->statement->count($lrs_id)
-    ])->paginate(15);
+      'limit' => $this->statement->count([
+        'lrs_id' => $lrs_id
+      ])
+    ]))->paginate(15);
 
     return View::make('partials.statements.list', array_merge($this->getLrs($lrs_id), [
       'statements' => $statements,
@@ -240,7 +245,7 @@ public function editCredentials( $lrs_id ){
       $message_type = 'error';
       $message = trans('update_key_error');
     }
-    
+
     return Redirect::back()->with($message_type, $message);
   }
 
@@ -251,7 +256,7 @@ public function editCredentials( $lrs_id ){
    */
   public function users($lrs_id) {
     $opts = $this->getLrs($lrs_id);
-    return View::make('partials.users.list', array_merge($opts, [ 
+    return View::make('partials.users.list', array_merge($opts, [
       'users'    => $opts['lrs']->users,
       'user_nav' => true
     ]));
@@ -260,7 +265,7 @@ public function users($lrs_id) {
 
   public function inviteUsersForm($lrs_id) {
     $opts = $this->getLrs($lrs_id);
-    return View::make('partials.lrs.invite', array_merge($opts, [ 
+    return View::make('partials.lrs.invite', array_merge($opts, [
       'users'    => $opts['lrs']->users,
       'user_nav' => true
     ]));
diff --git a/app/controllers/SiteController.php b/app/controllers/SiteController.php
index bf0ef74a4b..3d74c06e51 100644
--- a/app/controllers/SiteController.php
+++ b/app/controllers/SiteController.php
@@ -2,7 +2,7 @@
 
 use Locker\Repository\Site\SiteRepository as SiteRepo;
 use Locker\Repository\Lrs\Repository as LrsRepo;
-use Locker\Repository\Statement\StatementRepository as StatementRepo;
+use Locker\Repository\Statement\Repository as StatementRepo;
 use Locker\Repository\User\UserRepository as UserRepo;
 
 class SiteController extends BaseController {
@@ -20,7 +20,7 @@ public function __construct(SiteRepo $site, LrsRepo $lrs, UserRepo $user, Statem
 
     $this->beforeFilter('auth');
     $this->beforeFilter('auth.super', array('except' => array('inviteUsers')));
-    $this->beforeFilter('csrf', array('only' => array('update', 'verifyUser', 'inviteUsers'))); 
+    $this->beforeFilter('csrf', array('only' => array('update', 'verifyUser', 'inviteUsers')));
   }
 
   /**
@@ -35,7 +35,7 @@ public function index(){
     $admin_dashboard = new \app\locker\data\dashboards\AdminDashboard();
 
     return View::make('partials.site.dashboard', [
-      'site' => $site, 
+      'site' => $site,
       'list' => $list,
       'stats' => $admin_dashboard->getFullStats(),
       'graph_data' => $admin_dashboard->getGraphData()
@@ -51,7 +51,7 @@ public function index(){
   public function edit($id){
     $site = $this->site->find($id);
     return View::make('partials.site.edit', [
-      'site' => $site, 
+      'site' => $site,
       'settings_nav' => true
     ]);
   }
@@ -122,7 +122,7 @@ public function lrs(){
     $lrss = $this->lrs->index($opts);
 
     return Response::json(array_map(function ($lrs) {
-      $lrs->statement_total = $this->statement->count($lrs->_id);
+      $lrs->statement_total = $this->statement->count(['lrs_id' => $lrs->_id]);
       return $lrs;
     }, $lrss));
   }
@@ -148,7 +148,7 @@ public function users() {
    */
   public function inviteUsersForm() {
     return View::make('partials.site.invite', [
-      'users_nav' => true, 
+      'users_nav' => true,
       'admin_dash' => true
     ]);
   }
diff --git a/app/controllers/StatementController.php b/app/controllers/StatementController.php
index a6fff286bf..2d9d59a4de 100644
--- a/app/controllers/StatementController.php
+++ b/app/controllers/StatementController.php
@@ -1,6 +1,6 @@
 <?php
 
-use Locker\Repository\Statement\StatementRepository as StatementRepo;
+use Locker\Repository\Statement\Repository as StatementRepo;
 use Locker\Repository\Lrs\Repository as LrsRepo;
 
 class StatementController extends BaseController {
diff --git a/app/controllers/xapi/ActivityController.php b/app/controllers/xapi/ActivityController.php
index 0969a37102..439a835ab7 100644
--- a/app/controllers/xapi/ActivityController.php
+++ b/app/controllers/xapi/ActivityController.php
@@ -1,7 +1,6 @@
 <?php namespace Controllers\xAPI;
 
 use \Locker\Repository\Document\DocumentRepository as Document;
-use \Locker\Repository\Activity\ActivityRepository as Activity;
 use Locker\Repository\Document\DocumentType as DocumentType;
 
 class ActivityController extends DocumentController {
@@ -22,9 +21,8 @@ class ActivityController extends DocumentController {
    * @param Document $document
    * @param Activity $activity
    */
-  public function __construct(Document $document, Activity $activity){
+  public function __construct(Document $document){
     parent::__construct($document);
-    $this->activity = $activity;
   }
 
   /**
diff --git a/app/controllers/xapi/BaseController.php b/app/controllers/xapi/BaseController.php
index b7e99ebe41..28bcea9ee6 100644
--- a/app/controllers/xapi/BaseController.php
+++ b/app/controllers/xapi/BaseController.php
@@ -38,17 +38,12 @@ public function setParameters(){
    * @return mixed Result of the method.
    */
   public function selectMethod() {
-    try {
-      switch ($this->method) {
-        case 'HEAD':
-        case 'GET': return $this->get();
-        case 'PUT': return $this->update();
-        case 'POST': return $this->store();
-        case 'DELETE': return $this->destroy();
-      }
-    } catch (\Exception $ex) {
-      $code = method_exists($ex, 'getStatusCode') ? $ex->getStatusCode() : 400;
-      throw new Exceptions\Exception($ex->getMessage(), $code, $ex);
+    switch ($this->method) {
+      case 'HEAD':
+      case 'GET': return $this->get();
+      case 'PUT': return $this->update();
+      case 'POST': return $this->store();
+      case 'DELETE': return $this->destroy();
     }
   }
 
diff --git a/app/controllers/xapi/StatementController.php b/app/controllers/xapi/StatementController.php
index fcfd1ecce2..70b7bc9be7 100644
--- a/app/controllers/xapi/StatementController.php
+++ b/app/controllers/xapi/StatementController.php
@@ -1,9 +1,9 @@
 <?php namespace Controllers\xAPI;
 
-use \Locker\Repository\Statement\StatementRepository as Statement;
-use \Locker\Repository\Query\QueryRepository as Query;
+use \Locker\Repository\Statement\Repository as Statement;
 use \Locker\Helpers\Attachments as Attachments;
 use \Locker\Helpers\Exceptions as Exceptions;
+use \Locker\Helpers\Helpers as Helpers;
 
 class StatementController extends BaseController {
 
@@ -22,10 +22,11 @@ class StatementController extends BaseController {
    * Constructs a new StatementController.
    * @param StatementRepository $statement
    */
-  public function __construct(Statement $statement, Query $query) {
+  public function __construct(Statement $statement) {
     parent::__construct();
-    $this->statement = $statement;
-    $this->query = $query;
+    $this->statements = $statement;
+    $this->index_controller = new StatementIndexController($statement);
+    $this->store_controller = new StatementStoreController($statement);
   }
 
   /**
@@ -53,286 +54,49 @@ public function get() {
   }
 
   /**
-   * Deals with multipart requests.
-   * @return ['content' => $content, 'attachments' => $attachments].
-   */
-  private function getParts() {
-    $content = \LockerRequest::getContent();
-    $contentType = \LockerRequest::header('content-type');
-    $types = explode(';', $contentType, 2);
-    $mimeType = count($types) >= 1 ? $types[0] : $types;
-
-    if ($mimeType == 'multipart/mixed') {
-      $components = Attachments::setAttachments($contentType, $content);
-
-      // Returns 'formatting' error.
-      if (empty($components)) {
-        throw new Exceptions\Exception('There is a problem with the formatting of your submitted content.');
-      }
-
-      // Returns 'no attachment' error.
-      if (!isset($components['attachments'])) {
-        throw new Exceptions\Exception('There were no attachments.');
-      }
-
-      $content = $components['body'];
-      $attachments = $components['attachments'];
-    } else {
-      $attachments = '';
-    }
-
-    return [
-      'content' => $content,
-      'attachments' => $attachments
-    ];
-  }
-
-  private function checkContentType() {
-    $contentType = \LockerRequest::header('Content-Type');
-    if ($contentType === null) {
-      throw new Exceptions\Exception('Missing Content-Type.');
-    }
-
-    $validator = new \app\locker\statements\xAPIValidation();
-    $validator->checkTypes('Content-Type', $contentType, 'contentType', 'headers');
-    if ($validator->getStatus() !== 'passed') {
-      throw new Exceptions\Exception(implode(',', $validator->getErrors()));
-    }
-  }
-
-  /**
-   * Stores (POSTs) a newly created statement in storage.
+   * Updates (PUTs) Statement with the given id.
    * @return Response
    */
-  public function store() {
-    // Validates request.
+  public function update() {
+    // Runs filters.
     if ($result = $this->checkVersion()) return $result;
-    if ($result = $this->checkContentType()) return $result;
-    if (\LockerRequest::hasParam(self::STATEMENT_ID)) {
-      throw new Exceptions\Exception('Statement ID parameter is invalid.');
-    }
-
-    $parts = $this->getParts();
-    $content = $parts['content'];
-    $attachments = $parts['attachments'];
-
-    $statements = json_decode($content);
-
-    if ($statements === null && $content != 'null' && $content != '') {
-      throw new Exceptions\Exception('Invalid JSON');
-    }
-
-    // Ensures that $statements is an array.
-    if (!is_array($statements)) {
-      $statements = [$statements];
-    }
-
-    // Saves $statements with $attachments.
-    return $this->statement->create(
-      $statements,
-      $this->lrs,
-      $attachments
-    );
+    return $this->store_controller->update($this->lrs->_id);
   }
 
   /**
    * Updates (PUTs) Statement with the given id.
    * @return Response
    */
-  public function update() {
+  public function store() {
     // Runs filters.
     if ($result = $this->checkVersion()) return $result;
-    if ($result = $this->checkContentType()) return $result;
-
-    $parts = $this->getParts();
-    $content = $parts['content'];
-    $attachments = $parts['attachments'];
-
-    // Decodes the statement.
-    $statement = json_decode($content);
-
-    if ($statement === null && $content != 'null' && $content != '') {
-      throw new Exceptions\Exception('Invalid JSON');
-    }
-
-    $statementId = \LockerRequest::getParam(self::STATEMENT_ID);
-
-    // Returns a error if identifier is not present.
-    if (!$statementId) {
-      throw new Exceptions\Exception('A statement ID is required to PUT.');
-    }
-
-    // Attempts to create the statement if `statementId` is present.
-    $statement->id = $statementId;
-    $this->statement->create([$statement], $this->lrs, $attachments);
-    return \Response::make('', 204);
+    return $this->store_controller->store($this->lrs->_id);
   }
 
   /**
    * Gets an array of statements.
-   * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#723-getstatements
-   * @return StatementResult
+   * @return Response
    */
   public function index() {
-    // Gets the filters from the request.
-    $filters = [
-      'agent' => $this->validatedParam('agent', 'agent'),
-      'activity' => $this->validatedParam('irl', 'activity'),
-      'verb' => $this->validatedParam('irl', 'verb'),
-      'registration' => $this->validatedParam('uuid', 'registration'),
-      'since' => $this->validatedParam('isoTimestamp', 'since'),
-      'until' => $this->validatedParam('isoTimestamp', 'until'),
-      'active' => $this->validatedParam('boolean', 'active', true),
-      'voided' => $this->validatedParam('boolean', 'voided', false)
-    ];
-
-
-    // Gets the options/flags from the request.
-    $options = [
-      'related_activities' => $this->validatedParam('boolean', 'related_activities', false),
-      'related_agents' => $this->validatedParam('boolean', 'related_agents', false),
-      'ascending' => $this->validatedParam('boolean', 'ascending', false),
-      'format' => $this->validatedParam('string', 'format', 'exact'),
-      'offset' => $this->validatedParam('int', 'offset', 0),
-      'limit' => $this->validatedParam('int', 'limit', 100),
-      'attachments' => $this->validatedParam('boolean', 'attachments', false)
-    ];
-
-    // Gets the $statements from the LRS (with the $lrsId) that match the $filters with the $options.
-    $statements = $this->statement->index(
-      $this->lrs->_id,
-      $filters,
-      $options
-    );
-
-    $total = $statements->count();
-
-    // Gets the statements and uses offset and limit options.
-    $statements->skip((int) $options['offset']);
-    $statements->take((int) $options['limit']);
-    $statements = $statements->get()->toArray();
-
-    // Selects an output format.
-    if ($options['format'] === 'ids') {
-      $statements = $this->statement->toIds($statements);
-    } else if ($options['format'] === 'canonical') {
-      $langs = \Request::header('Accept-Language');
-      $langs = $langs !== '' ? explode(',', $langs) : [];
-      $statements = $this->statement->toCanonical($statements, $langs);
-    }
-
-    // Returns the StatementResult object.
-    $statement_result = json_encode($this->makeStatementObject($statements, [
-      'total' => $total,
-      'offset' => $options['offset'],
-      'limit' => $options['limit']
-    ]));
-
-    if ($options['attachments'] === true) {
-      $boundary = 'abcABC0123\'()+_,-./:=?';
-      $content_type = 'multipart/mixed; boundary='.$boundary;
-      $statement_result = "Content-Type:application/json\r\n\r\n".$statement_result;
-      $body = "--$boundary\r\n".implode(
-        "\r\n--$boundary\r\n",
-        array_merge([$statement_result], $this->statement->getAttachments($statements, $this->lrs->_id))
-      )."\r\n--$boundary--";
-    } else {
-      $content_type = 'application/json;';
-      $body = $statement_result;
-    }
-
-    // Creates the response.
-    return \Response::make($body, BaseController::OK, [
-      'Content-Type' => $content_type,
-      'X-Experience-API-Consistent-Through' => $this->statement->getCurrentDate()
-    ]);;
+    return $this->index_controller->index($this->lrs->_id);
   }
 
   /**
    * Gets the statement with the given $id.
-   * @param UUID $id
+   * @param String $id Statement's UUID.
    * @param boolean $voided determines if the statement is voided.
-   * @return Statement
+   * @return Response
    */
   public function show($id, $voided = false) {
     // Runs filters.
     if ($result = $this->checkVersion()) return $result;
 
-    $statement = $this->statement->show($this->lrs->_id, $id, $voided)->first();
-    if ($statement) {
-      $dotted_statement = \Locker\Helpers\Helpers::replaceHtmlEntity(
-        $statement->statement
-      );
-      return \Response::json($dotted_statement, 200);
-    } else {
-      throw new Exceptions\NotFound($id, 'Statement');
-    }
-  }
-
-  /**
-   * Constructs a response for $statements.
-   * @param array $statements Statements to return.
-   * @param array $params Filter.
-   * @param array $debug Log for debgging information.
-   * @return response
-   **/
-  private function makeStatementObject(array $statements, array $options) {
-    // Merges options with default options.
-    $options = array_merge([
-      'total' => count($statements),
-      'offset' => null,
-      'limit' => null
-    ], $options);
-
-    // Replaces '&46;' in keys with '.' in statements.
-    // http://docs.learninglocker.net/docs/statements#quirks
-    $statements = $statements ?: [];
-    $statements = \Locker\Helpers\Helpers::replaceHtmlEntity($statements);
-    foreach ($statements as &$s) {
-      $s = $s->statement;
-    }
+    $statement = $this->statements->show($id, [
+      'lrs_id' => $this->lrs->_id,
+      'voided' => $voided
+    ]);
 
-    // Creates the statement result.
-    $statement_result = [
-      'more' => $this->getMoreLink($options['total'], $options['limit'], $options['offset']),
-      'statements' => $statements
-    ];
-
-    return $statement_result;
-  }
-
-  /**
-   * Constructs the "more link" for a statement response.
-   * @param Integer $total Number of statements that can be returned for the given request parameters.
-   * @param Integer $limit Number of statements to be outputted in the response.
-   * @param Integer $offset Number of statements being skipped.
-   * @return String A URL that can be used to get more statements for the given request parameters.
-   */
-  private function getMoreLink($total, $limit, $offset) {
-    // Uses defaults.
-    $total = $total ?: 0;
-    $limit = $limit ?: 100;
-    $offset = $offset ?: 0;
-
-    // Calculates the $next_offset.
-    $next_offset = $offset + $limit;
-    if ($total <= $next_offset) return '';
-
-    // Changes (when defined) or appends (when undefined) offset.
-    $query = \Request::getQueryString();
-    $statement_route = \URL::route('xapi.statement', [], false);
-    $current_url = $query ? $statement_route.'?'.$query : $statement_route;
-
-    if (strpos($query, "offset=$offset") !== false) {
-      return str_replace(
-        'offset=' . $offset,
-        'offset=' . $next_offset,
-        $current_url
-      );
-    } else {
-      $separator = strpos($current_url, '?') !== False ? '&' : '?';
-      return $current_url . $separator . 'offset=' . $next_offset;
-    }
+    return \Response::json(Helpers::replaceHtmlEntity($statement), 200);
   }
 
   /**
diff --git a/app/controllers/xapi/StatementIndexController.php b/app/controllers/xapi/StatementIndexController.php
new file mode 100644
index 0000000000..4d3f8c0439
--- /dev/null
+++ b/app/controllers/xapi/StatementIndexController.php
@@ -0,0 +1,138 @@
+<?php namespace Controllers\xAPI;
+
+use \Locker\Repository\Statement\Repository as StatementRepo;
+use \Locker\Helpers\Exceptions as Exceptions;
+use \Locker\Helpers\Helpers as Helpers;
+use \LockerRequest as LockerRequest;
+
+class StatementIndexController {
+
+  const BOUNDARY = 'abcABC0123\'()+_,-./:=?';
+  const EOL = "\r\n";
+
+  /**
+   * Constructs a new StatementIndexController.
+   * @param StatementRepo $statement_repo
+   */
+  public function __construct(StatementRepo $statement_repo) {
+    $this->statements = $statement_repo;
+  }
+
+  /**
+   * Gets an array of statements.
+   * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#723-getstatements
+   * @param String $lrs_id
+   * @return Response
+   */
+  public function index($lrs_id) {
+    // Gets the acceptable languages.
+    $langs = LockerRequest::header('Accept-Language', []);
+    $langs = is_array($langs) ? $langs : explode(',', $langs);
+    $langs = array_map(function ($lang) {
+      return explode(';', $lang)[0];
+    }, $langs);
+
+    // Gets the params.
+    $params = LockerRequest::all();
+    if (isset($params['agent'])) {
+      $decoded_agent = json_decode($params['agent']);
+      if ($decoded_agent !== null) {
+        $params['agent'] = $decoded_agent;
+      }
+    }
+
+    // Gets an index of the statements with the given options.
+    list($statements, $count, $opts) = $this->statements->index(array_merge([
+      'lrs_id' => $lrs_id,
+      'langs' => $langs
+    ], $params));
+
+    // Defines the content type and body of the response.
+    if ($opts['attachments'] === true) {
+      $content_type = 'multipart/mixed; boundary='.static::BOUNDARY;
+      $body = $this->makeAttachmentsResult($statements, $count, $opts);
+    } else {
+      $content_type = 'application/json;';
+      $body = $this->makeStatementsResult($statements, $count, $opts);
+    }
+
+    // Creates the response.
+    return \Response::make($body, 200, [
+      'Content-Type' => $content_type,
+      'X-Experience-API-Consistent-Through' => Helpers::getCurrentDate()
+    ]);;
+  }
+
+  /**
+   * Makes a statements result.
+   * @param [\stdClass] $statements
+   * @param Int $count
+   * @param [String => Mixed] $opts
+   * @return \stdClass
+   */
+  private function makeStatementsResult(array $statements, $count, array $opts) {
+    // Defaults to empty array of statements.
+    $statements = $statements ?: [];
+
+    // Replaces '&46;' in keys with '.' in statements.
+    // http://docs.learninglocker.net/docs/installation#quirks
+    $statements = Helpers::replaceHtmlEntity($statements);
+
+    // Creates the statement result.
+    $statement_result = (object) [
+      'more' => $this->getMoreLink($count, $opts['limit'], $opts['offset']),
+      'statements' => $statements
+    ];
+
+    return json_encode($statement_result);
+  }
+
+  /**
+   * Makes an attachments result.
+   * @param [\stdClass] $statements
+   * @param [String => Mixed] $opts
+   * @return \stdClass
+   */
+  private function makeAttachmentsResult(array $statements, $count, array $opts) {
+    $content_type = 'multipart/mixed; boundary='.static::BOUNDARY;
+    $statement_result = "Content-Type:application/json{static::EOL}{static::EOL}".$this->makeStatementsResult(
+      $statements,
+      $count,
+      $opts
+    );
+    $body = "--{static::BOUNDARY}{static::EOL}".implode(
+      "{static::EOL}--{static::BOUNDARY}{static::EOL}",
+      array_merge([$statement_result], array_map(function ($attachment) {
+        return (
+          'Content-Type:'.$attachment->content_type.static::EOL.
+          'Content-Transfer-Encoding:binary'.static::EOL.
+          'X-Experience-API-Hash:'.$attachment->hash.
+          static::EOL.static::EOL.
+          $attachment->content
+        );
+      }, $this->statements->getAttachments($statements, $opts)))
+    )."{static::EOL}--{static::BOUNDARY}--";
+  }
+
+  private function getMoreLink($count, $limit, $offset) {
+    // Calculates the $next_offset.
+    $next_offset = $offset + $limit;
+    if ($count <= $next_offset) return '';
+
+    // Changes (when defined) or appends (when undefined) offset.
+    $query = \Request::getQueryString();
+    $statement_route = \URL::route('xapi.statement', [], false);
+    $current_url = $query ? $statement_route.'?'.$query : $statement_route;
+
+    if (strpos($query, "offset=$offset") !== false) {
+      return str_replace(
+        'offset=' . $offset,
+        'offset=' . $next_offset,
+        $current_url
+      );
+    } else {
+      $separator = strpos($current_url, '?') !== False ? '&' : '?';
+      return $current_url . $separator . 'offset=' . $next_offset;
+    }
+  }
+}
diff --git a/app/controllers/xapi/StatementStoreController.php b/app/controllers/xapi/StatementStoreController.php
new file mode 100644
index 0000000000..34f94a544b
--- /dev/null
+++ b/app/controllers/xapi/StatementStoreController.php
@@ -0,0 +1,150 @@
+<?php namespace Controllers\xAPI;
+
+use \Locker\Repository\Statement\Repository as StatementRepo;
+use \Locker\Helpers\Attachments as Attachments;
+use \Locker\Helpers\Exceptions as Exceptions;
+use \Locker\Helpers\Helpers as Helpers;
+use \Locker\XApi\IMT as XApiImt;
+use \LockerRequest as LockerRequest;
+use \Response as IlluminateResponse;
+
+class StatementStoreController {
+
+  /**
+   * Constructs a new StatementStoreController.
+   * @param StatementRepo $statement_repo
+   */
+  public function __construct(StatementRepo $statement_repo) {
+    $this->statements = $statement_repo;
+  }
+
+  /**
+   * Deals with multipart requests.
+   * @return ['content' => $content, 'attachments' => $attachments].
+   */
+  private function getParts() {
+    $content = \LockerRequest::getContent();
+    $contentType = \LockerRequest::header('content-type');
+    $types = explode(';', $contentType, 2);
+    $mimeType = count($types) >= 1 ? $types[0] : $types;
+
+    if ($mimeType == 'multipart/mixed') {
+      $components = Attachments::setAttachments($contentType, $content);
+
+      // Returns 'formatting' error.
+      if (empty($components)) {
+        throw new Exceptions\Exception('There is a problem with the formatting of your submitted content.');
+      }
+
+      // Returns 'no attachment' error.
+      if (!isset($components['attachments'])) {
+        throw new Exceptions\Exception('There were no attachments.');
+      }
+
+      $content = $components['body'];
+      $attachments = $components['attachments'];
+    } else {
+      $attachments = [];
+    }
+
+    return [
+      'content' => $content,
+      'attachments' => $attachments
+    ];
+  }
+
+  /**
+   * Stores (POSTs) a newly created statement in storage.
+   * @param String $lrs_id
+   * @return Response
+   */
+  public function store($lrs_id) {
+    if (LockerRequest::hasParam(StatementController::STATEMENT_ID)) {
+      throw new Exceptions\Exception('Statement ID parameter is invalid.');
+    }
+
+    return IlluminateResponse::json($this->createStatements($lrs_id), 200, Helpers::getCORSHeaders());
+  }
+
+  /**
+   * Updates (PUTs) Statement with the given id.
+   * @param String $lrs_id
+   * @return Response
+   */
+  public function update($lrs_id) {
+    $this->createStatements($lrs_id, function ($statements) {
+      $statement_id = \LockerRequest::getParam(StatementController::STATEMENT_ID);
+
+      // Returns a error if identifier is not present.
+      if (!$statement_id) {
+        throw new Exceptions\Exception('A statement ID is required to PUT.');
+      }
+
+      // Adds the ID to the statement.
+      $statements[0]->id = $statement_id;
+      return $statements;
+    });
+
+    return IlluminateResponse::make('', 204, Helpers::getCORSHeaders());
+  }
+
+  /**
+   * Creates statements from the content of the request.
+   * @param String $lrs_id
+   * @param Callable|null $modifier A function that modifies the statements before storing them.
+   * @return AssocArray Result of storing the statements.
+   */
+  private function createStatements($lrs_id, Callable $modifier = null) {
+    Helpers::validateAtom(new XApiImt(LockerRequest::header('Content-Type')));
+
+    // Gets parts of the request.
+    $parts = $this->getParts();
+    $content = $parts['content'];
+
+    // Decodes $statements from $content.
+    $statements = json_decode($content);
+    if ($statements === null && $content !== '') {
+      throw new Exceptions\Exception('Invalid JSON');
+    } else if ($statements === null) {
+      $statements = [];
+    }
+
+    // Ensures that $statements is an array.
+    if (!is_array($statements)) {
+      $statements = [$statements];
+    }
+
+    // Runs the modifier if there is one and there are statements.
+    if (count($statements) > 0 && $modifier !== null) {
+      $statements = $modifier($statements);
+    }
+
+    // Saves $statements with attachments.
+    return $this->statements->store(
+      $statements,
+      is_array($parts['attachments']) ? $parts['attachments'] : [],
+      [
+        'lrs_id' => $lrs_id,
+        'authority' => $this->getAuthority()
+      ]
+    );
+  }
+
+  private function getAuthority() {
+    $client = (new \Client)
+      ->where('api.basic_key', \LockerRequest::getUser())
+      ->where('api.basic_secret', \LockerRequest::getPassword())
+      ->first();
+
+    if ($client != null && isset($client['authority'])) {
+      return json_decode(json_encode($client['authority']));
+    } else {
+      $site = \Site::first();
+      return (object) [
+        'name' => $site->name,
+        'mbox' => 'mailto:' . $site->email,
+        'objectType' => 'Agent'
+      ];
+    }
+  }
+}
diff --git a/app/locker/helpers/Attachments.php b/app/locker/helpers/Attachments.php
index bfb331b633..394ab6e841 100644
--- a/app/locker/helpers/Attachments.php
+++ b/app/locker/helpers/Attachments.php
@@ -1,5 +1,6 @@
 <?php namespace Locker\Helpers;
-
+use \Locker\Helpers\Exceptions as Exceptions;
+use \Locker\Repository\Document\FileTypes as FileTypes;
 
 class Attachments {
 
@@ -7,111 +8,99 @@ class Attachments {
    * Get statements and attachments from submitted content
    *
    * @param $content_type
-   * @param $incoming_statement
+   * @param $content
    *
    * @return array
    *
    **/
-  static function setAttachments( $content_type, $incoming_statement ){
-
-    $return     = array();
-    $sha_hashes = array();
-
-    //grab boundary from content_type header - @todo not sure which way is better?
-    preg_match('/boundary=(.*)$/', $content_type, $matches);
-    //if no boundary, abort
-    if( !isset($matches[1]) ){
-      \App::abort(400, 'You need to set a boundary if submitting attachments.');
-    }
-    $boundary = '--' . $matches[1];
+  static function setAttachments($content_type, $content) {
+    $return = ['body' => '', 'attachments' => []];
+    $sha_hashes = [];
+    $boundary = static::getBoundary($content_type);
 
     // Fetch each part of the multipart document
-    $parts = array_slice(explode($boundary, $incoming_statement), 1);
-    $data = array();
+    $parts = array_slice(explode($boundary, $content), 1);
     $raw_headers = $body = '';
 
-    //loop through all parts on the body
     foreach ($parts as $count => $part) {
-      // At the end of the file, break
+      // Stops at the end of the file.
       if ($part == "--") break;
 
       // Determines the delimiter.
-      $delim = "\n";
-      if (strpos($part, "\r".$delim) !== false) $delim = "\r".$delim;
+      $delim = strpos($part, "\r\n") !== false ? "\r\n" : "\n";
 
       // Separate body contents from headers
       $part = ltrim($part, $delim);
       list($raw_headers, $body) = explode($delim.$delim, $part, 2);
+      $headers = static::getHeaders($raw_headers, $delim);
 
-      // Parse headers and separate so we can access
-      $raw_headers = explode($delim, $raw_headers);
-      $headers     = array();
-      foreach ($raw_headers as $header) {
-        list($name, $value) = explode(':', $header);
-        $headers[strtolower($name)] = ltrim($value, ' ');
-      }
-
-      //the first part must be statements
-      if( $count == 0 ){
-        //this is part one, which must be statements
-        if( $headers['content-type'] !== 'application/json' ){
-          \App::abort(400, 'Statements must make up the first part of the body.');
-        }
-
-        //get sha2 hash from each statement
-        $set_body = json_decode($body, true);
-        if( is_array(json_decode($body)) ){
-          foreach($set_body as $a){
-            foreach($a['attachments'] as $attach){
-              $sha_hashes[] = $attach['sha2'];
-            }
-          }
-        }else{
-          foreach($set_body['attachments'] as $attach){
-            $sha_hashes[] = $attach['sha2'];
-          }
+      if ($count == 0) {
+        if (!isset($headers['content-type']) || $headers['content-type'] !== 'application/json') {
+          throw new Exceptions\Exception('Statements must make up the first part of the body.');
         }
 
-        //set body which will = statements
         $return['body'] = $body;
+      } else {
+        static::validateHeaders($headers, $sha_hashes);
+        $return['attachments'][$count] = (object) [
+          'hash' => $headers['x-experience-api-hash'],
+          'content_type' => $headers['content-type'],
+          'content' => $body
+        ];
+      }
+    }
 
-      }else{
-
-        //get the attachment type (Should this be required? @todo)
-        if( !isset($headers['content-type']) ){
-          \App::abort(400, 'You need to set a content type for your attachments.');
-        }
-
-        //get the correct ext if valid
-        $fileTypes = new \Locker\Repository\Document\FileTypes;
-        $ext = array_search( $headers['content-type'], $fileTypes::getMap() );
-        if( $ext === false ){
-          \App::abort(400, 'This file type cannot be supported');
-        }
-
-        //if content-transfer-encoding is not binary, reject attachment @todo
-        // if( !isset($headers['content-transfer-encoding']) || $headers['content-transfer-encoding'] !== 'binary' ){
-        //   \App::abort(400, 'This is the wrong encoding type');
-        // }
-
-        //check X-Experience-API-Hash is set, otherwise reject @todo
-        if( !isset($headers['x-experience-api-hash']) || $headers['x-experience-api-hash'] == ''){
-          \App::abort(400, 'Attachments require an api hash.');
-        }
-
-        //check x-experience-api-hash is contained within a statement
-        if( !in_array($headers['x-experience-api-hash'], $sha_hashes)){
-          \App::abort(400, 'Attachments need to contain x-experience-api-hash that is declared in statement.');
-        }
+    return $return;
+  }
 
-        $return['attachments'][$count] = $part;
+  /**
+   * Gets the boundary from the content type.
+   * @param String $raw_headers
+   * @param String $delim
+   * @return [String => Mixed]
+   */
+  private static function getHeaders($raw_headers, $delim) {
+    $raw_headers = explode($delim, $raw_headers);
+    $headers = [];
+    foreach ($raw_headers as $header) {
+      list($name, $value) = explode(':', $header);
+      $headers[strtolower($name)] = ltrim($value, ' ');
+    }
+    return $headers;
+  }
 
-      }
+  /**
+   * Gets the boundary from the content type.
+   * @param String $content_type
+   * @return String
+   */
+  private static function getBoundary($content_type) {
+    preg_match('/boundary=(.*)$/', $content_type, $matches);
+    if (!isset($matches[1])) throw new Exceptions\Exception(
+      'You need to set a boundary if submitting attachments.'
+    );
+    return '--'.$matches[1];
+  }
 
+  /**
+   * Validates the attachment headers.
+   * @param [String => Mixed] $headers
+   */
+  private static function validateHeaders(array $headers, array $sha_hashes) {
+    if (!isset($headers['content-type'])) {
+      throw new Exceptions\Exception('You need to set a content type for your attachments.');
     }
 
-    return $return;
+    //get the correct ext if valid
+    $ext = array_search($headers['content-type'], FileTypes::getMap());
+    if ($ext === false) {
+      throw new Exceptions\Exception('This file type cannot be supported');
+    }
 
+    //check X-Experience-API-Hash is set, otherwise reject @todo
+    if (!isset($headers['x-experience-api-hash']) || $headers['x-experience-api-hash'] == '') {
+      throw new Exceptions\Exception('Attachments require an api hash.');
+    }
   }
 
 }
diff --git a/app/locker/helpers/Helpers.php b/app/locker/helpers/Helpers.php
index c7e4203eae..f52f5485b9 100644
--- a/app/locker/helpers/Helpers.php
+++ b/app/locker/helpers/Helpers.php
@@ -127,10 +127,10 @@ static function getEnvVar($var) {
       $defaults = include base_path() . '/.env.php';
       $value = $defaults[$var];
     }
-    
+
     return $value;
   }
-  
+
   /**
    * Determines which identifier is currently in use in the given actor.
    * @param \stdClass $actor.
@@ -154,7 +154,49 @@ static function validateAtom(XAPIAtom $atom, $trace = null) {
     if (count($errors) > 0) {
       throw new Exceptions\Validation(array_map(function (XAPIError $error) use ($trace) {
         return (string) ($trace === null ? $error : $error->addTrace($trace));
-      }, $errors)));
+      }, $errors));
     }
   }
+
+  /**
+   * Makes a new UUID.
+   * @return String Generated UUID.
+   */
+  static function makeUUID() {
+    $remote_addr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'LL';
+    mt_srand(crc32(serialize([microtime(true), $remote_addr, 'ETC'])));
+
+    return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+      mt_rand(0, 0xffff), mt_rand(0, 0xffff),
+      mt_rand(0, 0xffff),
+      mt_rand(0, 0x0fff) | 0x4000,
+      mt_rand(0, 0x3fff) | 0x8000,
+      mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
+    );
+  }
+
+  /**
+   * Gets the current date and time in ISO format using the current timezone.
+   * @return String Current ISO date and time.
+   */
+  static function getCurrentDate() {
+    $current_date = \DateTime::createFromFormat('U.u', sprintf('%.4f', microtime(true)));
+    $current_date->setTimezone(new \DateTimeZone(\Config::get('app.timezone')));
+    return $current_date->format('Y-m-d\TH:i:s.uP');
+  }
+
+  /**
+   * Gets the CORS headers.
+   * @return [String => Mixed] CORS headers.
+   */
+  static function getCORSHeaders() {
+    return [
+      'Access-Control-Allow-Origin' => \Request::root(),
+      'Access-Control-Allow-Methods' => 'GET, PUT, POST, DELETE, OPTIONS',
+      'Access-Control-Allow-Headers' => 'Origin, Content-Type, Accept, Authorization, X-Requested-With, X-Experience-API-Version, X-Experience-API-Consistent-Through, Updated',
+      'Access-Control-Allow-Credentials' => 'true',
+      'X-Experience-API-Consistent-Through' => Helpers::getCurrentDate(),
+      'X-Experience-API-Version' => '1.0.1'
+    ];
+  }
 }
diff --git a/app/locker/repository/Activity/ActivityRepository.php b/app/locker/repository/Activity/ActivityRepository.php
deleted file mode 100644
index cf276d5165..0000000000
--- a/app/locker/repository/Activity/ActivityRepository.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php namespace Locker\Repository\Activity;
-
-interface ActivityRepository {
-
-  public function saveActivity( $activity_id, $activity_def );
-
-  public function getActivity( $activity_id );
-
-}
\ No newline at end of file
diff --git a/app/locker/repository/Activity/EloquentActivityRepository.php b/app/locker/repository/Activity/EloquentActivityRepository.php
deleted file mode 100644
index 150702b890..0000000000
--- a/app/locker/repository/Activity/EloquentActivityRepository.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php namespace Locker\Repository\Activity;
-
-use Activity;
-
-class EloquentActivityRepository implements ActivityRepository {
-
-  /**
-  * Activity
-  */
-  protected $activity;
-
-  /**
-   * Construct
-   *
-   * @param Activity $activity
-   */
-  public function __construct( Activity $activity ){
-
-    $this->activity = $activity;
-
-  }
-
-  /**
-   * This is a temp solution, we need something better depending
-   * on authority to update activity stored.
-   *
-   **/
-  public function saveActivity( $activity_id, $activity_def ){
-
-    $exists = \Activity::find( $activity_id );
-
-    //if the object activity exists, remove and update with recent
-    if( $exists ){
-      \Activity::where('_id', $activity_id)->delete(); 
-    }
-
-    //save record
-    \Activity::insert(
-      array('_id'        => $activity_id, 
-            'definition' => $activity_def)
-    );
-
-  }
-
-  public function getActivity( $activity_id ){
-    return  \Activity::where('_id', $activity_id)->first();
-  }
-
-}
\ No newline at end of file
diff --git a/app/locker/repository/RepositoryServiceProvider.php b/app/locker/repository/RepositoryServiceProvider.php
index 1cfbe5638b..6adfd48a5b 100644
--- a/app/locker/repository/RepositoryServiceProvider.php
+++ b/app/locker/repository/RepositoryServiceProvider.php
@@ -11,8 +11,8 @@ public function register(){
       'Locker\Repository\User\EloquentUserRepository'
     );
     $this->app->bind(
-      'Locker\Repository\Statement\StatementRepository',
-      'Locker\Repository\Statement\EloquentStatementRepository'
+      'Locker\Repository\Statement\Repository',
+      'Locker\Repository\Statement\EloquentRepository'
     );
     $this->app->bind(
       'Locker\Repository\Lrs\Repository',
@@ -34,10 +34,6 @@ public function register(){
       'Locker\Repository\Document\DocumentRepository',
       'Locker\Repository\Document\EloquentDocumentRepository'
     );
-    $this->app->bind(
-      'Locker\Repository\Activity\ActivityRepository',
-      'Locker\Repository\Activity\EloquentActivityRepository'
-    );
     $this->app->bind(
       'Locker\Repository\OAuthApp\OAuthAppRepository',
       'Locker\Repository\OAuthApp\EloquentOAuthAppRepository'
diff --git a/app/locker/repository/Statement/EloquentIndexer.php b/app/locker/repository/Statement/EloquentIndexer.php
index c97d1d33fc..a6a3f4a222 100644
--- a/app/locker/repository/Statement/EloquentIndexer.php
+++ b/app/locker/repository/Statement/EloquentIndexer.php
@@ -1,68 +1,202 @@
 <?php namespace Locker\Repository\Statement;
 
 use \Locker\Helpers\Exceptions as Exceptions;
+use \Locker\Helpers\Helpers as Helpers;
 use \Locker\XApi\Helpers as XApiHelpers;
 use \Jenssegers\Mongodb\Eloquent\Builder as Builder;
 use \Illuminate\Database\Eloquent\Model as Model;
 
-class EloquentIndexer extends EloquentReader {
+interface IndexerInterface {
+  public function index(IndexOptions $opts);
+  public function format(Builder $builder, IndexOptions $opts);
+  public function count(Builder $builder, IndexOptions $opts);
+}
+
+class EloquentIndexer extends EloquentReader implements IndexerInterface {
+
+  protected $formatter;
+
+  public function __construct() {
+    $this->formatter = new Formatter();
+  }
 
   /**
    * Gets all of the available models with the options.
-   * @param [String => Mixed] $opts
+   * @param IndexOptions $opts
    * @return [Model]
    */
-  public function index(array $opts) {
-    $opts = new IndexOptions($opts);
-    return $this->where($opts);
-  }
-
-  protected function where(IndexOptions $opts) {
-    $builder = $this->where($opts->options);
+  public function index(IndexOptions $opts) {
+    $builder = $this->where($opts);
 
     return $this->constructFilterOpts($builder, $opts, [
-      'agent' => function ($agent, IndexOptions $opts) {
-        Helpers::validateAtom(\Locker\XApi\Agent::createFromJSON($agent));
-        return $this->matchAgent($agent, $options);
+      'agent' => function ($value, $builder, IndexOptions $opts) {
+        return $this->matchAgent($value, $builder, $opts);
+      },
+      'activity' => function ($value, $builder, IndexOptions $opts) {
+        return $this->matchActivity($value, $builder, $opts);
+      },
+      'verb' => function ($value, $builder, IndexOptions $opts) {
+        return $this->addWhere($builder, 'verb.id', $value);
+      },
+      'registration' => function ($value, $builder, IndexOptions $opts) {
+        return $this->addWhere($builder, 'context.registration', $value);
+      },
+      'since' => function ($value, $builder, IndexOptions $opts) {
+        return $this->addWhere($builder, 'stored', $value, '>');
+      },
+      'until' => function ($value, $builder, IndexOptions $opts) {
+        return $this->addWhere($builder, 'stored', $value, '<=');
+      },
+      'active' => function ($value, $builder, IndexOptions $opts) {
+        return $builder->where('active', $value);
+      },
+      'voided' => function ($value, $builder, IndexOptions $opts) {
+        return $builder->where('voided', $value);
       }
     ]);
   }
 
+  /**
+   * Adds where to builder.
+   * @param Builder $builder
+   * @param String $key
+   * @param Mixed $value
+   * @return Builder
+   */
+  private function addWhere(Builder $builder, $key, $value, $op = '=') {
+    return $builder->where(function ($query) use ($key, $value, $op) {
+      return $query
+        ->orWhere('statement.'.$key, $op, $value)
+        ->orWhere('refs.'.$key, $op, $value);
+    });
+  }
+
+  /**
+   * Adds wheres to builder.
+   * @param Builder $builder
+   * @param [String] $keys
+   * @param Mixed $value
+   * @return Builder
+   */
+  private function addWheres(Builder $builder, array $keys, $value) {
+    return $builder->where(function ($query) use ($keys, $value) {
+      foreach ($keys as $key) {
+        $query->orWhere(function ($query) use ($key, $value) {
+          return $query
+            ->orWhere('statement.'.$key, $value)
+            ->orWhere('refs.'.$key, $value);
+        });
+      }
+      return $query;
+    });
+  }
+
+  /**
+   * Extends a given Builder using the given options and option builders.
+   * @param Builder $builder
+   * @param IndexOptions $opts.
+   * @param [String => Callable] $builders Option builders.
+   * @return Builder
+   */
+  private function constructFilterOpts(Builder $builder, IndexOptions $opts, array $builders) {
+    foreach ($builders as $opt => $opt_builder) {
+      $opt_value = $opts->getOpt($opt);
+      $builder = $opt_value === null ? $builder : $opt_builder($opt_value, $builder, $opts);
+    }
+    return $builder;
+  }
+
   /**
    * Formats statements.
    * @param Builder $builder
-   * @param [String => Mixed] $opts
+   * @param IndexOptions $opts
    * @return [Model] Formatted statements.
    */
-  public function format(Builder $builder, array $opts) {
-    if ($opts['format'] === 'exact') {
-      $formatter = function ($model, $opts) {
-        return $model;
+  public function format(Builder $builder, IndexOptions $opts) {
+    // Determines the formatter to be used.
+    $format = $opts->getOpt('format');
+    if ($format === 'exact') {
+      $formatter = function ($statement, $opts) {
+        return $statement;
       };
-    } else if ($opts['format'] === 'ids') {
-      $formatter = function ($model, $opts) {
-        return $this->formatter->identityStatement($model);
+    } else if ($format === 'ids') {
+      $formatter = function ($statement, $opts) {
+        return $this->formatter->identityStatement($statement);
       };
-    } else if ($opts['format'] === 'canonical') {
-      $formatter = function ($model, $opts) {
-        return $this->formatter->canonicalStatement($model, $opts['langs']);
+    } else if ($format === 'canonical') {
+      $formatter = function ($statement, $opts) {
+        return $this->formatter->canonicalStatement($statement, $opts->getOpt('langs'));
       };
     } else {
-      throw new Exceptions\Exception("`$opts['format']` is not a valid format.");
+      throw new Exceptions\Exception("`$format` is not a valid format.");
     }
 
-    return $builder->get()->each(function (Model $model) use ($opts) {
-      return $formatter($model, $opts);
+    // Returns the models.
+    return json_decode($builder
+      ->orderBy('statement.stored', $opts->getOpt('ascending') ? 'ASC' : 'DESC')
+      ->skip($opts->getOpt('offset'))
+      ->take($opts->getOpt('limit'))
+      ->get()
+      ->map(function (Model $model) use ($opts, $formatter) {
+        return $formatter($this->formatModel($model), $opts);
+      }));
+  }
+
+  /**
+   * Constructs a Mongo match using the given agent and options.
+   * @param String $agent Agent to be matched.
+   * @param IndexOptions $opts Index options.
+   * @return Builder
+   */
+  private function matchAgent($agent, Builder $builder, IndexOptions $opts) {
+    $id_key = Helpers::getAgentIdentifier($agent);
+    $id_val = $agent->{$id_key};
+
+    return $builder->where(function ($query) use ($id_key, $id_val, $builder, $opts) {
+      $keys = ["actor.$id_key", "actor.members.$id_key", "object.$id_key"];
+
+      if ($opts->getOpt('related_agents') === true) {
+        $keys = array_merge($keys, [
+          "authority.$id_key",
+          "context.instructor.$id_key",
+          "context.team.$id_key"
+        ]);
+      }
+
+      $query = $this->addWheres($builder, $keys, $id_val);
+    });
+  }
+
+  /**
+   * Constructs a Mongo match using the given activity and options.
+   * @param String $activity Activity to be matched.
+   * @param IndexOptions $opts Index options.
+   * @return Builder
+   */
+  private function matchActivity($activity, Builder $builder, IndexOptions $opts) {
+    return $builder->where(function ($query) use ($activity, $builder, $opts) {
+      $keys = ['object.id'];
+
+      if ($opts->getOpt('related_activities') === true) {
+        $keys = array_merge($keys, [
+          'context.contextActivities.parent.id',
+          'context.contextActivities.grouping.id',
+          'context.contextActivities.category.id',
+          'context.contextActivities.other.id'
+        ]);
+      }
+
+      $query = $this->addWheres($builder, $keys, $activity);
     });
   }
 
   /**
    * Counts statements.
    * @param Builder $builder
-   * @param [String => Mixed] $opts
+   * @param IndexOptions $opts
    * @return Int Number of statements in Builder.
    */
-  public function count(Builder $builder, array $opts) {
+  public function count(Builder $builder, IndexOptions $opts) {
     return $builder->count();
   }
 }
diff --git a/app/locker/repository/Statement/EloquentInserter.php b/app/locker/repository/Statement/EloquentInserter.php
new file mode 100644
index 0000000000..bd4084bc06
--- /dev/null
+++ b/app/locker/repository/Statement/EloquentInserter.php
@@ -0,0 +1,97 @@
+<?php namespace Locker\Repository\Statement;
+
+use \Locker\Helpers\Exceptions as Exceptions;
+use \Locker\Helpers\Helpers as Helpers;
+
+interface Inserter {
+  public function insert(array $statements, StoreOptions $opts);
+}
+
+class EloquentInserter extends EloquentReader implements Inserter {
+
+  /**
+   * Inserts statements with the given options.
+   * @param [\stdClass] $statements
+   * @param StoreOptions $opts
+   * @throws Exceptions\Conflict
+   */
+  public function insert(array $statements, StoreOptions $opts) {
+    $models = array_map(function (\stdClass $statement) use ($opts) {
+      $this->checkForConflict($statement, $opts);
+      return $this->constructModel($statement, $opts);
+    }, $statements);
+
+    return $this->insertModels($models, $opts);
+  }
+
+  /**
+   * Checks for a duplicate statement with the given options.
+   * @param [\stdClass] $statements
+   * @param StoreOptions $opts
+   * @throws Exceptions\Conflict
+   */
+  private function checkForConflict(\stdClass $statement, StoreOptions $opts) {
+    $duplicate = $this->where($opts)
+      ->where('statement.id', $statement->id)
+      ->where('active', true)
+      ->first();
+
+    if ($duplicate === null) return;
+    $this->compareForConflict($statement, $this->formatModel($duplicate));
+  }
+
+  /**
+   * Compares two statements.
+   * Throws Exceptions\Conflict if the two statements match.
+   * @param \stdClass $statement_x
+   * @param \stdClass $statement_y
+   * @throws Exceptions\Conflict
+   */
+  private function compareForConflict(\stdClass $statement_x, \stdClass $statement_y) {
+    $matchable_x = $this->matchableStatement($statement_x);
+    $matchable_y = $this->matchableStatement($statement_y);
+    if ($matchable_x != $matchable_y) {
+      throw new Exceptions\Conflict(
+        "Conflicts\r\n`{json_encode($statement_x)}`\r\n`{json_encode($statement_y)}`."
+      );
+    };
+  }
+
+  /**
+   * Decodes the encoded statement.
+   * Removes properties not necessary for matching.
+   * @param \stdClass $statement
+   * @return \stdClass $statement
+   */
+  private function matchableStatement(\stdClass $statement) {
+    $statement = json_decode(json_encode($statement));
+    unset($statement->stored);
+    unset($statement->authority);
+    return $statement;
+  }
+
+  /**
+   * Constructs a model from the given statement and options.
+   * @param \stdClass $statement
+   * @param StoreOptions $opts
+   * @return [String => Mixed] $model
+   */
+  private function constructModel(\stdClass $statement, StoreOptions $opts) {
+    return [
+      'lrs' => ['_id' => $opts->getOpt('lrs_id')],
+      'statement' => Helpers::replaceFullStop(json_decode(json_encode($statement), true)),
+      'active' => false,
+      'voided' => false,
+      'timestamp' => new \MongoDate(strtotime($statement->timestamp))
+    ];
+  }
+
+  /**
+   * Inserts models with the given options.
+   * @param [[String => Mixed]] $models
+   * @param StoreOptions $opts
+   */
+  private function insertModels(array $models, StoreOptions $opts) {
+    return $this->where($opts)->insert($models);
+  }
+}
diff --git a/app/locker/repository/Statement/EloquentLinker.php b/app/locker/repository/Statement/EloquentLinker.php
new file mode 100644
index 0000000000..29efe3c866
--- /dev/null
+++ b/app/locker/repository/Statement/EloquentLinker.php
@@ -0,0 +1,157 @@
+<?php namespace Locker\Repository\Statement;
+
+use \Illuminate\Database\Eloquent\Model as Model;
+use \Illuminate\Database\Eloquent\Collection as Collection;
+use \Locker\Helpers\Helpers as Helpers;
+
+interface LinkerInterface {
+  public function updateReferences(array $statements, StoreOptions $opts);
+}
+
+class EloquentLinker extends EloquentReader implements LinkerInterface {
+
+  private $to_update = [];
+  private $downed = null;
+
+  /**
+   * Updates statement references.
+   * @param [\stdClass] $statements
+   * @param StoreOptions $opts
+   */
+  public function updateReferences(array $statements, StoreOptions $opts) {
+    $this->voider = strpos(json_encode($statements), 'voided') !== false;
+    $this->downed = new Collection();
+    $this->to_update = array_map(function (\stdClass $statement) use ($opts) {
+      return $this->getModel($statement->id, $opts);
+    }, $statements);
+
+    while (count($this->to_update) > 0) {
+      $this->upLink($this->to_update[0], [], $opts);
+    }
+  }
+
+  /**
+   * Determines if a statement is a referencing statement.
+   * @param \stdClass $statement
+   * @return Boolean
+   */
+  protected function isReferencing(\stdClass $statement) {
+    return (
+      isset($statement->object->objectType) &&
+      $statement->object->objectType === 'StatementRef'
+    );
+  }
+
+  /**
+   * Gets the statement as an associative array from the database.
+   * @param String $statement_id Statement's UUID.
+   * @param StoreOptions $opts
+   * @return [Model]
+   */
+  protected function getModel($statement_id, StoreOptions $opts) {
+    $model = $this->where($opts)
+      ->where('statement.id', $statement_id)
+      ->first();
+
+    return $model;
+  }
+
+  /**
+   * Goes up the reference chain until it reaches the top then goes down setting references.
+   * @param Model $model
+   * @param [String] $visited IDs of statements visisted in the current chain (avoids infinite loop).
+   * @param StoreOptions $opts
+   * @return [Model]
+   */
+  private function upLink(Model $model, array $visited, StoreOptions $opts) {
+    $statement = $this->formatModel($model);
+    if (in_array($statement->id, $visited)) return [];
+    $visited[] = $statement->id;
+    $up_refs = $this->upRefs($statement, $opts);
+    if ($up_refs->count() > 0) {
+      return $up_refs->each(function ($up_ref) use ($opts, $visited) {
+        if ($this->downed->has($up_ref->_id)) return;
+        $this->downed->merge($this->upLink($up_ref, $visited, $opts));
+        return $up_ref;
+      })->values();
+    } else {
+      return $this->downLink($model, [], $opts);
+    }
+  }
+
+  /**
+   * Goes down the reference chain setting references (refs).
+   * @param Model $model
+   * @param [String] $visited IDs of statements visisted in the current chain (avoids infinite loop).
+   * @param StoreOptions $opts
+   * @return [Model]
+   */
+  private function downLink(Model $model, array $visited, StoreOptions $opts) {
+    $statement = $this->formatModel($model);
+    if (in_array($model, $visited)) {
+      return array_slice($visited, array_search($model, $visited));
+    }
+    $visited[] = $model;
+    $down_ref = $this->downRef($statement, $opts);
+    if ($down_ref !== null) {
+      $refs = $this->downLink($down_ref, $visited, $opts);
+      $this->setRefs($statement, $refs, $opts);
+      $this->unQueue($model);
+      return array_merge([$model], $refs);
+    } else {
+      $this->unQueue($model);
+      return [$model];
+    }
+  }
+
+  /**
+   * Gets the statements referencing the given statement.
+   * @param \stdClass $statement
+   * @param StoreOptions $opts
+   * @return [\stdClass]
+   */
+  private function upRefs(\stdClass $statement, StoreOptions $opts) {
+    return $this->where($opts)
+      ->where('statement.object.id', $statement->id)
+      ->where('statement.object.objectType', 'StatementRef')
+      ->get();
+  }
+
+  /**
+   * Gets the statement referred to by the given statement.
+   * @param \stdClass $statement
+   * @param StoreOptions $opts
+   * @return Model
+   */
+  private function downRef(\stdClass $statement, StoreOptions $opts) {
+    if (!$this->isReferencing($statement)) return null;
+    return $this->getModel($statement->object->id, $opts);
+  }
+
+  /**
+   * Updates the refs for the given statement.
+   * @param \stdClass $statement
+   * @param [\stdClass] $refs Statements that are referenced by the given statement.
+   * @param StoreOptions $opts
+   */
+  private function setRefs(\stdClass $statement, array $refs, StoreOptions $opts) {
+    $this->where($opts)
+      ->where('statement.id', $statement->id)
+      ->update([
+        'refs' => array_map(function ($ref) {
+          return Helpers::replaceFullStop(json_decode(json_encode($ref->statement), true));
+        }, $refs)
+      ]);
+  }
+
+  /**
+   * Unqueues the statement so that it doesn't get relinked.
+   * @param Model $model
+   */
+  private function unQueue(Model $model) {
+    $updated_index = array_search($model, $this->to_update);
+    if ($updated_index !== false) {
+      array_splice($this->to_update, $updated_index, 1);
+    }
+  }
+}
diff --git a/app/locker/repository/Statement/EloquentReader.php b/app/locker/repository/Statement/EloquentReader.php
index 5a8be9675d..5d9742043f 100644
--- a/app/locker/repository/Statement/EloquentReader.php
+++ b/app/locker/repository/Statement/EloquentReader.php
@@ -1,5 +1,8 @@
 <?php namespace Locker\Repository\Statement;
 
+use \Illuminate\Database\Eloquent\Model as Model;
+use \Locker\Helpers\Helpers as Helpers;
+
 abstract class EloquentReader {
   protected $model = '\Statement';
 
@@ -8,7 +11,16 @@ abstract class EloquentReader {
    * @param [String => Mixed] $opts
    * @return \Jenssegers\Mongodb\Eloquent\Builder
    */
-  protected function where(array $opts) {
-    return (new $this->model)->where('lrs', $opts['lrs_id']);
+  protected function where(Options $opts) {
+    return (new $this->model)->where('lrs._id', $opts->getOpt('lrs_id'));
+  }
+
+  /**
+   * Gets the statement from the model as an Object.
+   * @param Model $model
+   * @return \stdClass
+   */
+  protected function formatModel(Model $model) {
+    return Helpers::replaceHTMLEntity($model->statement);
   }
 }
diff --git a/app/locker/repository/Statement/EloquentRepository.php b/app/locker/repository/Statement/EloquentRepository.php
new file mode 100644
index 0000000000..39868a4f17
--- /dev/null
+++ b/app/locker/repository/Statement/EloquentRepository.php
@@ -0,0 +1,79 @@
+<?php namespace Locker\Repository\Statement;
+
+interface Repository {
+  public function store(array $statements, array $attachments, array $opts);
+  public function index(array $opts);
+  public function show($id, array $opts);
+  public function getAttachments(array $statements, array $opts);
+  public function count(array $opts);
+}
+
+class EloquentRepository implements Repository {
+
+  /**
+   * Constructs a new EloquentRepository for statements.
+   */
+  public function __construct() {
+    $this->storer = new EloquentStorer();
+    $this->indexer = new EloquentIndexer();
+    $this->shower = new EloquentShower();
+    $this->attacher = new FileAttacher();
+  }
+
+  /**
+   * Stores statements and attachments with the given options.
+   * @param [\stdClass] $statements
+   * @param [\stdClass] $attachments
+   * @param [String => Mixed] $opts
+   * @return [String] UUIDs of the stored statements.
+   */
+  public function store(array $statements, array $attachments, array $opts) {
+    return $this->storer->store($statements, $attachments, new StoreOptions($opts));
+  }
+
+  /**
+   * Gets all of the available models with the options.
+   * @param [String => Mixed] $opts
+   * @return [[\stdClass], Int, [String => Mixed]] Array containing the statements, count, and opts.
+   */
+  public function index(array $opts) {
+    $opts = new IndexOptions($opts);
+    $builder = $this->indexer->index($opts);
+    return [
+      $this->indexer->format($builder, $opts),
+      $this->indexer->count($builder, $opts),
+      $opts->options
+    ];
+  }
+
+  /**
+   * Gets the model with the given ID and options.
+   * @param String $id ID to match.
+   * @param [String => Mixed] $opts
+   * @return [\stdClass]
+   */
+  public function show($id, array $opts) {
+    return $this->shower->show($id, new ShowOptions($opts));
+  }
+
+  /**
+   * Gets the attachments for the given statements and options.
+   * @param [\stdClass] $statements
+   * @param [String => Mixed] $opts
+   * @return [\stdClass]
+   */
+  public function getAttachments(array $statements, array $opts) {
+    return $this->attacher->index($statements, new IndexOptions($opts));
+  }
+
+  /**
+   * Gets a count of all the statements available with the given options.
+   * @param [String => Mixed] $opts
+   * @return Int
+   */
+  public function count(array $opts) {
+    $opts = new IndexOptions($opts);
+    $builder = $this->indexer->index($opts);
+    return $this->indexer->count($builder, $opts);
+  }
+}
diff --git a/app/locker/repository/Statement/EloquentShower.php b/app/locker/repository/Statement/EloquentShower.php
new file mode 100644
index 0000000000..4086044e5e
--- /dev/null
+++ b/app/locker/repository/Statement/EloquentShower.php
@@ -0,0 +1,30 @@
+<?php namespace Locker\Repository\Statement;
+
+use \Locker\Helpers\Exceptions as Exceptions;
+use \Locker\XApi\Statement as XApiStatement;
+use \Illuminate\Database\Eloquent\Model as Model;
+
+interface ShowerInterface {
+  public function show($id, ShowOptions $opts);
+}
+
+class EloquentShower extends EloquentReader implements ShowerInterface {
+
+  /**
+   * Gets the model with the given ID and options.
+   * @param String $id ID to match.
+   * @param ShowOptions $opts
+   * @return Model
+   */
+  public function show($id, ShowOptions $opts) {
+    $model = $this->where($opts)
+      ->where('statement.id', $id)
+      ->where('voided', $opts->getOpt('voided'))
+      ->where('active', $opts->getOpt('active'))
+      ->first();
+
+    if ($model === null) throw new Exceptions\NotFound($id, $this->model);
+
+    return $this->formatModel($model);
+  }
+}
diff --git a/app/locker/repository/Statement/EloquentStatementRepository.php b/app/locker/repository/Statement/EloquentStatementRepository.php
deleted file mode 100644
index 4dd4a49706..0000000000
--- a/app/locker/repository/Statement/EloquentStatementRepository.php
+++ /dev/null
@@ -1,970 +0,0 @@
-<?php namespace Locker\Repository\Statement;
-
-use \DateTime;
-use \Statement;
-use \Locker\Repository\Activity\ActivityRepository as Activity;
-use \Locker\Repository\Query\QueryRepository as Query;
-use \Locker\Repository\Document\FileTypes;
-use \Illuminate\Database\Eloquent\Builder as Builder;
-use \Locker\Helpers\Exceptions as Exceptions;
-use \Locker\Helpers\Helpers as Helpers;
-
-class EloquentStatementRepository implements StatementRepository {
-
-  // Defines properties to be set to construtor parameters.
-  protected $statement, $activity, $query;
-  protected $sent_ids = array();
-
-  // Number of statements to return by default.
-  const DEFAULT_LIMIT = 100;
-
-  /**
-   * Constructs a new EloquentStatementRepository.
-   * @param Statement $statement
-   * @param Activity $activity
-   * @param Query $query
-   */
-  public function __construct(Statement $statement, Activity $activity, Query $query) {
-    $this->statement = $statement;
-    $this->activity  = $activity;
-    $this->query     = $query;
-  }
-
-  /**
-   * Gets the statement with the given $id from the lrs (with the $lrsId).
-   * @param UUID $lrsId
-   * @param UUID $id
-   * @param boolean $voided determines if the statement is voided.
-   * @param boolean $active determines if the statement is active.
-   * @return Builder
-   */
-  public function show($lrsId, $id, $voided = false, $active = true) {
-    return $this->query->where($lrsId, [
-      ['statement.id', '=', $id],
-      ['voided', '=', $voided],
-      ['active', '=', $active]
-    ]);
-  }
-
-  /**
-   * Gets statements from the lrs (with the $lrsId) that match the $filters.
-   * @param UUID $lrsId
-   * @param [StatementFilter] $filters
-   * @param [StatementFilter] $options
-   * @return Builder
-   */
-  public function index($lrsId, array $filters, array $options) {
-    $where = [];
-
-    // Defaults filters.
-    $filters = array_merge([
-      'agent' => null,
-      'activity' => null,
-      'verb' => null,
-      'registration' => null,
-      'since' => null,
-      'until' => null,
-      'active' => true,
-      'voided' => false
-    ], $filters);
-
-    // Defaults options.
-    $options = array_merge([
-      'related_activities' => false,
-      'related_agents' => false,
-      'ascending' => false,
-      'format' => 'exact',
-      'offset' => 0,
-      'limit' => self::DEFAULT_LIMIT
-    ], $options);
-
-    // Checks params.
-    if ($options['offset'] < 0) throw new Exceptions\Exception('`offset` must be a positive interger.');
-    if ($options['limit'] < 0) throw new Exceptions\Exception('`limit` must be a positive interger.');
-    if (!in_array($options['format'], ['ids', 'exact', 'canonical'])) {
-      throw new Exceptions\Exception('`format` must be `ids`, `exact` or `canonical`.');
-    }
-
-    // Filters by date.
-    if (isset($filters['since'])) $where[] = ['statement.stored', '>', $filters['since']];
-    if (isset($filters['until'])) $where[] = ['statement.stored', '<', $filters['until']];
-    if (isset($filters['active'])) $where[] = ['active', '=', $filters['active']];
-    if (isset($filters['voided'])) $where[] = ['voided', '=', $filters['voided']];
-    $statements = $this->query->where($lrsId, $where);
-
-    // Adds filters that don't have options.
-    $statements = $this->addFilter($statements, $filters['verb'], [
-      'statement.verb.id'
-    ]);
-    $statements = $this->addFilter($statements, $filters['registration'], [
-      'statement.context.registration'
-    ]);
-
-    // Filters by activity.
-    $statements = $this->addOptionFilter($statements, $filters['activity'], $options['related_activities'], [
-      'statement.object.id'
-    ], [
-      'statement.context.contextActivities.parent.id',
-      'statement.context.contextActivities.grouping.id',
-      'statement.context.contextActivities.category.id',
-      'statement.context.contextActivities.other.id'
-    ]);
-
-    // Filters by agent.
-    $agent = $filters['agent'];
-    $identifier = $this->getIdentifier($agent);
-    if (isset($agent) && !is_array($agent)) throw new Exceptions\Exception('Invalid agent');
-    $agent = isset($agent) && isset($agent[$identifier]) ? $agent[$identifier] : null;
-
-    // Fixes https://github.com/LearningLocker/learninglocker/issues/519.
-    if ($identifier === 'account') {
-      $statements = $this->addOptionFilter($statements, $agent['name'], $options['related_agents'], [
-        'statement.actor.'.$identifier.'.name',
-        'statement.object.'.$identifier.'.name'
-      ], [
-        'statement.authority.'.$identifier.'.name',
-        'statement.context.instructor.'.$identifier.'.name',
-        'statement.context.team.'.$identifier.'.name'
-      ]);
-      $statements = $this->addOptionFilter($statements, $agent['homePage'], $options['related_agents'], [
-        'statement.actor.'.$identifier.'.homePage',
-        'statement.object.'.$identifier.'.homePage'
-      ], [
-        'statement.authority.'.$identifier.'.homePage',
-        'statement.context.instructor.'.$identifier.'.homePage',
-        'statement.context.team.'.$identifier.'.homePage'
-      ]);
-    } else {
-      $statements = $this->addOptionFilter($statements, $agent, $options['related_agents'], [
-        'statement.actor.'.$identifier,
-        'statement.object.'.$identifier
-      ], [
-        'statement.authority.'.$identifier,
-        'statement.context.instructor.'.$identifier,
-        'statement.context.team.'.$identifier
-      ]);
-    }
-
-    // Uses ordering.
-    if (isset($options['ascending']) && $options['ascending'] === true) {
-      $statements = $statements->orderBy('statement.stored', 'ASC');
-    } else {
-      $statements = $statements->orderBy('statement.stored', 'DESC');
-    }
-
-    return $statements;
-  }
-
-  /**
-   * Gets the identifier of the agent.
-   * @param array $agent
-   * @return string identifier (mbox, openid, account).
-   */
-  private function getIdentifier($agent) {
-    if (isset($agent)) {
-      if (isset($agent['mbox'])) return 'mbox';
-      if (isset($agent['openid'])) return 'openid';
-      if (isset($agent['account'])) return 'account';
-      if (isset($agent['mbox_sha1sum'])) return 'mbox_sha1sum';
-    } else {
-      return 'actor';
-    }
-  }
-
-  /**
-   * Returns $statements where the $value matches any of the $keys.
-   * @param Builder $statements
-   * @param mixed $value
-   * @param array $keys
-   * @return Builder
-   */
-  private function addFilter(Builder $statements, $value, array $keys) {
-    if (!isset($value)) return $statements;
-
-    // Adds keys for sub statement and statement references.
-    foreach ($keys as $key) {
-      $keys[] = 'refs.'.substr($key, 10);
-    }
-
-    return $this->orWhere($statements, $value, $keys);
-  }
-
-  /**
-   * Filters $statements with an options.
-   * @param Builder $statements Statements to be filtered.
-   * @param mixed $value Value to match against $keys.
-   * @param boolean $use_broad
-   * @param array $specific Keys to be search regardless of $use_broad.
-   * @param array $broad Addtional keys to be searched when $use_broad is true.
-   * @return Builder
-   */
-  private function addOptionFilter(Builder $statements, $value, $use_broad, array $specific, array $broad) {
-    $keys = $specific;
-
-    if ($use_broad === true) {
-      $keys = array_merge($keys, $broad);
-
-      // Adds keys for sub statement.
-      foreach ($keys as $key) {
-        $keys[] = 'statement.object.'.substr($key, 10);
-      }
-    }
-
-    return $this->addFilter($statements, $value, $keys);
-  }
-
-  /**
-   * Returns $statements where the $value matches any of the $keys.
-   * @param Builder $statements
-   * @param mixed $value
-   * @param array $keys
-   * @return Builder
-   */
-  private function orWhere(Builder $statements, $value, array $keys) {
-    return $statements->where(function (Builder $query) use ($keys, $value) {
-      foreach ($keys as $key) {
-        $query->orWhere($key, $value);
-      }
-      return $query;
-    });
-  }
-
-  /**
-   * Converts statements in the "canonical" format as defined by the spec.
-   * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#723-getstatements
-   * @param [statements]
-   * @return [statements]
-   */
-  public function toCanonical(array $statements, array $langs) {
-    foreach ($statements as $index => $statement) {
-      $statements[$index]['statement'] = $this->getStatementCanonical($statement['statement'], $langs);
-    }
-    return $statements;
-  }
-
-  /**
-   * Converts statements in the "ids" format as defined by the spec.
-   * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#723-getstatements
-   * @param [statements]
-   * @return [statements]
-   */
-  public function toIds(array $statements) {
-    foreach ($statements as $index => $statement) {
-      $statements[$index]['statement'] = $this->getStatementIds($statement['statement']);
-    }
-    return $statements;
-  }
-
-  /**
-   * Attempts to convert a $langMap to a single string using a relevant language from $langs.
-   * @param [LanguageMap] $langMap
-   * @param [Language] $langs
-   * @return String/[LanguageMap]
-   */
-  private function canonicalise(array $langMap, array $langs) {
-    foreach ($langs as $lang) {
-      if (isset($langMap[$lang])) {
-        return $langMap[$lang];
-      }
-    }
-    return $langMap;
-  }
-
-  /**
-   * Canonicalises some parts of the $statement as defined by the spec using $langs.
-   * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#723-getstatements
-   * @param Statement $statement
-   * @param [Language] $langs
-   * @return Statement
-   */
-  private function getStatementCanonical(array $statement, array $langs) {
-    if (isset($statement['object']['definition']['name'])) {
-      $statement['object']['definition']['name'] = $this->canonicalise(
-        $statement['object']['definition']['name'],
-        $langs
-      );
-    }
-    if (isset($statement['object']['definition']['description'])) {
-      $statement['object']['definition']['description'] = $this->canonicalise(
-        $statement['object']['definition']['description'],
-        $langs
-      );
-    }
-    return $statement;
-  }
-
-  /**
-   * Gets the identifier key of an $agent.
-   * @param Agent $actor
-   * @return string
-   */
-  private function getAgentIdentifier($actor) {
-    if (isset($actor['mbox'])) return 'mbox';
-    if (isset($actor['account'])) return 'account';
-    if (isset($actor['openid'])) return 'openid';
-    if (isset($actor['mbox_sha1sum'])) return 'mbox_sha1sum';
-    return null;
-  }
-
-  /**
-   * Ids some parts of the $statement as defined by the spec.
-   * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#723-getstatements
-   * @param Statement $statement
-   * @return Statement
-   */
-  private function getStatementIds(array $statement) {
-    $actor = $statement['actor'];
-
-    // Processes an anonymous group or actor.
-    if (isset($actor['objectType']) && $actor['objectType'] === 'Group' && $this->getAgentIdentifier($actor) === null) {
-      $members = [];
-      foreach ($actor['members'] as $member) {
-        $identifier = $this->getAgentIdentifier($member);
-        $members[] = [
-          $identifier => $member[$identifier]
-        ];
-      }
-      $actor['members'] = $members;
-    } else {
-      $identifier = $this->getAgentIdentifier($actor);
-      $actor = [
-        $identifier => $actor[$identifier],
-        'objectType' => isset($actor['objectType']) ? $actor['objectType'] : 'Agent'
-      ];
-    }
-
-    // Replace parts of the statements.
-    $statement['actor'] = $actor;
-    $identifier = $this->getAgentIdentifier($statement['object']) ?: 'id';
-    $statement['object'] = [
-      $identifier => $statement['object'][$identifier],
-      'objectType' => isset($statement['object']['objectType']) ? $statement['object']['objectType'] : 'Activity'
-    ];
-
-    return $statement;
-  }
-
-  /**
-   * Constructs the authority.
-   * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#authority
-   * @return Authority
-   */
-  private function constructAuthority() {
-    $client = (new \Client)
-      ->where('api.basic_key', \LockerRequest::getUser())
-      ->where('api.basic_secret', \LockerRequest::getPassword())
-      ->first();
-
-    if ($client != null && isset($client['authority'])) {
-      return json_decode(json_encode($client['authority']));
-    } else {
-      $site = \Site::first();
-      return (object) [
-        'name' => $site->name,
-        'mbox' => 'mailto:' . $site->email,
-        'objectType' => 'Agent'
-      ];
-    }
-  }
-
-  /**
-   * Validates $statements.
-   * @param [Statement] $statements
-   * @return [Statement] Valid statements.
-   */
-  private function validateStatements(array $statements, \Lrs $lrs) {
-    $statements = $this->removeDuplicateStatements($statements, $lrs);
-    $authority = $this->constructAuthority();
-    $void_statements = [];
-
-    foreach ($statements as $index => $statement) {
-      $statement->setProp('authority', $authority);
-      $errors = array_map(function ($error) {
-        return (string) $error->addTrace('statement');
-      }, $statement->validate());
-
-      if (!empty($errors)) {
-        throw new Exceptions\Validation($errors);
-      } else {
-        if ($this->isVoiding($statement->getValue())) {
-          $void_statements[] = $statement->getPropValue('object.id');
-        }
-      }
-    }
-    if ($void_statements) {
-      $this->validateVoid($statements, $lrs, $void_statements);
-    }
-    return $statements;
-  }
-
-  /**
-   * Check that all void reference ids exist in the database and are not themselves void statements
-   * @param array $statements
-   * @param Lrs $lrs
-   * @param array $references
-   * @throws \Exception
-   */
-  private function validateVoid(array $statements, \Lrs $lrs, array $references) {
-    $count = count($references);
-    $reference_count = $this->statement
-      ->where('lrs._id', $lrs->_id)
-      ->whereIn('statement.id', $references)
-      ->where('statement.verb.id', '<>', "http://adlnet.gov/expapi/verbs/voided")
-      ->count();
-    if ($reference_count != $count) {
-      throw new Exceptions\Exception('Voiding invalid or nonexistant statement');
-    }
-  }
-
-  /**
-   * Remove duplicate statements and generate ids
-   *
-   * @param array $statements
-   * @param \Lrs $lrs
-   * @return array
-   */
-  private function removeDuplicateStatements(array $statements, \Lrs $lrs) {
-    $new_id_count = 0;
-    $new_statements = [];
-    $indexed_statements = [];
-    foreach($statements as $index => $statement) {
-      $statement_id = $statement->getPropValue('id');
-      if ($statement_id !== null) {
-        if (isset($this->sent_ids[$statement_id])) {
-          $sent_statement = json_encode($this->sent_ids[$statement_id]);
-          $current_statement = json_encode($statement);
-          $this->checkMatch($new_statement, $current_statement);
-          unset($statements[$index]);
-        } else {
-          $this->sent_ids[$statement_id] = $statement;
-          $indexed_statements[$statement_id] = $statement;
-        }
-      } else {
-        $new_statements[] = $statement;
-      }
-    }
-
-    if (count($new_statements)) {
-      $new_statements = $this->assignIds($new_statements, $lrs);
-      $indexed_statements = array_merge($indexed_statements, $new_statements);
-    }
-
-    return $indexed_statements;
-  }
-
-  /**
-   * @param array $statements
-   * @param \Lrs $lrs
-   * @return array List of statements with assigned id
-   */
-  private function assignIds(array $statements, \Lrs $lrs) {
-    $indexed_statements = [];
-    $count = count($statements);
-    $uuids = $this->generateIds($count + 1);
-    $duplicates = $this->checkIdsExist($uuids, $lrs);
-    if ($duplicates) {
-      $uuids = array_diff($uuids, $duplicates);
-    }
-    while(count($uuids) < $count) {
-      $new_uuids = $this->generateIds($count - count($uuids));
-      $duplicates = $this->checkIdsExist($new_uuids, $lrs);
-      if ($duplicates) {
-        $new_uuids = array_diff($uuids, $duplicates);
-        $uuids = array_merge($new_uuids);
-      }
-    }
-
-    foreach($statements as $statement) {
-      $uuid = array_pop($uuids);
-      $statement->setProp('id', $uuid);
-      $indexed_statements[$uuid] = $statement;
-    }
-    return $indexed_statements;
-  }
-
-  private function checkMatch($new_statement, $old_statement) {
-    $new_statement_obj = \Locker\XApi\Statement::createFromJson($new_statement);
-    $old_statement_obj = \Locker\XApi\Statement::createFromJson($old_statement);
-    $new_statement = json_decode($new_statement_obj->toJson(), true);
-    $old_statement = json_decode($old_statement_obj->toJson(), true);
-    array_multisort($new_statement);
-    array_multisort($old_statement);
-    ksort($new_statement);
-    ksort($old_statement);
-    unset($new_statement['stored']);
-    unset($old_statement['stored']);
-    if ($new_statement !== $old_statement) {
-      $new_statement = $new_statement_obj->toJson();
-      $old_statement = $old_statement_obj->toJson();
-      throw new Exceptions\Conflict(
-        "Conflicts\r\n`$new_statement`\r\n`$old_statement`."
-      );
-    };
-  }
-
-  /**
-   * Check lrs for list of statement ids, optional list of statements by id for comparison
-   *
-   * @param array $uuids
-   * @param \Lrs $lrs
-   * @param array $statements
-   * @return array List of duplicate ids
-   */
-  private function checkIdsExist(array $uuids, \Lrs $lrs, array $statements=null) {
-    $duplicates = array();
-
-    if ($uuids) {
-      $existingModels = $this->statement
-        ->where('lrs._id', $lrs->_id)
-        ->whereIn('statement.id', $uuids)
-        ->get();
-
-      if(!$existingModels->isEmpty()) {
-        foreach($existingModels as $existingModel) {
-          $existingStatement = $existingModel->statement;
-          $id = $existingStatement['id'];
-          $duplicates[] = $id;
-          if ($statements && isset($statements[$id])) {
-            $statement = $statements[$id];
-            $this->checkMatch($statement->toJson(), json_encode($existingStatement));
-          }
-        }
-      }
-    }
-    return $duplicates;
-  }
-
-  /**
-   * Generate an array of uuids of size $count
-   *
-   * @param integer $count
-   * @return array List of uuids
-   */
-  private function generateIds($count) {
-    $uuids = array();
-    $validator = new \app\locker\statements\xAPIValidation();
-    $i = 1;
-    while ($i <= $count) {
-      $uuid = $validator->makeUUID();
-      if (isset($this->sent_ids[$uuid])) {
-        continue;
-      }
-      $i++;
-      $uuids[] = $uuid;
-    }
-
-    return $uuids;
-  }
-
-  /**
-   * Create statements.
-   * @param [Statement] $statements
-   * @param Lrs $lrs
-   * @return array list of statements
-   */
-  private function createStatements(array $statements, \Lrs $lrs) {
-    if (count($this->sent_ids)) {
-      // check for duplicates from statements with pre-assigned ids
-      $this->checkIdsExist(array_keys($this->sent_ids), $lrs, $statements);
-    }
-
-    // Replaces '.' in keys with '&46;'.
-    $statements = array_map(function (\Locker\XApi\Statement $statement) use ($lrs) {
-      $replaceFullStop = function ($object, $replaceFullStop) {
-        if ($object instanceof \Locker\XApi\Element) {
-          $prop_keys = array_keys(get_object_vars($object->getValue()));
-          foreach ($prop_keys as $prop_key) {
-            $new_prop_key = str_replace('.', '&46;', $prop_key);
-            $prop_value = $object->getProp($prop_key);
-            $new_value = $replaceFullStop($prop_value, $replaceFullStop);
-            $object->unsetProp($prop_key);
-            $object->setProp($new_prop_key, $new_value);
-          }
-          return $object;
-        } else {
-          return $object;
-        }
-      };
-      $replaceFullStop($statement, $replaceFullStop);
-
-      return $this->makeStatement($statement, $lrs);
-    }, $statements);
-
-    $this->statement->where('lrs._id', $lrs->id)->insert(array_values($statements));
-    return $statements;
-  }
-
-  /**
-   * Sets references
-   * @param array $statements
-   * @param \Lrs $lrs
-   * @return array list of statements with references
-   */
-  public function updateReferences(array $statements, \Lrs $lrs) {
-    foreach($statements as $id => $statement) {
-      if ($this->isReferencing($statement['statement'])) {
-        // Finds the statement that it references.
-        $refs = [];
-        $this->recursiveCheckReferences($statements, $lrs, $refs, $statement['statement']->object->id);
-        // Updates the refs.
-        if ($refs) {
-          $refs = array_values($refs);
-          $statements[$id]['refs'] = $refs;
-          $this->statement
-            ->where('lrs._id', $lrs->id)
-            ->where('statement.id', $id)->update([
-              'refs' => $refs
-            ]);
-        }
-      }
-    }
-    $this->updateReferrers($statements, $lrs);
-    return $statements;
-  }
-
-  private function recursiveCheckReferences(array $statements, \Lrs $lrs, array &$refs, $id) {
-    // check if $id refers to a statement being inserted
-    if (isset($refs[$id])) {
-      return $refs;
-    }
-
-    if (isset($statements[$id])) {
-      $s = $statements[$id];
-      $refs[$id] = $s->statement;
-      if ($this->isReferencing($s->statement)) {
-        $s_id = $s->statement->getPropValue('object.id');
-        $this->recursiveCheckReferences($statements, $lrs, $refs, $s_id);
-      }
-    } else {
-      $reference = $this->query->where($lrs->_id, [
-        ['statement.id', '=', $id]
-      ])->first();
-      if ($reference) {
-        $refs[$id] = $reference->statement;
-        if ($this->isReferencing((object) $reference->statement)) {
-          $s_id = $reference->statement['object']['id'];
-          $this->recursiveCheckReferences($statements, $lrs, $refs, $s_id);
-        }
-      }
-    }
-    return $refs;
-  }
-
-  /**
-   * Adds statement to refs in a existing referrer.
-   * @param [Statement] $statements
-   * @return [Statement]
-   */
-  private function updateReferrers(array $statements, \Lrs $lrs) {
-    if (count($this->sent_ids)) {
-      $referrers = $this->query->where($lrs->_id, [
-          ['statement.object.id', 'in', array_keys($statements)],
-          ['statement.object.objectType', '=', 'StatementRef'],
-      ])->get();
-
-      // Updates the refs $referrers.
-      foreach ($referrers as $referrer) {
-        $statement_id = $referrer['statement']['object']['id'];
-        $statement = $statements[$statement_id];
-        if (isset($statement['refs'])) {
-          $referrer->refs = array_merge([$statement['statement']], $statement['refs']);
-        } else {
-          $referrer->refs = [$statement['statement']];
-        }
-        if (!$referrer->save()) throw new Exceptions\Exception('Failed to save referrer.');
-      }
-    }
-    return $statements;
-  }
-
-  private function isReferencing(\stdClass $statement) {
-    return (
-      isset($statement->object->id) &&
-      isset($statement->object->objectType) &&
-      $statement->object->objectType === 'StatementRef'
-    );
-  }
-
-  /**
-   * Determines if a $statement voids another.
-   * @param Statement $statement
-   * @return boolean
-   */
-  private function isVoiding(\stdClass $statement) {
-    if (($statement->verb->id === 'http://adlnet.gov/expapi/verbs/voided') && $this->isReferencing($statement)) {
-      return true;
-    }
-    return false;
-  }
-
-  private function voidStatement($statement, $lrs) {
-    if (!$this->isVoiding($statement['statement'])) return $statement;
-    $reference = $this->query->where($lrs->_id, [
-        ['statement.id', '=', $statement['statement']->object->id]
-    ])->first();
-    $ref_statement = json_decode(json_encode($reference->statement));
-    if ($this->isVoiding($ref_statement)) {
-       throw new Exceptions\Exception('Cannot void a voiding statement');
-    }
-    $reference->voided = true;
-    if (!$reference->save()) throw new Exceptions\Exception('Failed to void statement.');
-    return $statement;
-  }
-
-  public function voidStatements(array $statements, \Lrs $lrs) {
-    return array_map(function (array $statement) use ($lrs) {
-      return $this->voidStatement($statement, $lrs);
-    }, $statements);
-  }
-
-  public function activateStatements(array $statements, \Lrs $lrs) {
-    $updated = $this->statement->where('lrs._id', $lrs->id)->whereIn('statement.id', array_keys($statements))->update(array('active' => true));
-  }
-
-  /**
-   * Creates $statements in the $lrs with $attachments.
-   * @param [Statement] $statements
-   * @param \LRS $lrs
-   * @param string $attachments
-   * @return array create result (see makeCreateResult function)
-   */
-  public function create(array $statements, \Lrs $lrs, $attachments = '') {
-    $statements = array_map(function (\stdClass $statement) {
-      return new \Locker\XApi\Statement($statement);
-    }, $statements);
-    $statements = $this->validateStatements($statements, $lrs);
-    $statements = $this->createStatements($statements, $lrs);
-    $statements = $this->updateReferences($statements, $lrs);
-    $statements = $this->voidStatements($statements, $lrs);
-    $this->activateStatements($statements, $lrs);
-
-    // Stores the $attachments.
-    if ($attachments != '') {
-      $this->storeAttachments($attachments, $lrs->_id);
-    }
-    return array_keys($statements);
-  }
-
-  /**
-   * Validates a $statement with an $authority.
-   * @param Statement $statement
-   * @param Authority $Authority
-   * @return Validator
-   */
-  private function validateStatement(array $statement, array $authority) {
-    return (new \app\locker\statements\xAPIValidation())->runValidation(
-      $statement,
-      $authority
-    );
-  }
-
-  /**
-   * Makes a $statement for the current $lrs.
-   * @param Statement $statement
-   * @param LRS $lrs
-   * @return Statement
-   */
-  private function makeStatement(\Locker\XApi\Statement $statement, \Lrs $lrs) {
-    // Uses defaults where possible.
-    $currentDate = $this->getCurrentDate();
-    $statement->setProp('stored', $currentDate);
-    if ($statement->getPropValue('timestamp') === null) {
-      $statement->setProp('timestamp', $currentDate);
-    }
-
-    // For now we store the latest submitted definition.
-    // @todo this will change when we have a way to determine authority to edit.
-    if ($statement->getPropValue('object.definition') !== null) {
-      $this->activity->saveActivity(
-        $statement->getPropValue('object.id'),
-        $statement->getPropValue('object.definition')
-      );
-    }
-    // Create a new statement model
-    return [
-      'lrs' => [
-        '_id' => $lrs->_id,
-        'name' => $lrs->title
-      ],
-      'statement' => $statement->getValue(),
-      'active' => false,
-      'voided' => false,
-      'timestamp' => new \MongoDate(strtotime($statement->getPropValue('timestamp')))
-    ];
-  }
-
-  /**
-   * Make an associative array that represents the result of creating statements.
-   * @param [StatementId] $ids Array of IDs of successfully created statements.
-   * @param boolean $success
-   * @param string $description Description of the result.
-   * @return array create result.
-   */
-  private function makeCreateResult(array $ids, $success = false, $description = '') {
-    return [
-      'success' => $success,
-      'ids' => $ids,
-      'message' => $description
-    ];
-  }
-
-  /**
-   * Calculates the current date(consistent through xAPI header).
-   * @return string
-   */
-  public function getCurrentDate() {
-    $current_date = \DateTime::createFromFormat('U.u', sprintf('%.4f', microtime(true)));
-    $current_date->setTimezone(new \DateTimeZone(\Config::get('app.timezone')));
-    return $current_date->format('Y-m-d\TH:i:s.uP');
-  }
-
-  /**
-   * Replace `.` with `&46;` in keys of a $statement.
-   * @param Statement $statement
-   * @return Statement
-   */
-  private function replaceFullStop(array $statement){
-    return \Locker\Helpers\Helpers::replaceFullStop($statement);
-  }
-
-  /**
-   * Check to see if a submitted statementId already exist and if so
-   * are the two statements idntical? If not, return true.
-   *
-   * @param uuid   $id
-   * @param string $lrs
-   * @return boolean
-   *
-   **/
-  private function doesStatementIdExist($lrsId, array $statement) {
-    $existingModel = $this->statement
-      ->where('lrs._id', $lrsId)
-      ->where('statement.id', $statement['id'])
-      ->first();
-
-    if ($existingModel) {
-      $existingStatement = json_encode($existingModel->statement);
-      $this->checkMatch($existingStatement, json_encode($statement));
-      return $existingModel;
-    }
-
-    return null;
-  }
-
-  public function getAttachments($statements, $lrs) {
-    $destination_path = Helpers::getEnvVar('LOCAL_FILESTORE').'/'.$lrs.'/attachments/';
-    $attachments = [];
-
-    foreach ($statements as $statement) {
-      $statement = $statement['statement'];
-      $attachments = array_merge($attachments, array_map(function ($attachment) use ($destination_path) {
-        $ext = array_search($attachment['contentType'], FileTypes::getMap());
-        $filename = $attachment['sha2'].'.'.$ext;
-        return (
-          'Content-Type:'.$attachment['contentType']."\r\n".
-          'Content-Transfer-Encoding:binary'."\r\n".
-          'X-Experience-API-Hash:'.$attachment['sha2'].
-          "\r\n\r\n".
-          file_get_contents($destination_path.$filename)
-        );
-      }, isset($statement['attachments']) ? $statement['attachments'] : []));
-    }
-
-    return $attachments;
-  }
-
-  /**
-   * Store any attachments
-   *
-   **/
-  private function storeAttachments( $attachments, $lrs ){
-    foreach ($attachments as $attachment) {
-      // Determines the delimiter.
-      $delim = "\n";
-      if (strpos($attachment, "\r".$delim) !== false) $delim = "\r".$delim;
-
-      // Separate body contents from headers
-      $attachment = ltrim($attachment, $delim);
-      list($raw_headers, $body) = explode($delim.$delim, $attachment, 2);
-
-      // Parse headers and separate so we can access
-      $raw_headers = explode($delim, $raw_headers);
-      $headers = [];
-      foreach ($raw_headers as $header) {
-        list($name, $value) = explode(':', $header);
-        $headers[strtolower($name)] = ltrim($value, ' ');
-      }
-
-      //get the correct ext if valid
-      $ext = array_search($headers['content-type'], FileTypes::getMap());
-      if ($ext === false)  throw new Exceptions\Exception(
-        'This file type cannot be supported'
-      );
-
-      $destination_path = Helpers::getEnvVar('LOCAL_FILESTORE').'/'.$lrs.'/attachments/';
-      $filename = $headers['x-experience-api-hash'];
-
-      //create directory if it doesn't exist
-      if (!\File::exists($destination_path)) {
-        \File::makeDirectory($destination_path, 0775, true);
-      }
-
-      $filename = $destination_path.$filename.'.'.$ext;
-      $file = fopen($filename, 'wb'); //opens the file for writing with a BINARY (b) fla
-      $size = fwrite($file, $body); //write the data to the file
-      fclose($file);
-
-      if($size === false) throw new Exceptions\Exception(
-        'There was an issue saving the attachment'
-      );
-    }
-
-  }
-
-  /**
-   * Count statements for any give lrs
-   * @param string Lrs
-   * @param array parameters Any parameters for filtering
-   * @return count
-   **/
-  public function count( $lrs, $parameters=null ){
-    $query = $this->statement->where('lrs._id', $lrs);
-    if(!is_null($parameters)){
-      $this->addParameters( $query, $parameters, true );
-    }
-    $count = $query->count();
-    $query->remember(5);
-    return $count;
-  }
-
-  public function grouped($id, $parameters){
-    $type = isset($parameters['grouping']) ? strtolower($parameters['grouping']) : '';
-
-    switch ($type) {
-      case "time":
-        $interval = isset($parameters['interval']) ? $parameters['interval'] : "day";
-        $filters = isset($parameters['filters']) ? json_decode($parameters['filters'], true) : array();
-        $filters['lrs._id'] = $id;
-        $results = $this->query->timedGrouping( $filters, $interval );
-        break;
-    }
-
-    return $results;
-  }
-
-  public function timeGrouping($query, $interval ){
-    return $query;
-  }
-
-  public function actorGrouping($query){
-    $query->aggregate(
-      array('$match' => array()),
-      array(
-        '$group' => array(
-          '_id' => 'statement.actor.mbox'
-        )
-      )
-    );
-    return $query;
-  }
-}
diff --git a/app/locker/repository/Statement/EloquentStorer.php b/app/locker/repository/Statement/EloquentStorer.php
new file mode 100644
index 0000000000..e025a1af30
--- /dev/null
+++ b/app/locker/repository/Statement/EloquentStorer.php
@@ -0,0 +1,96 @@
+<?php namespace Locker\Repository\Statement;
+
+use \Locker\Helpers\Helpers as Helpers;
+use \Locker\XApi\Statement as XAPIStatement;
+
+interface Storer {
+  public function store(array $statements, array $attachments, StoreOptions $opts);
+}
+
+class EloquentStorer extends EloquentReader implements Storer {
+
+  protected $inserter, $linker, $voider, $attacher, $hashes;
+
+  public function __construct() {
+    $this->inserter = new EloquentInserter();
+    $this->linker = new EloquentLinker();
+    $this->voider = new EloquentVoider();
+    $this->attacher = new FileAttacher();
+  }
+
+  /**
+   * Stores statements and attachments with the given options
+   * @param [\stdClass] $statements
+   * @param [String => Mixed] $attachments
+   * @param StoreOptions $opts
+   * @return [String] UUIDs of the statements stored.
+   */
+  public function store(array $statements, array $attachments, StoreOptions $opts) {
+    $id_statements = $this->constructValidStatements($statements, $opts);
+    $ids = array_keys($id_statements);
+    $statements = array_values($id_statements);
+
+    $this->inserter->insert($statements, $opts);
+    $this->linker->updateReferences($statements, $opts);
+    $this->voider->voidStatements($statements, $opts);
+    $this->attacher->store($attachments, $this->hashes, $opts);
+    $this->activateStatements($ids, $opts);
+
+    return $ids;
+  }
+
+  /**
+   * Constructs valid statements.
+   * @param [\stdClass] $statements
+   * @param StoreOptions $opts
+   * @return [String => \stdClass] Array of statements mapped to their UUIDs.
+   */
+  private function constructValidStatements(array $statements, StoreOptions $opts) {
+    $constructed = [];
+    $this->hashes = [];
+
+    foreach ($statements as $statement) {
+      $statement->authority = $opts->getOpt('authority');
+      $statement->stored = Helpers::getCurrentDate();
+
+      if (!isset($statement->timestamp)) {
+        $statement->timestamp = $statement->stored;
+      }
+
+      if (!isset($statement->id)) {
+        $statement->id = Helpers::makeUUID();
+      }
+
+      // Validates statement.
+      $constructed_statement = new XAPIStatement($statement);
+      Helpers::validateAtom($constructed_statement, 'statement');
+      $statement = $constructed_statement->getValue();
+
+      // Gets attachment hashes.
+      $attachments = !isset($statement->attachments) ? [] : $statement->attachments;
+      foreach ($attachments as $attachment) {
+        $this->hashes[] = $attachment->sha2;
+      }
+
+      // Adds $statement to $constructed.
+      if (isset($constructed[$statement->id])) {
+        $this->inserter->compareForConflict($statement, $constructed[$statement->id]);
+      } else {
+        $constructed[$statement->id] = $statement;
+      }
+    }
+
+    return $constructed;
+  }
+
+  /**
+   * Activates the statements using their UUIDs.
+   * @param [String] $ids UUIDs of the statements to be activated.
+   * @param StoreOptions $opts
+   */
+  private function activateStatements(array $ids, StoreOptions $opts) {
+    return $this->where($opts)
+      ->whereIn('statement.id', $ids)
+      ->update(['active' => true]);
+  }
+}
diff --git a/app/locker/repository/Statement/EloquentVoider.php b/app/locker/repository/Statement/EloquentVoider.php
new file mode 100644
index 0000000000..3f08b11fd5
--- /dev/null
+++ b/app/locker/repository/Statement/EloquentVoider.php
@@ -0,0 +1,59 @@
+<?php namespace Locker\Repository\Statement;
+
+use \Locker\Helpers\Exceptions as Exceptions;
+use \Illuminate\Database\Eloquent\Model as Model;
+
+interface VoiderInterface {
+  public function voidStatements(array $statements, StoreOptions $opts);
+}
+
+class EloquentVoider extends EloquentLinker implements VoiderInterface {
+
+  /**
+   * Voids statements that need to be voided.
+   * @param [\stdClass] $statements
+   * @param StoreOptions $opts
+   */
+  public function voidStatements(array $statements, StoreOptions $opts) {
+    return array_map(function (\stdClass $voider) use ($opts) {
+      return $this->voidStatement($voider, $opts);
+    }, $statements);
+  }
+
+  /**
+   * Voids a statement if it needs to be voided.
+   * @param \stdClass $voider
+   * @param StoreOptions $opts
+   */
+  private function voidStatement(\stdClass $voider, StoreOptions $opts) {
+    if (!$this->isVoiding($voider)) return;
+
+    $voided = $this->getModel($voider->object->id, $opts);
+
+    if ($voided !== null) {
+      if ($this->isVoiding($this->formatModel($voided))) throw new Exceptions\Exception(trans(
+        'xapi.errors.void_voider'
+      ));
+
+      $voided->voided = true;
+      $voided->save();
+    } else {
+      throw new Exceptions\Exception(trans(
+        'xapi.errors.void_null'
+      ));
+    }
+  }
+
+  /**
+   * Determines if a statement is a voiding statement.
+   * @param \stdClass $statement
+   * @return Boolean
+   */
+  private function isVoiding(\stdClass $statement) {
+    return (
+      isset($statement->verb->id) &&
+      $statement->verb->id === 'http://adlnet.gov/expapi/verbs/voided' &&
+      $this->isReferencing($statement)
+    );
+  }
+}
diff --git a/app/locker/repository/Statement/FileAttacher.php b/app/locker/repository/Statement/FileAttacher.php
new file mode 100644
index 0000000000..3e8edb15b0
--- /dev/null
+++ b/app/locker/repository/Statement/FileAttacher.php
@@ -0,0 +1,78 @@
+<?php namespace Locker\Repository\Statement;
+
+use \Locker\Repository\Document\FileTypes as FileTypes;
+use \Locker\Helpers\Helpers as Helpers;
+use \Locker\Helpers\Exceptions as Exceptions;
+
+class FileAttacher {
+
+  /**
+   * Stores attachments with the given options.
+   * @param [\stdClass] $attachments
+   * @param [String] $hashes
+   * @param StoreOptions $opts
+   */
+  public function store(array $attachments, array $hashes, StoreOptions $opts) {
+    $dir = $this->getDir($opts);
+    if (!is_dir($dir) && count($attachments > 0) && !empty($attachments)) {
+      mkdir($dir, null, true);
+    }
+
+    foreach ($attachments as $attachment) {
+      if (!in_array($attachment->hash, $hashes)) throw new Exceptions\Exception(
+        'Attachment hash does not exist in given statements'
+      );
+
+      $ext = $this->getExt($attachment->content_type);
+      if ($ext === false) throw new Exceptions\Exception(
+        'This file type cannot be supported'
+      );
+
+      $file = $attachment->hash.$ext;
+      file_put_contents($dir.'.'.$file, $attachment->content);
+    }
+  }
+
+  /**
+   * Gets all of the attachments for the given statements.
+   * @param [\stdClass] $statements
+   * @param IndexOptions $opts
+   * @return [\stdClass]
+   */
+  public function index(array $statements, IndexOptions $opts) {
+    $dir = $this->getDir($opts);
+
+    $attachments = [];
+    foreach ($statements as $statement) {
+      $attachments = array_merge($attachments, array_map(function ($attachment) use ($dir) {
+        $ext = $this->getExt($attachment->contentType);
+        $filename = $attachment->sha2.'.'.$ext;
+        return (object) [
+          'content_type' => $attachment['contentType'],
+          'hash' => $attachment->sha2,
+          'content' => file_get_contents($dir.$filename)
+        ];
+      }, isset($statement->attachments) ? $statement->attachments : []));
+    }
+
+    return $attachments;
+  }
+
+  /**
+   * Gets the extension from the given content type.
+   * @param String $content_type
+   * @return String
+   */
+  private function getExt($content_type) {
+    return array_search($content_type, FileTypes::getMap());
+  }
+
+  /**
+   * Gets the directory for attachments with the given options.
+   * @param Options $opts
+   * @return String
+   */
+  private function getDir(Options $opts) {
+    return Helpers::getEnvVar('LOCAL_FILESTORE').'/'.$opts->getOpt('lrs_id').'/attachments/';
+  }
+}
diff --git a/app/locker/repository/Statement/IndexOptions.php b/app/locker/repository/Statement/IndexOptions.php
index 78194ed6cb..c9ca3e8311 100644
--- a/app/locker/repository/Statement/IndexOptions.php
+++ b/app/locker/repository/Statement/IndexOptions.php
@@ -1,30 +1,64 @@
 <?php namespace Locker\Repository\Statement;
 
 use \Locker\Helpers\Helpers as Helpers;
+use \Locker\Helpers\Exceptions as Exceptions;
 
-class IndexOptions {
-  public $options = [];
-
-  public function __construct(array $opts) {
-    $this->options = $this->mergeDefaults($opts);
-    $this->validate();
-  }
-
-  public function getOpt($opt) {
-    return $options[$opt];
-  }
+class IndexOptions extends Options {
+  protected $defaults = [
+    'agent' => null,
+    'activity' => null,
+    'verb' => null,
+    'registration' => null,
+    'since' => null,
+    'until' => null,
+    'active' => true,
+    'voided' => false,
+    'related_activities' => false,
+    'related_agents' => false,
+    'ascending' => false,
+    'format' => 'exact',
+    'offset' => 0,
+    'limit' => 0,
+    'langs' => [],
+    'attachments' => false
+  ];
+  protected $types = [
+    'agent' => 'Actor',
+    'activity' => 'IRI',
+    'verb' => 'IRI',
+    'registration' => 'UUID',
+    'since' => 'Timestamp',
+    'until' => 'Timestamp',
+    'active' => 'Boolean',
+    'voided' => 'Boolean',
+    'related_activities' => 'Boolean',
+    'related_agents' => 'Boolean',
+    'ascending' => 'Boolean',
+    'format' => 'String',
+    'offset' => 'Integer',
+    'limit' => 'Integer',
+    'langs' => ['Language'],
+    'attachments' => 'Boolean',
+    'lrs_id' => 'String'
+  ];
 
   /**
    * Validates the given options as index options.
+   * @param [String => Mixed] $opts
+   * @return [String => Mixed]
    */
-  private function validate() {
-    $opts = $this->options;
+  protected function validate($opts) {
+    $opts = parent::validate($opts);
+
+    // Validates values.
+    if (!isset($opts['lrs_id'])) throw new Exceptions\Exception('`lrs_id` must be set.');
     if ($opts['offset'] < 0) throw new Exceptions\Exception('`offset` must be a positive interger.');
     if ($opts['limit'] < 1) throw new Exceptions\Exception('`limit` must be a positive interger.');
-    XApiHelpers::checkType('related_activities', 'boolean', $opts['related_agents']));
-    XApiHelpers::checkType('related_activities', 'boolean', $opts['related_activities']));
-    XApiHelpers::checkType('attachments', 'boolean', $opts['attachments']));
-    XApiHelpers::checkType('ascending', 'boolean', $opts['ascending']));
+    if (!in_array($opts['format'], ['exact', 'canonical', 'ids'])) {
+      throw new Exceptions\Exception('`format` must be "exact", "canonical", or "ids".');
+    }
+
+    return $opts;
   }
 
   /**
@@ -32,60 +66,21 @@ private function validate() {
    * @param [String => mixed] $opts Index options.
    * @return [String => mixed]
    */
-  private function mergeDefaults(array $opts) {
+  protected function mergeDefaults(array $opts) {
     // Merges with defaults.
-    $options = array_merge([
-      'agent' => null,
-      'activity' => null,
-      'verb' => null,
-      'registration' => null,
-      'since' => null,
-      'until' => null,
-      'active' => true,
-      'voided' => false,
-      'related_activities' => false,
-      'related_agents' => false,
-      'ascending' => false,
-      'format' => 'exact',
-      'offset' => 0,
-      'limit' => 0,
-      'langs' => [],
-      'attachments' => false
-    ], $opts);
+    $opts = parent::mergeDefaults($opts);
 
     // Converts types.
-    $options['active'] = $this->convertToBoolean($options['active']);
-    $options['voided'] = $this->convertToBoolean($options['voided']);
-    $options['related_agents'] = $this->convertToBoolean($options['related_agents']);
-    $options['related_activities'] = $this->convertToBoolean($options['related_activities']);
-    $options['attachments'] = $this->convertToBoolean($options['attachments']);
-    $options['ascending'] = $this->convertToBoolean($options['ascending']);
-    $options['limit'] = $this->convertToInt($options['limit']);
-    $options['offset'] = $this->convertToInt($options['offset']);
-
-    if ($options['limit'] === 0) $options['limit'] = 100;
-    return $options;
-  }
+    $opts['active'] = $this->convertToBoolean($opts['active']);
+    $opts['voided'] = $this->convertToBoolean($opts['voided']);
+    $opts['related_agents'] = $this->convertToBoolean($opts['related_agents']);
+    $opts['related_activities'] = $this->convertToBoolean($opts['related_activities']);
+    $opts['attachments'] = $this->convertToBoolean($opts['attachments']);
+    $opts['ascending'] = $this->convertToBoolean($opts['ascending']);
+    $opts['limit'] = $this->convertToInt($opts['limit']);
+    $opts['offset'] = $this->convertToInt($opts['offset']);
 
-  /**
-   * Converts the given value to a Boolean if it can be.
-   * @param mixed $value
-   * @return Boolean|mixed Returns the value unchanged if it can't be converted.
-   */
-  private function convertToBoolean($value) {
-    if (is_string($value)) $value = strtolower($value);
-    if ($value === 'true') return true;
-    if ($value === 'false') return false;
-    return $value;
-  }
-
-  /**
-   * Converts the given value to a Integer if it can be.
-   * @param mixed $value
-   * @return Integer|mixed Returns the value unchanged if it can't be converted.
-   */
-  private function convertToInt($value) {
-    $converted_value = (int) $value;
-    return ($value !== (string) $converted_value) ? $value : $converted_value;
+    if ($opts['limit'] === 0) $opts['limit'] = 100;
+    return $opts;
   }
 }
diff --git a/app/locker/repository/Statement/Options.php b/app/locker/repository/Statement/Options.php
new file mode 100644
index 0000000000..547caff34b
--- /dev/null
+++ b/app/locker/repository/Statement/Options.php
@@ -0,0 +1,81 @@
+<?php namespace Locker\Repository\Statement;
+
+use \Locker\Helpers\Helpers as Helpers;
+use \Locker\Helpers\Exceptions as Exceptions;
+
+abstract class Options {
+  public $options = [];
+  protected $defaults = [];
+  protected $types = [];
+
+  public function __construct(array $opts) {
+    $this->options = $this->mergeDefaults($opts);
+    $this->options = $this->validate($this->options);
+  }
+
+  /**
+   * Gets an options.
+   * @param String $opt Option name.
+   * @return Mixed
+   */
+  public function getOpt($opt) {
+    return $this->options[$opt];
+  }
+
+  /**
+   * Validates the given options as index options.
+   * @param [String => Mixed] $opts
+   * @return [String => Mixed]
+   */
+  protected function validate($opts) {
+    foreach ($opts as $key => $value) {
+      if ($value !== null) {
+        if (is_array($this->types[$key])) {
+          $class = '\Locker\XApi\\'.$this->types[$key][0];
+          if (!is_array($value)) {
+            throw new Exceptions\Exception("$key must be an array.");
+          }
+          foreach ($value as $item) {
+            Helpers::validateAtom(new $class($item));
+          }
+        } else {
+          $class = '\Locker\XApi\\'.$this->types[$key];
+          Helpers::validateAtom(new $class($value));
+        }
+      }
+    }
+
+    return $opts;
+  }
+
+  /**
+   * Returns all of the index options set to their default or given value (using the given options).
+   * @param [String => mixed] $opts Index options.
+   * @return [String => mixed]
+   */
+  protected function mergeDefaults(array $opts) {
+    return array_merge($this->defaults, $opts);
+  }
+
+  /**
+   * Converts the given value to a Boolean if it can be.
+   * @param mixed $value
+   * @return Boolean|mixed Returns the value unchanged if it can't be converted.
+   */
+  protected function convertToBoolean($value) {
+    if (is_string($value)) $value = strtolower($value);
+    if ($value === 'true') return true;
+    if ($value === 'false') return false;
+    return $value;
+  }
+
+  /**
+   * Converts the given value to a Integer if it can be.
+   * @param mixed $value
+   * @return Integer|mixed Returns the value unchanged if it can't be converted.
+   */
+  protected function convertToInt($value) {
+    $converted_value = (int) $value;
+    return ($value !== (string) $converted_value) ? $value : $converted_value;
+  }
+}
diff --git a/app/locker/repository/Statement/ShowOptions.php b/app/locker/repository/Statement/ShowOptions.php
new file mode 100644
index 0000000000..04e78a374a
--- /dev/null
+++ b/app/locker/repository/Statement/ShowOptions.php
@@ -0,0 +1,13 @@
+<?php namespace Locker\Repository\Statement;
+
+class ShowOptions extends Options {
+  protected $defaults = [
+    'voided' => false,
+    'active' => true
+  ];
+  protected $types = [
+    'lrs_id' => 'String',
+    'voided' => 'Boolean',
+    'active' => 'Boolean'
+  ];
+}
diff --git a/app/locker/repository/Statement/StatementRepository.php b/app/locker/repository/Statement/StatementRepository.php
deleted file mode 100644
index 548b0fe7ed..0000000000
--- a/app/locker/repository/Statement/StatementRepository.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php namespace Locker\Repository\Statement;
-
-interface StatementRepository {
-
-  public function index($lrsId, array $filters, array $options);
-  public function show($lrsId, $id, $voided);
-  public function toCanonical(array $statements, array $langs);
-  public function toIds(array $statements);
-  public function getCurrentDate();
-  public function create(array $statements, \Lrs $lrs, $attachment);
-
-}
\ No newline at end of file
diff --git a/app/locker/repository/Statement/StoreOptions.php b/app/locker/repository/Statement/StoreOptions.php
new file mode 100644
index 0000000000..7ca41f642b
--- /dev/null
+++ b/app/locker/repository/Statement/StoreOptions.php
@@ -0,0 +1,9 @@
+<?php namespace Locker\Repository\Statement;
+
+class StoreOptions extends Options {
+  protected $defaults = [];
+  protected $types = [
+    'lrs_id' => 'String',
+    'authority' => 'Authority'
+  ];
+}
diff --git a/app/models/Statement.php b/app/models/Statement.php
index 687f41c022..9a7ece1ba1 100644
--- a/app/models/Statement.php
+++ b/app/models/Statement.php
@@ -4,38 +4,12 @@
 
 class Statement extends Eloquent {
 
-  /**
-   * Our MongoDB collection used by the model.
-   *
-   * @var string
-   */
   protected $collection = 'statements';
+  protected $hidden = ['_id', 'created_at', 'updated_at'];
+  protected $fillable = ['statement', 'active', 'voided', 'refs', 'lrs', 'timestamp'];
 
-  /**
-   * We don't need default Laravel created_at
-   **/
-  //public $timestamps = false;
-
-  /**
-   * The attributes excluded from the model's JSON form.
-   *
-   * @var array
-   */
-  protected $hidden = array('_id', 'created_at', 'updated_at');
-
-  /**
-   * For mass assigning which we use for TC statements,
-   * set the fillable fields.
-   **/
-  // protected $fillable = array('actor', 'verb', 'result', 'object', 'context', 
-  //   'authority', 'stored', 'timestamp', 'id', 'attachments', 'version');
-
-  /**
-   * All statements belong to an LRS
-   *
-   **/
   public function lrs(){
     return $this->belongsTo('Lrs');
   }
 
-}
\ No newline at end of file
+}
diff --git a/app/tests/API/StatementsTest.php b/app/tests/API/StatementsTest.php
deleted file mode 100644
index 199f6964f7..0000000000
--- a/app/tests/API/StatementsTest.php
+++ /dev/null
@@ -1,155 +0,0 @@
-<?php namespace Tests\API;
-use \Route as Route;
-
-class StatementsTest extends TestCase {
-  static protected $endpoint = '/api/v1/statements';
-  protected $pipeline = null;
-
-  public function setup() {
-    parent::setup();
-  }
-
-  protected function getPipeline() {
-    return $this->pipeline ?: file_get_contents(__DIR__ . '/../Fixtures/pipeline.json');
-  }
-
-  protected function requestStatementsAPI($method = 'GET', $url = '', $params = []) {
-    $server = $this->getHeaders($this->lrs->api);
-    Route::enableFilters();
-    return $this->call($method, $url, $params, [], $server, '');
-  }
-
-  public function testAggregate() {
-    $response = $this->requestStatementsAPI('GET', static::$endpoint.'/aggregate', [
-      'pipeline' => $this->getPipeline()
-    ]);
-
-    // Checks that the response is correct.
-    $this->assertEquals(200, $response->getStatusCode(), 'Incorrect status code.');
-    $this->assertEquals(true, method_exists($response, 'getContent'), 'Incorrect response.');
-
-    // Checks that the content is correct.
-    $content = json_decode($response->getContent());
-    $this->assertEquals(true, is_object($content), 'Incorrect content type.');
-    $this->assertEquals(true, isset($content->result), 'No result.');
-    $this->assertEquals(true, is_array($content->result), 'Incorrect result type.');
-    $this->assertEquals(static::$statements, count($content->result), 'Incorrect number of results.');
-    $this->assertEquals(true, is_object($content->result[0]), 'Incorrect projection type.');
-    $this->assertEquals(true, isset($content->result[0]->statement), 'No statement.');
-    $this->assertEquals(true, is_object($content->result[0]->statement), 'Incorrect statement type.');
-    $this->assertEquals(true, isset($content->result[0]->statement->actor), 'No actor.');
-    $this->assertEquals(true, isset($content->ok), 'No ok.');
-    $this->assertEquals(true, is_numeric($content->ok), 'Incorrect ok type.');
-    $this->assertEquals(1, $content->ok, 'Incorrect ok.');
-  }
-
-  public function testAggregateTime() {
-    $response = $this->requestStatementsAPI('GET', static::$endpoint.'/aggregate/time', [
-      'match' => '{"active": true}'
-    ]);
-
-    // Checks that the response is correct.
-    $this->assertEquals(200, $response->getStatusCode(), 'Incorrect status code.');
-    $this->assertEquals(true, method_exists($response, 'getContent'), 'Incorrect response.');
-
-    // Checks that the content is correct.
-    $content = json_decode($response->getContent());
-    $this->assertEquals(true, is_object($content), 'Incorrect content type.');
-    $this->assertEquals(true, isset($content->result), 'No result.');
-    $this->assertEquals(true, is_array($content->result), 'Incorrect result type.');
-    $this->assertEquals(1, count($content->result), 'Incorrect number of results.');
-    $this->assertEquals(true, is_object($content->result[0]), 'Incorrect projection type.');
-    $this->assertEquals(true, isset($content->result[0]->count), 'No count.');
-    $this->assertEquals(true, is_numeric($content->result[0]->count), 'Incorrect count type.');
-    $this->assertEquals(static::$statements, is_numeric($content->result[0]->count), 'Incorrect count.');
-    $this->assertEquals(true, isset($content->result[0]->date), 'No date.');
-    $this->assertEquals(true, is_array($content->result[0]->date), 'Incorrect date type.');
-    $this->assertEquals(true, isset($content->ok), 'No ok.');
-    $this->assertEquals(true, is_numeric($content->ok), 'Incorrect ok type.');
-    $this->assertEquals(1, $content->ok, 'Incorrect ok.');
-  }
-
-  public function testAggregateObject() {
-    $response = $this->requestStatementsAPI('GET', static::$endpoint.'/aggregate/object', [
-      'match' => '{"active": true}'
-    ]);
-
-    // Checks that the response is correct.
-    $this->assertEquals(200, $response->getStatusCode(), 'Incorrect status code.');
-    $this->assertEquals(true, method_exists($response, 'getContent'), 'Incorrect response.');
-
-    // Checks that the content is correct.
-    $content = json_decode($response->getContent());
-    $this->assertEquals(true, is_object($content), 'Incorrect content type.');
-    $this->assertEquals(true, isset($content->result), 'No result.');
-    $this->assertEquals(true, is_array($content->result), 'Incorrect result type.');
-    $this->assertEquals(1, count($content->result), 'Incorrect number of results.');
-    $this->assertEquals(true, is_object($content->result[0]), 'Incorrect projection type.');
-    $this->assertEquals(true, isset($content->result[0]->count), 'No count.');
-    $this->assertEquals(true, is_numeric($content->result[0]->count), 'Incorrect count type.');
-    $this->assertEquals(static::$statements, is_numeric($content->result[0]->count), 'Incorrect count.');
-    $this->assertEquals(true, isset($content->result[0]->data), 'No data.');
-    $this->assertEquals(true, is_array($content->result[0]->data), 'Incorrect data type.');
-    $this->assertEquals(static::$statements, count($content->result[0]->data), 'Incorrect data.');
-    $this->assertEquals(true, isset($content->result[0]->data[0]), 'Incorrect data item.');
-    $this->assertEquals(true, is_object($content->result[0]->data[0]), 'Incorrect data item type.');
-    $this->assertEquals(true, isset($content->result[0]->data[0]->actor), 'No actor.');
-    $this->assertEquals(true, is_object($content->result[0]->data[0]->actor), 'Incorrect actor type.');
-    $this->assertEquals(true, isset($content->ok), 'No ok.');
-    $this->assertEquals(true, is_numeric($content->ok), 'Incorrect ok type.');
-    $this->assertEquals(1, $content->ok, 'Incorrect ok.');
-  }
-
-  public function testWhere() {
-    $response = $this->requestStatementsAPI('GET', static::$endpoint.'/where', [
-      'filter' => '[[{"active", true}]]',
-      'limit' => 1,
-      'page' => 1
-    ]);
-
-    // Checks that the response is correct.
-    $this->assertEquals(200, $response->getStatusCode(), 'Incorrect status code.');
-    $this->assertEquals(true, method_exists($response, 'getContent'), 'Incorrect response.');
-
-    // Checks that the content is correct.
-    $content = json_decode($response->getContent());
-    $this->assertEquals(true, is_object($content), 'Incorrect content type.');
-
-    // Checks set props.
-    $this->assertEquals(true, isset($content->total), 'No total.');
-    $this->assertEquals(true, isset($content->per_page), 'No per_page.');
-    $this->assertEquals(true, isset($content->current_page), 'No current_page.');
-    $this->assertEquals(true, isset($content->last_page), 'No last_page.');
-    $this->assertEquals(true, isset($content->from), 'No from.');
-    $this->assertEquals(true, isset($content->to), 'No to.');
-    $this->assertEquals(true, isset($content->data), 'No data.');
-
-    // Checks prop types.
-    $this->assertEquals(true, is_numeric($content->total), 'Incorrect total type.');
-    $this->assertEquals(true, is_numeric($content->per_page), 'Incorrect per_page type.');
-    $this->assertEquals(true, is_numeric($content->current_page), 'Incorrect current_page type.');
-    $this->assertEquals(true, is_numeric($content->last_page), 'Incorrect last_page type.');
-    $this->assertEquals(true, is_numeric($content->from), 'Incorrect from type.');
-    $this->assertEquals(true, is_numeric($content->to), 'Incorrect to type.');
-    $this->assertEquals(true, is_array($content->data), 'Incorrect data type.');
-
-    // Checks prop content.
-    $this->assertEquals(static::$statements, $content->total, 'Incorrect total value.');
-    $this->assertEquals(1, $content->per_page, 'Incorrect per_page value.');
-    $this->assertEquals(1, $content->current_page, 'Incorrect current_page value.');
-    $this->assertEquals(static::$statements, $content->last_page, 'Incorrect last_page value.');
-    $this->assertEquals(1, $content->from, 'Incorrect from value.');
-    $this->assertEquals(1, $content->to, 'Incorrect to value.');
-    $this->assertEquals(1, count($content->data), 'Incorrect data count.');
-    $this->assertEquals(true, isset($content->data[0]), 'No data item.');
-    $this->assertEquals(true, is_object($content->data[0]), 'Incorrect data item type.');
-    $this->assertEquals(true, isset($content->data[0]->statement), 'No statement.');
-    $this->assertEquals(true, is_object($content->data[0]->statement), 'Incorrect statement type.');
-    $this->assertEquals(true, isset($content->data[0]->statement->actor), 'No actor.');
-    $this->assertEquals(true, is_object($content->data[0]->statement->actor), 'Incorrect actor type.');
-  }
-
-  public function tearDown() {
-    parent::tearDown();
-  }
-}
diff --git a/app/tests/API/TestCase.php b/app/tests/API/TestCase.php
deleted file mode 100644
index 351d54a182..0000000000
--- a/app/tests/API/TestCase.php
+++ /dev/null
@@ -1,108 +0,0 @@
-<?php namespace Tests\API;
-use \Locker\Helpers\Helpers as Helpers;
-use \Illuminate\Foundation\Testing\TestCase as Base;
-use \User as User;
-use \Site as Site;
-use \Lrs as Lrs;
-use \Auth as Auth;
-use \Route as Route;
-use \Statement as Statement;
-
-abstract class TestCase extends Base {
-  static protected $endpoint = '/api/v1/...';
-  static protected $statements = 5;
-  protected $user = null;
-  protected $lrs = null;
-
-  public function setup() {
-    parent::setup();
-    $this->user = $this->createUser();
-    Auth::login($this->user);
-    $this->lrs = $this->createLRS();
-    $this->createStatements();
-  }
-
-  public function createApplication() {
-    $unitTesting = true;
-    $testEnvironment = 'testing';
-    return require __DIR__ . '/../../../bootstrap/start.php';
-  }
-
-  protected function createUser() {
-    $user = User::firstOrCreate(['email' => 'test@example.com']);
-
-    // If first user, create site object
-    if (User::count() === 1) {
-      $site = new Site([
-        'name' => 'Test Site',
-        'email' => $user->email,
-        'lang' => 'en-US',
-        'create_lrs' => ['super'],
-        'registration' => 'Closed',
-        'restrict' => 'None',
-        'domain' => '',
-        'super' => [['user' => $user->_id]],
-      ]);
-      $site->save();
-    }
-    return $user;
-  }
-
-  protected function createLRS() {
-    $lrs = new Lrs([
-      'title' => 'TestLRS',
-      'api' => [
-        'basic_key' => Helpers::getRandomValue(),
-        'basic_secret' => Helpers::getRandomValue()
-      ],
-      'owner' => [
-        '_id' => Auth::user()->_id,
-      ],
-      'users' => [[
-        '_id' => Auth::user()->_id,
-        'email' => Auth::user()->email,
-        'name' => Auth::user()->name,
-        'role' => 'admin'
-      ]],
-      'domain' => '',
-    ]);
-
-    $lrs->save();
-
-    // Hack header request
-    $_SERVER['SERVER_NAME'] = $lrs->title . '.com.vn';
-    return $lrs;
-  }
-
-  protected function createStatements() {
-    $statement = $this->getStatement();
-    for ($i = 0; $i < static::$statements; $i += 1) {
-      $response = $this->requestAPI('POST', '/data/xAPI/statements', $statement);
-    }
-  }
-
-  protected function getHeaders($auth) {
-    return [
-      'PHP_AUTH_USER' => $auth['basic_key'],
-      'PHP_AUTH_PW' => $auth['basic_secret']
-    ];
-  }
-
-  protected function requestAPI($method = 'GET', $url = '', $content = '') {
-    $server = $this->getHeaders($this->lrs->api);
-    $server['HTTP_X-Experience-API-Version'] = '1.0.1';
-    Route::enableFilters();
-    return $this->call($method, $url, [], [], $server, $content);
-  }
-
-  protected function getStatement() {
-    return file_get_contents(__DIR__ . '/../Fixtures/statement.json');
-  }
-
-  public function tearDown() {
-    Statement::where('lrs._id', $this->lrs->_id)->delete();
-    $this->lrs->delete();
-    $this->user->delete();
-    parent::tearDown();
-  }
-}
diff --git a/app/tests/ActivityTest.php b/app/tests/ActivityTest.php
deleted file mode 100644
index 2168d5d17d..0000000000
--- a/app/tests/ActivityTest.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-class ActivityTest extends TestCase {
-
-  /**
-   * Test Activity
-   */
-  public function testActivityCRUD() {
-    $activity = new Activity;
-    $activity->_id = \Locker\Helpers\Helpers::getRandomValue();
-    $activity->definition = array(
-      'type' => \Locker\Helpers\Helpers::getRandomValue(),
-      'name' => array(
-        'en-US' => \Locker\Helpers\Helpers::getRandomValue()
-      ),
-      'description' => array(
-        'en-US' => \Locker\Helpers\Helpers::getRandomValue()
-      )
-    );
-    $result = $activity->save();
-    $this->assertTrue($result);
-
-    // Load activity from db
-    $aid = $activity->_id;
-    $db_activity = Activity::find($aid);
-    $this->assertEquals($db_activity->_id, $activity->_id);
-
-    // Delete activity
-    $db_activity->delete();
-    $this->assertEquals(Activity::find($aid), NULL);
-  }
-}
-
diff --git a/app/tests/ApiV1QueryAnalyticsTest.php b/app/tests/ApiV1QueryAnalyticsTest.php
deleted file mode 100644
index f89dd6166d..0000000000
--- a/app/tests/ApiV1QueryAnalyticsTest.php
+++ /dev/null
@@ -1,122 +0,0 @@
-<?php
-
-class ApiV1QueryAnalyticsTest extends TestCase {
-
-  public function setUp() {
-    // Calls parent setup.
-    parent::setUp();
-
-    // Authenticates as super user.
-    Route::enableFilters();
-    $user = User::firstOrCreate(['email' => 'quan@ll.com']);
-    Auth::login($user);
-    $this->createLRS();
-    
-    // Creates testing statements.
-    $vs = json_decode(file_get_contents(__DIR__ . '/Fixtures/Analytics.json'), true);
-    $statement = App::make('Locker\Repository\Statement\EloquentStatementRepository');
-    $statement->create([json_decode(json_encode($vs))], $this->lrs);
-
-    $vs2 = $vs;
-    $vs2['object']['definition']['type'] = 'http://activitystrea.ms/schema/2.0/badge';
-
-    $statement2 = App::make('Locker\Repository\Statement\EloquentStatementRepository');
-    $statement2->create([json_decode(json_encode($vs2))], $this->lrs);
-  }
-
-  private function callResponse($params = [], $lrs) {
-    $auth = [
-      'PHP_AUTH_USER' => $lrs->api['basic_key'],
-      'PHP_AUTH_PW' => $lrs->api['basic_secret']
-    ];
-    $route = '/api/v1/query/analytics';
-
-    return $this->call('GET', $route, $params, [], $auth);
-  }
-
-  public function testDefaultQuery() {
-    $response = $this->callResponse([], $this->lrs);
-    $data = $response->getData();
-    $this->assertEquals($data->version, 'v1');
-    $this->assertEquals($data->route, 'api/v1/query/analytics');
-  }
-
-  public function testTimeQuery() {
-    $response = $this->callResponse(['type' => 'time'], $this->lrs);
-    $data = $response->getData()->data;
-    $this->assertEquals($data[0]->count, 2);
-  }
-
-  public function testUserQuery() {
-    $response = $this->callResponse(['type' => 'user'], $this->lrs);
-
-    $data = $response->getData()->data;
-    $checkTypeUser = TRUE;
-    foreach ($data as $value) {
-        if (!in_array($value->data->name, array('quanvm', 'quanvm2'))) {
-            $checkTypeUser = FALSE;
-        }
-    }
-    $this->assertTRUE($checkTypeUser);
-  }
-
-  public function testVerbQuery() {
-    $response = $this->callResponse(['type' => 'verb'], $this->lrs);
-    $data = $response->getData()->data;
-    $this->assertEquals($data[0]->data->id, "http://adlnet.gov/expapi/verbs/experienced");
-  }
-
-  public function testDayQuery() {
-    $intervalLrs = Lrs::find('536b02d4c01f1325618b4567');
-    if ($intervalLrs) {
-      $response = $this->callResponse(['interval' => 'Day'], $intervalLrs);
-      $data = $response->getData()->data;
-      $this->assertEquals(count($data), 2);
-    }
-  }
-
-  public function testMonthQuery() {
-    $intervalLrs = Lrs::find('536b03bbc01f13a6618b4567');
-    if ($intervalLrs) {
-      $response = $this->callResponse(['interval' => 'Month'], $intervalLrs);
-      $data = $response->getData()->data;
-      $this->assertEquals(count($data), 2);
-    }
-  }
-
-  public function testYearQuery() {
-    $intervalLrs = Lrs::find('536b05ccc01f1392638b4567');
-    if ($intervalLrs) {
-      $response = $this->callResponse(['interval' => 'Year'], $intervalLrs);
-      $data = $response->getData()->data;
-      $this->assertEquals(count($data), 2);
-    }
-  }
-
-  public function testSinceQuery() {
-    $response = $this->callResponse(['since' => date('Y-m-d')], $this->lrs);
-    $data = $response->getData()->data;
-    $this->assertEquals($data[0]->count, 2);
-  }
-
-  public function testEmptySinceQuery() {
-    $date = date('Y-m-d', strtotime("+1 day"));
-    $response = $this->callResponse(['since' => $date], $this->lrs);
-    $data = $response->getData()->data;
-    $this->assertTrue(empty($data));
-  }
-
-  public function testUntilQuery() {
-    $date = date('Y-m-d', strtotime("+1 day"));
-    $response = $this->callResponse(['until' => $date], $this->lrs);
-    $data = $response->getData()->data;
-    $this->assertEquals($data[0]->count, 2);
-  }
-
-  public function testEmptyUntilQuery() {
-    $date = date('Y-m-d', strtotime("-1 day"));
-    $response = $this->callResponse(['until' => $date], $this->lrs);
-    $data = $response->getData()->data;
-    $this->assertTrue(empty($data));
-  }
-}
diff --git a/app/tests/AuthorityTest.php b/app/tests/AuthorityTest.php
deleted file mode 100644
index eaa7de93f7..0000000000
--- a/app/tests/AuthorityTest.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-/**
- * Test authority in statment
- *
- * @see https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#authority
- */
-class AuthorityTest extends TestCase
-{
-
-  public $lrs;
-  protected $statement;
-
-  public function setUp()
-  {
-    parent::setUp();
-    // Authentication as super user.
-    $user = User::firstOrCreate(array('email' => $this->dummyEmail()));
-    Auth::login($user);
-    $this->lrs = $this->createLRS();
-    $this->statement = App::make('Locker\Repository\Statement\EloquentStatementRepository');
-  }
-
-  public function testAuthority()
-  {
-    $stmt = $this->defaultStatment();
-
-    // Ensure if authority is empty the LRS will be create anonymous authoriry
-    $authority = $stmt['authority'];
-    unset($stmt['authority']);
-    $return = $this->createStatement($stmt, $this->lrs);
-    $this->assertEquals(gettype($return), 'array');
-
-    $stmt_id = reset($return);
-    $obj_stmt = $this->statement->show($this->lrs->_id, $stmt_id)->first();
-    $stmt_authority = $obj_stmt->statement['authority'];
-    $this->assertTrue(!empty($stmt_authority));
-
-    // Ensure authority stored in db is same value with statment send to LRS
-    $stmt['authority'] = $authority;
-    $return = $this->createStatement($stmt, $this->lrs);
-    $stmt_id = reset($return);
-    $obj_stmt = $this->statement->show($this->lrs->_id, $stmt_id)->first();
-
-    $stmt_authority = $obj_stmt->statement['authority'];
-    $this->assertEquals($authority, $stmt_authority);
-  }
-
-}
diff --git a/app/tests/ExampleTest.php b/app/tests/ExampleTest.php
deleted file mode 100644
index 39c122901a..0000000000
--- a/app/tests/ExampleTest.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-class ExampleTest extends TestCase
-{
-  /**
-   * A basic functional test example.
-   *
-   * @return void
-   */
-  public function testBasicExample()
-  {
-    $crawler = $this->client->request('GET', '/');
-
-    $this->assertTrue($this->client->getResponse()->isOk());
-  }
-
-}
diff --git a/app/tests/Fixtures/Analytics.json b/app/tests/Fixtures/Analytics.json
deleted file mode 100644
index 2ec09a1543..0000000000
--- a/app/tests/Fixtures/Analytics.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{
-  "actor": {
-    "objectType": "Agent",
-    "mbox": "mailto:duy.nguyen@go1.com.au",
-    "name": "quanvm"
-  },
-  "verb": {
-    "id": "http://adlnet.gov/expapi/verbs/experienced",
-    "display": {"und": "experienced"}
-  },
-  "context": {
-    "contextActivities": {
-      "parent": [{
-        "id": "http://tincanapi.com/GolfExample_TCAPI",
-        "objectType": "Activity"
-      }],
-      "grouping": [{
-        "id": "http://tincanapi.com/GolfExample_TCAPI",
-        "objectType": "Activity"
-      }]
-    }
-  },
-  "object": {
-    "id": "http://tincanapi.com/GolfExample_TCAPI/Playing/Scoring.html",
-    "objectType": "Activity",
-    "definition": {
-      "name": {
-        "en-US": "Scoring"
-      },
-      "description": {
-        "en-US": "An overview of how to score a round of golf."
-      },
-      "type": "http://activitystrea.ms/schema/1.0/badge"
-    }
-  },
-  "authority": {
-    "name": "",
-    "mbox": "mailto:quan@ll.com",
-    "objectType": "Agent"
-  }
-}
\ No newline at end of file
diff --git a/app/tests/Fixtures/Statements/Invalid/Actor/Group/Member/object-type-is-not-agent.json b/app/tests/Fixtures/Statements/Invalid/Actor/Group/Member/object-type-is-not-agent.json
deleted file mode 100644
index b75a1b373c..0000000000
--- a/app/tests/Fixtures/Statements/Invalid/Actor/Group/Member/object-type-is-not-agent.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
-    "id": "88664422-1234-5678-1234-567812345678",
-    "actor": {
-        "objectType": "Group",
-        "member": [
-            {
-                "objectType": "Agent",
-                "mbox": "mailto:bob@example.com"
-            },
-            {
-                "objectType": "Group",
-                "mbox": "mailto:group@example.com"
-            }
-        ]
-    },
-    "verb": {
-        "id": "http://adlnet.gov/expapi/verbs/created",
-        "display": {
-            "en-US": "created"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    }
-}
\ No newline at end of file
diff --git a/app/tests/Fixtures/Statements/Invalid/Actor/Group/missing-member.json b/app/tests/Fixtures/Statements/Invalid/Actor/Group/missing-member.json
deleted file mode 100644
index 4a6c28c42c..0000000000
--- a/app/tests/Fixtures/Statements/Invalid/Actor/Group/missing-member.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
-    "id": "88664422-1234-5678-1234-567812345678",
-    "actor": {
-        "objectType": "Group"
-    },
-    "verb": {
-        "id": "http://adlnet.gov/expapi/verbs/created",
-        "display": {
-            "en-US": "created"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    }
-}
\ No newline at end of file
diff --git a/app/tests/Fixtures/Statements/Invalid/Actor/Mbox/invalid-format.json b/app/tests/Fixtures/Statements/Invalid/Actor/Mbox/invalid-format.json
deleted file mode 100644
index 0a922804dc..0000000000
--- a/app/tests/Fixtures/Statements/Invalid/Actor/Mbox/invalid-format.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
-    "id": "88664422-1234-5678-1234-567812345678",
-    "actor": {
-        "mbox": "A@b@c@example.com"
-    },
-    "verb": {
-        "id": "http://adlnet.gov/expapi/verbs/created",
-        "display": {
-            "en-US": "created"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    }
-}
\ No newline at end of file
diff --git a/app/tests/Fixtures/Statements/Invalid/Actor/missing-actor.json b/app/tests/Fixtures/Statements/Invalid/Actor/missing-actor.json
deleted file mode 100644
index 950d2d9831..0000000000
--- a/app/tests/Fixtures/Statements/Invalid/Actor/missing-actor.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-    "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0",
-    "verb": {
-        "id": "http://adlnet.gov/expapi/verbs/created",
-        "display": {
-            "en-US": "created"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    },
-    "verb": {
-        "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced",
-        "display": {
-            "und": "experienced"
-        }
-    }
-}
\ No newline at end of file
diff --git a/app/tests/Fixtures/Statements/Invalid/Attachment/content-type-is-not-string.json b/app/tests/Fixtures/Statements/Invalid/Attachment/content-type-is-not-string.json
deleted file mode 100644
index d8b5605139..0000000000
--- a/app/tests/Fixtures/Statements/Invalid/Attachment/content-type-is-not-string.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
-    "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0",
-    "actor": {
-        "mbox": "mailto:zachary+pierce@gmail.com"
-    },
-    "verb": {
-        "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced",
-        "display": {
-            "und": "experienced"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    },
-    "attachments": [
-        {
-            "usageType": "http://example.com/usage/info/A",
-            "display": {"en-GB":"hello world"},
-            "contentType": 1.23,
-            "length": 654,
-            "sha2": "71b9be12fb30c4648b5f17b37d705440c78bbec56b4e30a40deeb5f163e617f2"
-        }
-    ]
-}
diff --git a/app/tests/Fixtures/Statements/Invalid/Attachment/display.json b/app/tests/Fixtures/Statements/Invalid/Attachment/display.json
deleted file mode 100644
index 67bf027986..0000000000
--- a/app/tests/Fixtures/Statements/Invalid/Attachment/display.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-    "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0",
-    "actor": {
-        "mbox": "mailto:zachary+pierce@gmail.com"
-    },
-    "verb": {
-        "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced",
-        "display": {
-            "und": "experienced"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    },
-    "attachments": [
-        {
-            "usageType": "http://example.com/usage/info/A",
-            "display": {
-                "$": "hello"
-            },
-            "contentType": "text/plain",
-            "length": 100,
-            "sha2": "71b9be12fb30c4648b5f17b37d705440c78bbec56b4e30a40deeb5f163e617f2"
-        }
-    ]
-}
diff --git a/app/tests/Fixtures/Statements/Invalid/Attachment/length-is-not-integer.json b/app/tests/Fixtures/Statements/Invalid/Attachment/length-is-not-integer.json
deleted file mode 100644
index 8f10f365a3..0000000000
--- a/app/tests/Fixtures/Statements/Invalid/Attachment/length-is-not-integer.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
-    "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0",
-    "actor": {
-        "mbox": "mailto:zachary+pierce@gmail.com"
-    },
-    "verb": {
-        "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced",
-        "display": {
-            "und": "experienced"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    },
-    "attachments": [
-        {
-            "usageType": "http://example.com/usage/info/A",
-            "display": {"en-GB":"hello world"},
-            "contentType": "text/plain",
-            "length": "100MB",
-            "sha2": "71b9be12fb30c4648b5f17b37d705440c78bbec56b4e30a40deeb5f163e617f2"
-        }
-    ]
-}
diff --git a/app/tests/Fixtures/Statements/Invalid/Attachment/missing-sha2.json b/app/tests/Fixtures/Statements/Invalid/Attachment/missing-sha2.json
deleted file mode 100644
index 8a016e9286..0000000000
--- a/app/tests/Fixtures/Statements/Invalid/Attachment/missing-sha2.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
-    "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0",
-    "actor": {
-        "mbox": "mailto:zachary+pierce@gmail.com"
-    },
-    "verb": {
-        "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced",
-        "display": {
-            "und": "experienced"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    },
-    "attachments": [
-        {
-            "usageType": "http://example.com/usage/info/A",
-            "display": {"en-GB":"hello world"},
-            "contentType": "text/plain",
-            "length": 100
-        }
-    ]
-}
diff --git a/app/tests/Fixtures/Statements/Invalid/Attachment/missing-usage-type.json b/app/tests/Fixtures/Statements/Invalid/Attachment/missing-usage-type.json
deleted file mode 100644
index fea1a0df39..0000000000
--- a/app/tests/Fixtures/Statements/Invalid/Attachment/missing-usage-type.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
-    "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0",
-    "actor": {
-        "mbox": "mailto:zachary+pierce@gmail.com"
-    },
-    "verb": {
-        "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced",
-        "display": {
-            "und": "experienced"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    },
-    "attachments": [
-        {
-            "display": {"en-GB":"hello world"},
-            "contentType": "text/plain",
-            "length": 100,
-            "sha2": "71b9be12fb30c4648b5f17b37d705440c78bbec56b4e30a40deeb5f163e617f2"
-        }
-    ]
-}
diff --git a/app/tests/Fixtures/Statements/Invalid/Attachment/sha2-is-not-valid.json b/app/tests/Fixtures/Statements/Invalid/Attachment/sha2-is-not-valid.json
deleted file mode 100644
index c131f9dc41..0000000000
--- a/app/tests/Fixtures/Statements/Invalid/Attachment/sha2-is-not-valid.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
-    "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0",
-    "actor": {
-        "mbox": "mailto:zachary+pierce@gmail.com"
-    },
-    "verb": {
-        "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced",
-        "display": {
-            "und": "experienced"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    },
-    "attachments": [
-        {
-            "usageType": "http://example.com/usage/info/A",
-            "display": {"en-GB":"hello world"},
-            "contentType": "text/plain",
-            "length": 100,
-            "sha2": "invalid-sha2-string"
-        }
-    ]
-}
diff --git a/app/tests/Fixtures/Statements/Invalid/Authority/Member/wrong-object-type.json b/app/tests/Fixtures/Statements/Invalid/Authority/Member/wrong-object-type.json
deleted file mode 100644
index 57b6b86785..0000000000
--- a/app/tests/Fixtures/Statements/Invalid/Authority/Member/wrong-object-type.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
-    "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0",
-    "actor": {
-        "mbox": "mailto:zachary+pierce@gmail.com"
-    },
-    "verb": {
-        "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced",
-        "display": {
-            "und": "experienced"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    },
-    "authority": {
-        "objectType": "Group",
-        "member": [
-            {
-                "objectType": "Agent",
-                "mbox": "mailto:bob@example.com"
-            },
-            {
-                "objectType": "Group",
-                "mbox": "mailto:group@example.com"
-            }
-        ]
-    }
-}
diff --git a/app/tests/Fixtures/Statements/Invalid/Verb/missing-display.json b/app/tests/Fixtures/Statements/Invalid/Verb/missing-display.json
deleted file mode 100644
index e5195cfe93..0000000000
--- a/app/tests/Fixtures/Statements/Invalid/Verb/missing-display.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-    "actor": {
-        "mbox": "mailto:zachary+pierce@gmail.com"
-    },
-    "verb": {
-        "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced"
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    }
-}
diff --git a/app/tests/Fixtures/Statements/Valid/Actor/group.json b/app/tests/Fixtures/Statements/Valid/Actor/group.json
deleted file mode 100644
index 77926f0ff1..0000000000
--- a/app/tests/Fixtures/Statements/Valid/Actor/group.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
-    "id": "88664422-1234-5678-1234-567812345678",
-    "actor": {
-        "objectType": "Group",
-        "member": [
-            {
-                "objectType": "Agent",
-                "mbox": "mailto:bob@example.com"
-            },
-            {
-                "objectType": "Agent",
-                "mbox": "mailto:group@example.com"
-            }
-        ]
-    },
-    "verb": {
-        "id": "http://adlnet.gov/expapi/verbs/created",
-        "display": {
-            "en-US": "created"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    }
-}
\ No newline at end of file
diff --git a/app/tests/Fixtures/Statements/Valid/Object/with-definition.json b/app/tests/Fixtures/Statements/Valid/Object/with-definition.json
deleted file mode 100644
index 7bab3ca69e..0000000000
--- a/app/tests/Fixtures/Statements/Valid/Object/with-definition.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
-    "id": "12345678-1234-5678-1234-567812345678",
-    "actor": {
-        "mbox": "mailto:zachary+pierce@gmail.com"
-    },
-    "verb": {
-        "id": "http://adlnet.gov/expapi/verbs/created",
-        "display": {
-            "en-US": "created"
-        }
-    },
-    "object": {
-        "id": "http:\/\/tincanapi.com\/GolfExample_TCAPI\/Playing\/Scoring.html",
-        "objectType": "Activity",
-        "definition": {
-            "name": {
-                "en-US": "Scoring"
-            },
-            "description": {
-                "en-US": "An overview of how to score a round of golf."
-            }
-        }
-    }
-}
diff --git a/app/tests/Fixtures/Statements/Valid/Verb/Display/multilingual.json b/app/tests/Fixtures/Statements/Valid/Verb/Display/multilingual.json
deleted file mode 100644
index 947ad1d520..0000000000
--- a/app/tests/Fixtures/Statements/Valid/Verb/Display/multilingual.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-    "id": "12345678-1234-5678-1234-567812345678",
-    "actor": {
-        "mbox": "mailto:zachary+pierce@gmail.com"
-    },
-    "verb": {
-        "id": "http://adlnet.gov/expapi/verbs/passed/",
-        "display": {
-            "en-US": "created",
-            "en-US": "created",
-            "vn": "tạo"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    }
-}
\ No newline at end of file
diff --git a/app/tests/Fixtures/Statements/Valid/simple.json b/app/tests/Fixtures/Statements/Valid/simple.json
deleted file mode 100644
index 957017d7e7..0000000000
--- a/app/tests/Fixtures/Statements/Valid/simple.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
-    "id": "12345678-1234-5678-1234-567812345678",
-    "actor": {
-        "mbox": "mailto:zachary+pierce@gmail.com"
-    },
-    "verb": {
-        "id": "http://adlnet.gov/expapi/verbs/created",
-        "display": {
-            "en-US": "created"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    }
-}
\ No newline at end of file
diff --git a/app/tests/Fixtures/Statements/Valid/with-authority.json b/app/tests/Fixtures/Statements/Valid/with-authority.json
deleted file mode 100644
index 8d87e9a0f5..0000000000
--- a/app/tests/Fixtures/Statements/Valid/with-authority.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
-    "id": "12345678-1234-5678-1234-567812345678",
-    "actor": {
-        "mbox": "mailto:zachary+pierce@gmail.com"
-    },
-    "verb": {
-        "id": "http://adlnet.gov/expapi/verbs/created",
-        "display": {
-            "en-US": "created"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    },
-    "authority": {
-        "name": "Quan Vo",
-        "mbox": "mailto:quan@ll.com",
-        "objectType": "Agent"
-    }
-}
\ No newline at end of file
diff --git a/app/tests/Fixtures/Statements/Valid/with-context.json b/app/tests/Fixtures/Statements/Valid/with-context.json
deleted file mode 100644
index e01466d3a0..0000000000
--- a/app/tests/Fixtures/Statements/Valid/with-context.json
+++ /dev/null
@@ -1,48 +0,0 @@
-{
-    "id": "12345678-1234-5678-1234-567812345678",
-    "actor": {
-        "mbox": "mailto:zachary+pierce@gmail.com"
-    },
-    "verb": {
-        "id": "http://adlnet.gov/expapi/verbs/created",
-        "display": {
-            "en-US": "created"
-        }
-    },
-    "object": {
-        "id": "http://ZackPierce.github.io/xAPI-Validator-JS",
-        "objectType": "Activity"
-    },
-    "context": {
-        "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7",
-        "contextActivities": {
-            "parent": [
-                {
-                    "id": "http://www.example.com/meetings/series/267",
-                    "objectType": "Activity"
-                }
-            ],
-            "category": [
-                {
-                    "id": "http://www.example.com/meetings/categories/teammeeting",
-                    "objectType": "Activity",
-                    "definition": {
-                        "name": {
-                            "en": "team meeting"
-                        },
-                        "description": {
-                            "en": "A category of meeting used for regular team meetings."
-                        },
-                        "type": "http://example.com/expapi/activities/meetingcategory"
-                    }
-                }
-            ],
-            "other": [
-                {
-                    "id": "http://www.example.com/meetings/occurances/3425567",
-                    "objectType": "Activity"
-                }
-            ]
-        }
-    }
-}
\ No newline at end of file
diff --git a/app/tests/Fixtures/pipeline.json b/app/tests/Fixtures/pipeline.json
deleted file mode 100644
index dcbeec71d0..0000000000
--- a/app/tests/Fixtures/pipeline.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"$match":{"active":true}},{"$project":{"_id":0,"statement":1}}]
diff --git a/app/tests/InstanceTestCase.php b/app/tests/InstanceTestCase.php
new file mode 100644
index 0000000000..3577925d45
--- /dev/null
+++ b/app/tests/InstanceTestCase.php
@@ -0,0 +1,41 @@
+<?php namespace Tests;
+
+abstract class InstanceTestCase extends TestCase {
+  protected $user, $site;
+
+  public function setUp() {
+    parent::setUp();
+    $this->user = $this->createUser();
+    $this->site = $this->createSite($this->user);
+  }
+
+  protected function createSite(\User $user) {
+    $model = new \Site([
+      'name' => 'Test',
+      'description' => '',
+      'email' => $user->email,
+      'lang' => 'en-US',
+      'create_lrs' => 'super',
+      'registration' => 'Closed',
+      'restrict' => 'None',
+      'domain' => '',
+      'super' => [['user' => $user->_id]]
+    ]);
+    $model->save();
+    return $model;
+  }
+
+  protected function createUser() {
+    $model = new \User([
+      'email' => 'test@example.com'
+    ]);
+    $model->save();
+    return $model;
+  }
+
+  public function tearDown() {
+    $this->site->delete();
+    $this->user->delete();
+    parent::tearDown();
+  }
+}
diff --git a/app/tests/LrsTest.php b/app/tests/LrsTest.php
deleted file mode 100644
index defd20a775..0000000000
--- a/app/tests/LrsTest.php
+++ /dev/null
@@ -1,107 +0,0 @@
-<?php
-
-class LrsTest extends TestCase
-{
-
-  public function setUp()
-  {
-    parent::setUp();
-
-    Route::enableFilters();
-
-    // Authentication as super user.
-    $user = User::firstOrCreate(['email' => 'quan@ll.com']);
-    Auth::login($user);
-  }
-
-  /**
-   * Test LRS
-   */
-  public function testLRS()
-  {
-    $lrs = new Lrs;
-
-    // Test title required.
-    $values = array(
-      'title' => '',
-      'description' => \Locker\Helpers\Helpers::getRandomValue(),
-      'api' => array('basic_key' => \Locker\Helpers\Helpers::getRandomValue(),
-        'basic_secret' => \Locker\Helpers\Helpers::getRandomValue())
-    );
-    $validator = $lrs->validate($values);
-    $this->assertTrue($validator->fails());
-    $this->assertFalse($validator->passes());
-
-    $values['title'] = \Locker\Helpers\Helpers::getRandomValue();
-    $validator = $lrs->validate($values);
-    $this->assertTrue($validator->passes());
-
-    // Validate auth_service
-
-    $values['auth_service_url'] = 'http://' . \Locker\Helpers\Helpers::getRandomValue() . '.adurolms.com';
-    $validator = $lrs->validate($values);
-    $this->assertTrue($validator->passes());
-
-    // Add new lrs
-    $lrs->title = $values['title'];
-    $lrs->description = $values['description'];
-    $lrs->api = $values['api'];
-    $result = $lrs->save();
-    $this->assertTrue($result);
-
-    // Load lrs from db
-    $lrs_id = $lrs->_id;
-    $db_lrs = Lrs::find($lrs_id);
-    $this->assertEquals($db_lrs->_id, $lrs->_id);
-
-    // Edit lrs
-    $title = \Locker\Helpers\Helpers::getRandomValue();
-    $db_lrs->title = $title;
-    $db_lrs->save();
-    $this->assertEquals($db_lrs->title, $title);
-
-    // Delete lrs
-    $db_lrs->delete();
-    $this->assertEquals(Lrs::find($lrs_id), NULL, 'delete lrs');
-  }
-
-  public function testInternalAuthentication()
-  {
-    $this->createLRS();
-
-    //create client for Auth Service
-    $auth = [
-      'api_key' => $this->lrs->api['basic_key'],
-      'api_secret' => $this->lrs->api['basic_secret'],
-    ];
-
-    $response = $this->_makeRequest($auth);
-    $this->assertEquals($response->getStatusCode(), 200);
-  }
-
-  public function testAuthenticationService()
-  {
-    $this->createLRS();
-
-    //create client for Auth Service
-    $auth = [
-      'api_key' => $this->lrs->api['basic_key'],
-      'api_secret' => $this->lrs->api['basic_secret'],
-    ];
-
-    // Make sure response data for the get request
-    $response = $this->_makeRequest($auth, ['auth_type' => 'central']);
-    $this->assertEquals($response->getStatusCode(), 200);
-  }
-
-  private function _makeRequest($auth, $param = [])
-  {
-    return $this->call("GET", '/data/xAPI/statements', $param, [], $this->makeRequestHeaders($auth));
-  }
-
-  public function testEnpoint()
-  {
-    $this->assertTrue(true);
-  }
-
-}
diff --git a/app/tests/LrsTestCase.php b/app/tests/LrsTestCase.php
new file mode 100644
index 0000000000..095fc94564
--- /dev/null
+++ b/app/tests/LrsTestCase.php
@@ -0,0 +1,39 @@
+<?php namespace Tests;
+use \Locker\Helpers\Helpers as Helpers;
+
+abstract class LrsTestCase extends InstanceTestCase {
+  protected $lrs;
+
+  public function setUp() {
+    parent::setUp();
+    $this->lrs = $this->createLrs($this->user);
+    $_SERVER['SERVER_NAME'] = $this->lrs->title.'.com.vn';
+  }
+
+  protected function createLrs(\User $user) {
+    $model = new \Lrs([
+      'title' => Helpers::getRandomValue(),
+      'description' => Helpers::getRandomValue(),
+      'subdomain' => Helpers::getRandomValue(),
+      'api' => [
+        'basic_key' => helpers::getRandomValue(),
+        'basic_secret' => helpers::getRandomValue()
+      ],
+      'owner' => ['_id' => $user->_id],
+      'users' => Helpers::getRandomValue(),
+      'users' => [[
+        '_id' => $user->_id,
+        'email' => $user->email,
+        'name' => $user->name,
+        'role' => 'admin'
+      ]]
+    ]);
+    $model->save();
+    return $model;
+  }
+
+  public function tearDown() {
+    $this->lrs->delete();
+    parent::tearDown();
+  }
+}
diff --git a/app/tests/StatementContextTest.php b/app/tests/StatementContextTest.php
deleted file mode 100644
index 569f257d79..0000000000
--- a/app/tests/StatementContextTest.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-class StatementContextTest extends TestCase
-{
-
-  public function setUp()
-  {
-    parent::setUp();
-    // Authentication as super user.
-    $user = User::firstOrCreate(array('email' => $this->dummyEmail()));
-    Auth::login($user);
-    $this->lrs = $this->createLRS();
-    $this->statement = App::make('Locker\Repository\Statement\EloquentStatementRepository');
-  }
-
-  /**
-   * The LRS MUST return single Activity Objects as an array of length one
-   * containing the same Activity.
-   */
-  public function testContextActivities()
-  {
-    $stmt = $this->defaultStatment();
-    // $parent = new stdClass();
-    // $parent->id = 'http://tincanapi.com/GolfExample_TCAPI';
-    // $parent->objectType = 'Activity';
-    $parent = [
-      'id' => 'http://tincanapi.com/GolfExample_TCAPI',
-      'objectType' => 'Activity'
-    ];
-    $contextActivities = ['parent' => [$parent]]; // $parent should be an array not an object.
-    $stmt['context']['contextActivities'] = $contextActivities;
-
-    $return = $this->createStatement($stmt, $this->lrs);
-
-    $id = $return[0];
-    $saved_statement = $this->statement->show($this->lrs->_id, $id)->first();
-    // The parent must be array.
-    $this->assertTrue(is_array($saved_statement['statement']['context']['contextActivities']['parent']));
-  }
-
-}
diff --git a/app/tests/StatementGetTest.php b/app/tests/StatementGetTest.php
deleted file mode 100644
index 48d3cdfebd..0000000000
--- a/app/tests/StatementGetTest.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-
-class StatementGetTest extends TestCase
-{
-
-  public function setUp()
-  {
-    parent::setUp();
-
-    Route::enableFilters();
-
-    // Authentication as super user.
-    $user = User::firstOrCreate(['email' => 'quan@ll.com']);
-    Auth::login($user);
-  }
-
-  private function _makeRequest($auth, $version)
-  {
-    return $this->call("GET", '/data/xAPI/statements', [], [], $this->makeRequestHeaders($auth, $version));
-  }
-
-  /**
-   * Create statements for lrs
-   *
-   * @param string $version Make sure LRS response to all valid version.
-   * @return void
-   * @dataProvider dataGetAuthService
-   */
-  public function testGetAuthService($version, $expecting_code)
-  {
-    $lrs = $this->createLRS();
-
-    // create client for Auth Service
-    $auth = [
-      'api_key' => $this->lrs->api['basic_key'],
-      'api_secret' => $this->lrs->api['basic_secret'],
-    ];
-
-
-    // Make sure response data for the get request
-    $response = $this->_makeRequest($auth, $version);
-    $this->assertEquals($expecting_code, $response->getStatusCode());
-
-    $lrs->delete();
-  }
-
-  public function dataGetAuthService() {
-    $data = [];
-
-    foreach (range(0, 20) as $i) {
-      if (array_rand([true, false])) {
-        $data[] = ["1.0.{$i}", 200];
-      }
-    }
-
-    //$data[] = ["0.9", 400];
-    //$data[] = ["1.1", 400];
-
-    return $data;
-  }
-}
diff --git a/app/tests/StatementPostTest.php b/app/tests/StatementPostTest.php
deleted file mode 100644
index 943f13cc1f..0000000000
--- a/app/tests/StatementPostTest.php
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-
-class StatementPostTest extends TestCase
-{
-
-  public function setUp()
-  {
-    parent::setUp();
-
-    Route::enableFilters();
-
-    // Authentication as super user.
-    $user = User::firstOrCreate(['email' => 'quan@ll.com']);
-    Auth::login($user);
-  }
-
-  private function _makeRequest($param, $method, $auth)
-  {
-    return $this->call($method, '/data/xAPI/statements', [], [], $this->makeRequestHeaders($auth), !empty($param) ? json_encode($param) : []);
-  }
-
-  /**
-   * Make a post request to LRS
-   *
-   * @return void
-   */
-  public function testPostBehavior()
-  {
-    $this->createLRS();
-
-    $vs = $this->defaultStatment();
-    $result = $this->createStatement($vs, $this->lrs);
-
-    $statement = App::make('Locker\Repository\Statement\EloquentStatementRepository');
-    $createdStatement = $statement->show($this->lrs->_id, $result[0])->first();
-
-    $param = array(
-      'actor' => $createdStatement->statement['actor'],
-      'verb' => $createdStatement->statement['verb'],
-      'context' => $createdStatement->statement['context'],
-      'object' => $createdStatement->statement['object'],
-      'id' => $createdStatement->statement['id'],
-      'timestamp' => $createdStatement->statement['timestamp'],
-    );
-
-    // create client for Auth Service
-    $auth = [
-      'api_key' => $this->lrs->api['basic_key'],
-      'api_secret' => $this->lrs->api['basic_secret'],
-    ];
-
-    // case: conflict-matches
-    /* @var $response Illuminate\Http\JsonResponse */
-    $response = $this->_makeRequest($param, "POST", $auth);
-    $this->assertEquals(200, $response->getStatusCode());
-
-    // case: conflict nomatch
-    $param['result'] = new \stdClass();
-    try {
-      $response = $this->_makeRequest($param, "POST", $auth);
-    } catch (\Exception $ex) {
-      $this->assertEquals(true, method_exists($ex, 'getStatusCode'));
-      $this->assertEquals(409, $ex->getStatusCode());
-    }
-
-    // Make sure response data for the get request
-    $responseGet = $this->_makeRequest(new \stdClass(), "GET", $auth);
-    $this->assertEquals(200, $responseGet->getStatusCode());
-
-    // Make sure response data for the get request
-    unset($param['result']);
-    $responsePost = $this->_makeRequest($param, "POST", $auth);
-    $this->assertEquals(200, $responsePost->getStatusCode());
-  }
-
-  /**
-   * make a post request to lrs with Auth Service
-   *
-   * @return void
-   */
-  public function testPostAuthService()
-  {
-    $this->createLRS();
-
-    $vs = $this->defaultStatment();
-    $result = $this->createStatement($vs, $this->lrs);
-
-    $statement = App::make('Locker\Repository\Statement\EloquentStatementRepository');
-    $createdStatement = $statement->show($this->lrs->_id, $result[0])->first();
-
-    $param = [
-      'actor' => $createdStatement->statement['actor'],
-      'verb' => $createdStatement->statement['verb'],
-      'context' => $createdStatement->statement['context'],
-      'object' => $createdStatement->statement['object'],
-      'id' => $createdStatement->statement['id'],
-      'timestamp' => $createdStatement->statement['timestamp'],
-    ];
-
-    // create client for Auth Service
-    $auth = [
-      'api_key' => $this->lrs->api['basic_key'],
-      'api_secret' => $this->lrs->api['basic_secret'],
-    ];
-
-    $response = $this->_makeRequest($param, "POST", $auth);
-
-    $responseData = $response->getContent();
-    $responseStatus = $response->getStatusCode();
-
-    $this->assertEquals(200, $responseStatus);
-  }
-
-}
diff --git a/app/tests/StatementPutTest.php b/app/tests/StatementPutTest.php
deleted file mode 100644
index 13d328e418..0000000000
--- a/app/tests/StatementPutTest.php
+++ /dev/null
@@ -1,119 +0,0 @@
-<?php
-
-class StatementPutTest extends TestCase
-{
-
-  public function setUp()
-  {
-    parent::setUp();
-
-    Route::enableFilters();
-
-    // Authentication as super user.
-    $user = User::firstOrCreate(['email' => 'quan@ll.com']);
-    Auth::login($user);
-  }
-
-  private function _makeRequest($param, $auth)
-  {
-    return $this->call('PUT', '/data/xAPI/statements', ['statementId' => $param['id']], [], $this->makeRequestHeaders($auth), json_encode($param));
-  }
-
-  /**
-   * Create statements for lrs
-   *
-   * @return void
-   */
-  public function testPutBehavior()
-  {
-    $this->createLRS();
-
-    $vs = $this->defaultStatment();
-    $result = $this->createStatement($vs, $this->lrs);
-
-    $statement = App::make('Locker\Repository\Statement\EloquentStatementRepository');
-    $createdStatement = $statement->show($this->lrs->_id, $result[0])->first();
-
-    $param = array(
-      'actor' => $createdStatement->statement['actor'],
-      'verb' => $createdStatement->statement['verb'],
-      'context' => $createdStatement->statement['context'],
-      'object' => $createdStatement->statement['object'],
-      'id' => $createdStatement->statement['id'],
-      'timestamp' => $createdStatement->statement['timestamp'],
-    );
-
-    // create client for Auth Service
-    $auth = [
-      'api_key' => $this->lrs->api['basic_key'],
-      'api_secret' => $this->lrs->api['basic_secret'],
-    ];
-
-    // case: conflict-matches
-    $response = $this->_makeRequest($param, $auth);
-    $responseData = method_exists($response, 'getData');
-    $responseStatus = $response->getStatusCode();
-
-    $this->assertEquals(204, $responseStatus);
-    $this->assertEquals(false, $responseData);
-
-    // case: conflict nomatch
-    $param['result'] = new \stdClass();
-    try {
-      $response = $this->_makeRequest($param, $auth);
-    } catch (\Exception $ex) {
-      $this->assertEquals(true, method_exists($ex, 'getStatusCode'));
-      $this->assertEquals(409, $ex->getStatusCode());
-    }
-  }
-
-  /**
-   * Create statements for lrs with Auth Service
-   *
-   * @return void
-   */
-  public function testPutAuthService()
-  {
-    $this->createLRS();
-
-    $vs = $this->defaultStatment();
-    $result = $this->createStatement($vs, $this->lrs);
-
-    $statement = App::make('Locker\Repository\Statement\EloquentStatementRepository');
-    $createdStatement = $statement->show($this->lrs->_id, $result[0])->first();
-
-    $param = [
-      'actor' => $createdStatement->statement['actor'],
-      'verb' => $createdStatement->statement['verb'],
-      'context' => $createdStatement->statement['context'],
-      'object' => $createdStatement->statement['object'],
-      'id' => $createdStatement->statement['id'],
-      'timestamp' => $createdStatement->statement['timestamp'],
-    ];
-
-    // create client for Auth Service
-    // create client for Auth Service
-    $auth = [
-      'api_key' => $this->lrs->api['basic_key'],
-      'api_secret' => $this->lrs->api['basic_secret'],
-    ];
-
-    $response = $this->_makeRequest($param, $auth);
-    $responseData = method_exists($response, 'getData');
-    $responseStatus = $response->getStatusCode();
-
-    $this->assertEquals(204, $responseStatus);
-    $this->assertEquals(false, $responseData);
-  }
-
-  public function tearDown()
-  {
-    parent::tearDown();
-
-    // Need LRS table is empty because waiting the getLrsBySubdomain()
-    if ($this->lrs) {
-      $this->lrs->delete();
-    }
-  }
-
-}
diff --git a/app/tests/StatementTest.php b/app/tests/StatementTest.php
deleted file mode 100644
index 6abdfbc996..0000000000
--- a/app/tests/StatementTest.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-class StatementTest extends TestCase {
-
-  public function setUp() {
-    parent::setUp();
-    // Authentication as super user.
-    $user = User::firstOrCreate(array('email' => 'quan@ll.com'));
-    Auth::login($user);  
-    $this->createLRS();
-  }
-
-  /**
-   * Create statements for lrs
-   *
-   * @return void
-   */
-  public function testCreate() {
-    $vs = $this->defaultStatment();
-    
-    $statement = App::make('Locker\Repository\Statement\EloquentStatementRepository');
-    $result = $statement->create([json_decode(json_encode($vs))], $this->lrs);
-    
-    $this->assertTrue(is_array($result));
-  }
-
-}
diff --git a/app/tests/StatementVersionTest.php b/app/tests/StatementVersionTest.php
deleted file mode 100644
index 7bb7cd62d7..0000000000
--- a/app/tests/StatementVersionTest.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-
-class StatementVersionTest extends TestCase
-{
-
-    public function setUp()
-    {
-        parent::setUp();
-
-        Route::enableFilters();
-
-        // Authentication as super user.
-        $user = User::firstOrCreate(['email' => 'andy@ll.com']);
-        Auth::login($user);
-
-        $this->lrs = $this->createLRS();
-    }
-
-    /**
-     * If no 'version' provided in statement, LRS must save '1.0.0' as default
-     * as value.
-     */
-    public function testMissingVersion()
-    {
-        $statement = $this->defaultStatment();
-
-        // make sure there is no version
-        $this->assertTrue(!isset($statement['version']), 'There is no version provided');
-
-        $return = $this->createStatement($statement, $this->lrs);
-
-        $id = reset($return);
-
-        /* @var $saved_statement Statement */
-        $saved_statement = App::make('Locker\Repository\Statement\EloquentStatementRepository')->show($this->lrs->_id, $id)->first();
-        $this->assertTrue(isset($saved_statement->statement['version']), 'There is no version provided');
-        $this->assertEquals('1.0.0', $saved_statement->statement['version']);
-    }
-
-}
diff --git a/app/tests/StatementsTestCase.php b/app/tests/StatementsTestCase.php
new file mode 100644
index 0000000000..d74093901f
--- /dev/null
+++ b/app/tests/StatementsTestCase.php
@@ -0,0 +1,57 @@
+<?php namespace Tests;
+use \Locker\Helpers\Helpers as Helpers;
+
+abstract class StatementsTestCase extends LrsTestCase {
+  protected $statements;
+
+  public function setUp() {
+    parent::setUp();
+    $this->statements = [$this->createStatement(
+      $this->lrs,
+      $this->generateStatement()
+    )];
+  }
+
+  protected function generateStatement($statement = []) {
+    $timestamp = Helpers::getCurrentDate();
+    return array_merge([
+      'actor' => [
+        'mbox' => 'mailto:test@example.com',
+        'objectType' => 'Agent'
+      ],
+      'verb' => [
+        'id' => 'http://www.example.com/verbs/test'
+      ],
+      'object' => [
+        'id' => 'http://www.example.com/objects/test',
+        'objectType' => 'Activity'
+      ],
+      'timestamp' => $timestamp,
+      'stored' => $timestamp,
+      'authority' => [
+        'mbox' => 'mailto:test@example.com',
+        'objectType' => 'Agent'
+      ]
+    ], $statement);
+  }
+
+  protected function createStatement(\Lrs $lrs, array $statement) {
+    $model = new \Statement([
+      'lrs' => ['_id' => $lrs->_id],
+      'statement' => $statement,
+      'active' => true,
+      'voided' => false,
+      'refs' => []
+    ]);
+    $model->timestamp = new \MongoDate(strtotime($model->statement['timestamp']));
+    $model->save();
+    return $model;
+  }
+
+  public function tearDown() {
+    array_map(function ($statement) {
+      $statement->delete();
+    }, $this->statements);
+    parent::tearDown();
+  }
+}
diff --git a/app/tests/TestCase.php b/app/tests/TestCase.php
index 4e791cb2e9..c39d6bab66 100644
--- a/app/tests/TestCase.php
+++ b/app/tests/TestCase.php
@@ -1,168 +1,16 @@
-<?php
+<?php namespace Tests;
+use \Illuminate\Foundation\Testing\TestCase as IlluminateTest;
 
-use \Locker\Helpers\Helpers as helpers;
-
-class TestCase extends Illuminate\Foundation\Testing\TestCase
-{
+abstract class TestCase extends IlluminateTest {
 
   /**
    * Creates the application.
-   *
    * @return \Symfony\Component\HttpKernel\HttpKernelInterface
    */
-  public function createApplication()
-  {
+  public function createApplication() {
     $unitTesting = true;
-
     $testEnvironment = 'testing';
-
     return require __DIR__ . '/../../bootstrap/start.php';
   }
 
-  public function setUp()
-  {
-    parent::setUp();
-    $user = User::firstOrCreate(['email' => 'quan@ll.com']);
-    //if first user, create site object
-    if (\User::count() == 1) {
-      $site = new \Site;
-      $site->name = 'Test';
-      $site->description = '';
-      $site->email = $user->email;
-      $site->lang = 'en-US';
-      $site->create_lrs = array('super');
-      $site->registration = 'Closed';
-      $site->restrict = 'None'; //restrict registration to a specific email domain
-      $site->domain = '';
-      $site->super = array(array('user' => $user->_id));
-      $site->save();
-    }
-  }
-
-  /**
-   * Create dummy LRS
-   * @return \Lrs
-   */
-  protected function createLRS()
-  {
-    $lrs = new Lrs;
-    $lrs->title = helpers::getRandomValue();
-    $lrs->description = helpers::getRandomValue();
-    $lrs->subdomain = helpers::getRandomValue();
-    $lrs->api = array(
-      'basic_key' => helpers::getRandomValue(),
-      'basic_secret' => helpers::getRandomValue()
-    );
-
-    // $lrs->auth_service = property_exists($this, 'lrsAuthMethod') ? $this->lrsAuthMethod : Lrs::INTERNAL_LRS;
-    // $lrs->auth_service_url = property_exists($this, 'auth_service_url') ?
-    //     $this->auth_service_url : '';
-    // $lrs->token = 'our-token';
-
-    $lrs->owner = array('_id' => Auth::user()->_id);
-    $lrs->users = array(
-      array('_id' => Auth::user()->_id,
-        'email' => Auth::user()->email,
-        'name' => Auth::user()->name,
-        'role' => 'admin'
-      )
-    );
-
-    $lrs->save();
-    $this->lrs = $lrs;
-
-    // Hack header request
-    $_SERVER['SERVER_NAME'] = $this->lrs->title . '.com.vn';
-    return $lrs;
-  }
-
-  /**
-   * Return default statement data.
-   */
-  protected function defaultStatment()
-  {
-    $siteAttrs = \Site::first();
-
-    return [
-      'actor' => [
-        'objectType' => 'Agent',
-        'mbox' => 'mailto:duy.nguyen@go1.com.au',
-        'name' => 'duynguyen'
-      ],
-      'verb' => [
-        "id" => "http://adlnet.gov/expapi/verbs/experienced",
-        "display" => ["und" => "experienced"]
-      ],
-      'context' => [
-        "contextActivities" => [
-          "parent" => [[
-            "id" => "http://tincanapi.com/GolfExample_TCAPI",
-            "objectType" => "Activity"
-          ]],
-          "grouping" => [[
-            "id" => "http://tincanapi.com/GolfExample_TCAPI",
-            "objectType" => "Activity"
-          ]]
-        ]
-      ],
-      "object" => [
-        "id" => "http://tincanapi.com/GolfExample_TCAPI/Playing/Scoring.html",
-        "objectType" => "Activity",
-        "definition" => [
-          "name" => [
-            "en-US" => "Scoring"
-          ],
-          "description" => [
-            "en-US" => "An overview of how to score a round of golf."
-          ]
-        ]
-      ],
-      "authority" => [
-        "name" => $siteAttrs->name,
-        "mbox" => "mailto:" . $siteAttrs->email,
-        "objectType" => "Agent"
-      ],
-    ];
-  }
-
-  /**
-   * Create dummy statement with lrs.
-   *
-   * @param Lrs $lrs
-   * @return type
-   */
-  protected function createStatement($statement, $lrs)
-  {
-    return App::make('Locker\Repository\Statement\EloquentStatementRepository')
-      ->create([json_decode(json_encode($statement))], $lrs);
-  }
-
-  /**
-   * Create dummy Auth Client
-   * @param type $lrs
-   * @return type
-   */
-  protected function createClientAuth($auth)
-  {
-    return [
-      'name' => helpers::getRandomValue(),
-      'api_key' => $auth['api_key'],
-      'api_secret' => $auth['api_secret'],
-    ];
-  }
-
-  protected function dummyEmail()
-  {
-    return helpers::getRandomValue() . '@go1.com.au';
-  }
-
-  protected function makeRequestHeaders($auth, $version="1.0.1")
-  {
-    return [
-      'PHP_AUTH_USER' => $auth['api_key'],
-      'PHP_AUTH_PW' => $auth['api_secret'],
-      'HTTP_X-Experience-API-Version' => $version
-    ];
-  }
-
 }
diff --git a/app/tests/Fixtures/Repos/StatementFormatter1.json b/app/tests/fixtures/Repos/StatementFormatter1.json
similarity index 100%
rename from app/tests/Fixtures/Repos/StatementFormatter1.json
rename to app/tests/fixtures/Repos/StatementFormatter1.json
diff --git a/app/tests/Fixtures/statement.json b/app/tests/fixtures/statement.json
similarity index 100%
rename from app/tests/Fixtures/statement.json
rename to app/tests/fixtures/statement.json
diff --git a/app/tests/readme.md b/app/tests/readme.md
new file mode 100644
index 0000000000..b511630e12
--- /dev/null
+++ b/app/tests/readme.md
@@ -0,0 +1,25 @@
+# Tests
+This file explains the purposes of some of the classes and directories within this directory.
+
+## Abstract Classes
+### TestCase
+This is a class to extend for all tests.
+
+### InstanceTestCase (extends TestCase)
+This is a class to extend for testing a Learning Locker (LL) instance/installation. Any tests that depend on a LL instance being setup before testing should utilise this class.
+
+### LrsTestCase (extends InstanceTestCase)
+This is a class to extend for testing an LRS inside a LL instance. Any tests that depend on an LRS being created before testing should utilise this class.
+
+### StatementsTestCase (extends LrsTestCase)
+This is a class to extend for testing retrieval of statements inside an LRS. Any tests that depend on a statements being created before or during testing should utilise this class.
+
+## Directories
+### fixtures
+This directory should contain any files required by the tests.
+
+### repos
+This directory should contain tests for repositories (data in, data out).
+
+### routes
+This directory should contain tests for routes (request in, response out). Inside this directory there is a RouteTestTrait that should be used by all classes inside this directory for sending requests.
diff --git a/app/tests/repos/Statement/EloquentIndexerTest.php b/app/tests/repos/Statement/EloquentIndexerTest.php
new file mode 100644
index 0000000000..3ae0fb5ed5
--- /dev/null
+++ b/app/tests/repos/Statement/EloquentIndexerTest.php
@@ -0,0 +1,56 @@
+<?php namespace Tests\Repos\Statement;
+
+use \Locker\Repository\Statement\EloquentIndexer as Indexer;
+use \Locker\Repository\Statement\IndexOptions as IndexOptions;
+
+class EloquentIndexerTest extends EloquentTest {
+
+  public function setup() {
+    parent::setup();
+    $this->indexer = new Indexer();
+  }
+
+  public function testIndex() {
+    $opts = new IndexOptions([
+      'lrs_id' => $this->lrs->_id
+    ]);
+    $result = $this->indexer->index($opts);
+
+    $this->assertEquals(true, is_object($result));
+    $this->assertEquals('Jenssegers\Mongodb\Eloquent\Builder', get_class($result));
+  }
+
+  public function testFormat() {
+    $opts = new IndexOptions([
+      'lrs_id' => $this->lrs->_id
+    ]);
+    $result = $this->indexer->index($opts);
+    $result = $this->indexer->format($result, $opts);
+
+    $this->assertEquals(true, is_array($result));
+    $this->assertEquals(count($this->statements), count($result));
+    foreach ($result as $statement) {
+      $this->assertEquals(true, is_object($statement));
+      $this->assertEquals(true, isset($statement->id));
+      $this->assertEquals(true, is_string($statement->id));
+      $expected_statement = $this->statements[0]->statement;
+      $this->assertStatementMatch($expected_statement, $statement);
+    }
+  }
+
+  public function testCount() {
+    $opts = new IndexOptions([
+      'lrs_id' => $this->lrs->_id
+    ]);
+    $result = $this->indexer->index($opts);
+    $result = $this->indexer->count($result, $opts);
+
+    $this->assertEquals(true, is_int($result));
+    $this->assertEquals(count($this->statements), $result);
+  }
+
+  protected function assertStatementMatch(\stdClass $statement_a, \stdClass $statement_b) {
+    unset($statement_b->version);
+    $this->assertEquals(true, $statement_a == $statement_b);
+  }
+}
diff --git a/app/tests/repos/Statement/EloquentLinkerTest.php b/app/tests/repos/Statement/EloquentLinkerTest.php
new file mode 100644
index 0000000000..f1108f2146
--- /dev/null
+++ b/app/tests/repos/Statement/EloquentLinkerTest.php
@@ -0,0 +1,16 @@
+<?php namespace Tests\Repos\Statement;
+
+use \Locker\Repository\Statement\EloquentLinker as Linker;
+use \Locker\Repository\Statement\StorerOptions as StorerOptions;
+
+class EloquentLinkerTest extends EloquentTest {
+
+  public function setup() {
+    parent::setup();
+    $this->linker = new Linker();
+  }
+
+  public function test() {
+    return null;
+  }
+}
diff --git a/app/tests/repos/Statement/EloquentShowerTest.php b/app/tests/repos/Statement/EloquentShowerTest.php
new file mode 100644
index 0000000000..911acaff98
--- /dev/null
+++ b/app/tests/repos/Statement/EloquentShowerTest.php
@@ -0,0 +1,75 @@
+<?php namespace Tests\Repos\Statement;
+
+use \Locker\Repository\Statement\EloquentShower as Shower;
+use \Locker\Repository\Statement\ShowOptions as ShowOptions;
+
+class EloquentShowerTest extends EloquentTest {
+
+  public function setup() {
+    parent::setup();
+    $this->shower = new Shower();
+  }
+
+  public function testShow() {
+    $opts = new ShowOptions([
+      'lrs_id' => $this->lrs->_id
+    ]);
+    $result = $this->shower->show('00000000-0000-0000-0000-000000000000', $opts);
+
+    $this->assertEquals(true, is_object($result));
+    $this->assertEquals('stdClass', get_class($result));
+    $this->assertStatementMatch($this->statements[0]->statement, $result);
+  }
+
+  public function testShowInactive() {
+    $opts = new ShowOptions([
+      'lrs_id' => $this->lrs->_id,
+      'active' => false
+    ]);
+    $model = $this->createStatement(1);
+    $model->active = false;
+    $model->save();
+    $result = $this->shower->show('10000000-0000-0000-0000-000000000000', $opts);
+
+    $this->assertEquals(true, is_object($result));
+    $this->assertEquals('stdClass', get_class($result));
+    $this->assertStatementMatch($model->statement, $result);
+  }
+
+  public function testShowVoided() {
+    $opts = new ShowOptions([
+      'lrs_id' => $this->lrs->_id,
+      'voided' => true
+    ]);
+    $model = $this->createStatement(1);
+    $model->voided = true;
+    $model->save();
+    $result = $this->shower->show('10000000-0000-0000-0000-000000000000', $opts);
+
+    $this->assertEquals(true, is_object($result));
+    $this->assertEquals('stdClass', get_class($result));
+    $this->assertStatementMatch($model->statement, $result);
+  }
+
+  public function testShowInactiveVoided() {
+    $opts = new ShowOptions([
+      'lrs_id' => $this->lrs->_id,
+      'active' => false,
+      'voided' => true
+    ]);
+    $model = $this->createStatement(1);
+    $model->active = false;
+    $model->voided = true;
+    $model->save();
+    $result = $this->shower->show('10000000-0000-0000-0000-000000000000', $opts);
+
+    $this->assertEquals(true, is_object($result));
+    $this->assertEquals('stdClass', get_class($result));
+    $this->assertStatementMatch($model->statement, $result);
+  }
+
+  private function assertStatementMatch(\stdClass $statement_a, \stdClass $statement_b) {
+    unset($statement_b->version);
+    $this->assertEquals(true, $statement_a == $statement_b);
+  }
+}
diff --git a/app/tests/repos/Statement/EloquentTest.php b/app/tests/repos/Statement/EloquentTest.php
new file mode 100644
index 0000000000..84229c3875
--- /dev/null
+++ b/app/tests/repos/Statement/EloquentTest.php
@@ -0,0 +1,59 @@
+<?php namespace Tests\Repos\Statement;
+
+use \Illuminate\Foundation\Testing\TestCase as Base;
+
+abstract class EloquentTest extends Base {
+
+  public function setup() {
+    parent::setup();
+    $this->lrs = $this->createLRS();
+    $this->statements = [$this->createStatement(0)];
+  }
+
+  public function createApplication() {
+    $unitTesting = true;
+    $testEnvironment = 'testing';
+    return require __DIR__ . '/../../../../bootstrap/start.php';
+  }
+
+  private function createLrs() {
+    $lrs = new \Lrs([
+      'title' => 'TestLRS',
+      'api' => [],
+      'owner' => [],
+      'users' => [],
+      'domain' => '',
+    ]);
+
+    $lrs->save();
+    return $lrs;
+  }
+
+  protected function createStatement($id) {
+    $model = new \Statement($this->getStatement());
+    $model->statement->id = ((string) $id).'0000000-0000-0000-0000-000000000000';
+    $model->save();
+    return $model;
+  }
+
+  protected function getStatement() {
+    return [
+      'statement' => json_decode(file_get_contents(__DIR__ . '../../../fixtures/statement.json')),
+      'active' => true,
+      'voided' => false,
+      'refs' => [],
+      'timestamp' => new \MongoDate(strtotime('now')),
+      'lrs' => [
+        '_id' => $this->lrs->_id
+      ]
+    ];
+  }
+
+  public function tearDown() {
+    $this->lrs->delete();
+    foreach ($this->statements as $statement) {
+      $statement->delete();
+    }
+    parent::tearDown();
+  }
+}
diff --git a/app/tests/Repos/StatementFormatterTest.php b/app/tests/repos/Statement/FormatterTest.php
similarity index 90%
rename from app/tests/Repos/StatementFormatterTest.php
rename to app/tests/repos/Statement/FormatterTest.php
index 977cfcd756..84524e258f 100644
--- a/app/tests/Repos/StatementFormatterTest.php
+++ b/app/tests/repos/Statement/FormatterTest.php
@@ -1,9 +1,9 @@
-<?php namespace Tests\Repos;
+<?php namespace Tests\Repos\Statement;
 
 use \Illuminate\Foundation\Testing\TestCase as Base;
 use \Locker\Repository\Statement\Formatter as Formatter;
 
-class StatementFormatterTest extends Base {
+class FormatterTest extends Base {
 
   public function setup() {
     parent::setup();
@@ -13,7 +13,7 @@ public function setup() {
   public function createApplication() {
     $unitTesting = true;
     $testEnvironment = 'testing';
-    return require __DIR__ . '/../../../bootstrap/start.php';
+    return require __DIR__ . '/../../../../bootstrap/start.php';
   }
 
   private function cloneObj(\stdClass $obj) {
@@ -21,7 +21,7 @@ private function cloneObj(\stdClass $obj) {
   }
 
   public function testIdentityStatements() {
-    $statement = json_decode(file_get_contents(__DIR__ . '/../Fixtures/Repos/StatementFormatter1.json'));
+    $statement = json_decode(file_get_contents(__DIR__ . '/../../fixtures/Repos/StatementFormatter1.json'));
     $formatted = $this->formatter->identityStatement($this->cloneObj($statement));
 
     $this->assertEquals(true, is_object($formatted));
@@ -54,7 +54,7 @@ public function testIdentityStatements() {
   }
 
   public function testCanonicalStatements() {
-    $statement = json_decode(file_get_contents(__DIR__ . '/../Fixtures/Repos/StatementFormatter1.json'));
+    $statement = json_decode(file_get_contents(__DIR__ . '/../../fixtures/Repos/StatementFormatter1.json'));
     $formatted = $this->formatter->canonicalStatement($this->cloneObj($statement), ['en-GB']);
 
     $this->assertEquals(true, is_object($formatted));
diff --git a/app/tests/repos/Statement/IndexOptionsTest.php b/app/tests/repos/Statement/IndexOptionsTest.php
new file mode 100644
index 0000000000..1e67750412
--- /dev/null
+++ b/app/tests/repos/Statement/IndexOptionsTest.php
@@ -0,0 +1,100 @@
+<?php namespace Tests\Repos\Statement;
+
+class IndexOptionsTest extends OptionsTest {
+  protected $options_class = '\Locker\Repository\Statement\IndexOptions';
+  protected $overwrite_opts = [
+    'lrs_id' => '1',
+  ];
+  protected $default_opts = [
+    'agent' => null,
+    'activity' => null,
+    'verb' => null,
+    'registration' => null,
+    'since' => null,
+    'until' => null,
+    'active' => true,
+    'voided' => false,
+    'related_activities' => false,
+    'related_agents' => false,
+    'ascending' => false,
+    'format' => 'exact',
+    'offset' => 0,
+    'limit' => 100,
+    'langs' => [],
+    'attachments' => false,
+    'lrs_id' => '1'
+  ];
+  protected $valid_opts = [
+    'activity' => 'http://www.example.com',
+    'verb' => 'http://www.example.com',
+    'registration' => '93439880-e35a-11e4-b571-0800200c9a66',
+    'since' => '2015-01-01T00:00Z',
+    'until' => '2015-01-01T00:00Z',
+    'active' => true,
+    'voided' => true,
+    'related_activities' => true,
+    'related_agents' => true,
+    'ascending' => true,
+    'format' => 'exact',
+    'offset' => 0,
+    'limit' => 1,
+    'langs' => [],
+    'attachments' => true,
+    'lrs_id' => '1'
+  ];
+  protected $invalid_opts = [
+    'activity' => 'zz.example.com',
+    'verb' => 'zz.example.com',
+    'registration' => '93439880-e35a-11e4-b571-0800200c9a66Z',
+    'since' => '2015-01-01T00:00ZYX',
+    'until' => '2015-01-01T00:00ZYX',
+    'active' => 'invalid',
+    'voided' => 'invalid',
+    'related_activities' => 'invalid',
+    'related_agents' => 'invalid',
+    'ascending' => 'invalid',
+    'format' => 'invalid',
+    'offset' => -1,
+    'limit' => -1,
+    'langs' => 'invalid',
+    'attachments' => 'invalid',
+    'lrs_id' => true
+  ];
+
+  public function setup() {
+    parent::setup();
+    $this->valid_opts['agent'] = (object) ['mbox' => 'mailto:test@example.com'];
+    $this->invalid_opts['agent'] = (object) ['mbox' => 'test@example.com'];
+  }
+
+  public function testValidCanonicalFormat() {
+    try {
+      $start_opts = $this->valid_opts;
+      $start_opts['format'] = 'canonical';
+      $end_opts = new $this->options_class($start_opts);
+
+      foreach ($end_opts->options as $key => $val) {
+        $this->assertEquals($start_opts[$key], $val);
+      }
+    } catch (\Exception $ex) {
+      \Log::error($ex);
+      $this->assertEquals(false, true, $ex->getMessage());
+    }
+  }
+
+  public function testValidIdsFormat() {
+    try {
+      $start_opts = $this->valid_opts;
+      $start_opts['format'] = 'ids';
+      $end_opts = new $this->options_class($start_opts);
+
+      foreach ($end_opts->options as $key => $val) {
+        $this->assertEquals($start_opts[$key], $val);
+      }
+    } catch (\Exception $ex) {
+      \Log::error($ex);
+      $this->assertEquals(false, true, $ex->getMessage());
+    }
+  }
+
+}
diff --git a/app/tests/repos/Statement/OptionsTest.php b/app/tests/repos/Statement/OptionsTest.php
new file mode 100644
index 0000000000..95dd75f6d9
--- /dev/null
+++ b/app/tests/repos/Statement/OptionsTest.php
@@ -0,0 +1,74 @@
+<?php namespace Tests\Repos\Statement;
+
+use \Illuminate\Foundation\Testing\TestCase as Base;
+
+abstract class OptionsTest extends Base {
+  protected $default_opts = [];
+  protected $overwrite_opts = [];
+  protected $valid_opts = [];
+  protected $invalid_opts = [];
+  protected $options_class = '';
+
+  public function createApplication() {
+    $unitTesting = true;
+    $testEnvironment = 'testing';
+    return require __DIR__ . '/../../../../bootstrap/start.php';
+  }
+
+  public function testValidOptions() {
+    try {
+      $start_opts = $this->valid_opts;
+      $end_opts = new $this->options_class($start_opts);
+
+      foreach ($end_opts->options as $key => $val) {
+        $this->assertEquals($start_opts[$key], $val);
+      }
+    } catch (\Exception $ex) {
+      \Log::error($ex);
+      $this->assertEquals(false, true, $ex->getMessage());
+    }
+  }
+
+  public function testDefaultOptions() {
+    try {
+      $start_opts = $this->default_opts;
+      $end_opts = new $this->options_class($this->overwrite_opts);
+
+      foreach ($end_opts->options as $key => $val) {
+        $this->assertEquals($start_opts[$key], $val);
+      }
+    } catch (\Exception $ex) {
+      \Log::error($ex);
+      $this->assertEquals(false, true, $ex->getMessage());
+    }
+  }
+
+  public function testInvalidOptions() {
+    foreach ($this->invalid_opts as $invalid_key => $invalid_val) {
+      try {
+        $caught = false;
+        $start_opts = $this->valid_opts;
+        $start_opts[$invalid_key] = $invalid_val;
+        $end_opts = new $this->options_class($start_opts);
+      } catch (\Exception $ex) {
+        $caught = true;
+      }
+      $this->assertEquals(true, $caught);
+    }
+  }
+
+  public function testGetOpts() {
+    try {
+      $start_opts = $this->valid_opts;
+      $end_opts = new $this->options_class($start_opts);
+
+      foreach ($start_opts as $key => $val) {
+        $this->assertEquals($val, $end_opts->getOpt($key));
+      }
+    } catch (\Exception $ex) {
+      \Log::error($ex);
+      $this->assertEquals(false, true, $ex->getMessage());
+    }
+  }
+
+}
diff --git a/app/tests/repos/Statement/ShowOptionsTest.php b/app/tests/repos/Statement/ShowOptionsTest.php
new file mode 100644
index 0000000000..67cd536095
--- /dev/null
+++ b/app/tests/repos/Statement/ShowOptionsTest.php
@@ -0,0 +1,24 @@
+<?php namespace Tests\Repos\Statement;
+
+class ShowOptionsTest extends OptionsTest {
+  protected $options_class = '\Locker\Repository\Statement\ShowOptions';
+  protected $overwrite_opts = [
+    'lrs_id' => '1',
+  ];
+  protected $default_opts = [
+    'lrs_id' => '1',
+    'voided' => false,
+    'active' => true
+  ];
+  protected $valid_opts = [
+    'lrs_id' => '1',
+    'voided' => true,
+    'active' => false
+  ];
+  protected $invalid_opts = [
+    'lrs_id' => true,
+    'voided' => 'false',
+    'active' => 'true'
+  ];
+
+}
diff --git a/app/tests/repos/Statement/StoreOptionsTest.php b/app/tests/repos/Statement/StoreOptionsTest.php
new file mode 100644
index 0000000000..623c8a0069
--- /dev/null
+++ b/app/tests/repos/Statement/StoreOptionsTest.php
@@ -0,0 +1,26 @@
+<?php namespace Tests\Repos\Statement;
+
+class StoreOptionsTest extends OptionsTest {
+  protected $options_class = '\Locker\Repository\Statement\StoreOptions';
+  protected $overwrite_opts = [
+    'lrs_id' => '1',
+  ];
+  protected $default_opts = [
+    'lrs_id' => '1',
+  ];
+  protected $valid_opts = [
+    'lrs_id' => '1',
+  ];
+  protected $invalid_opts = [
+    'lrs_id' => true
+  ];
+
+  public function setup() {
+    parent::setup();
+    $this->overwrite_opts['authority'] = (object) ['mbox' => 'mailto:test@example.com'];
+    $this->default_opts['authority'] = $this->overwrite_opts['authority'];
+    $this->valid_opts['authority'] = $this->overwrite_opts['authority'];
+    $this->invalid_opts['authority'] = (object) ['mbox' => 'test@example.com'];
+  }
+
+}
diff --git a/app/tests/API/ExportsTest.php b/app/tests/routes/ExportsTest.php
similarity index 83%
rename from app/tests/API/ExportsTest.php
rename to app/tests/routes/ExportsTest.php
index 2355ae025d..02a529380e 100644
--- a/app/tests/API/ExportsTest.php
+++ b/app/tests/routes/ExportsTest.php
@@ -1,5 +1,4 @@
-<?php namespace Tests\API;
-use \Report as Report;
+<?php namespace Tests\Routes;
 
 class ExportsTest extends ResourcesTestCase {
   static protected $endpoint = '/api/v1/exports';
@@ -22,7 +21,7 @@ protected function constructData($data) {
   }
 
   protected function createReport() {
-    $model = new Report([
+    $model = new \Report([
       'name' => 'Test report',
       'description' => 'Test report description',
       'query' => [
@@ -35,7 +34,7 @@ protected function createReport() {
   }
 
   public function testJson() {
-    $response = $this->requestAPI('GET', static::$endpoint.'/'."{$this->model->_id}/show");
+    $response = $this->requestResource('GET', static::$endpoint.'/'."{$this->model->_id}/show");
     $content = $response->getContent();
 
     // Checks that the response is correct.
@@ -43,7 +42,7 @@ public function testJson() {
   }
 
   public function testCsv() {
-    $response = $this->requestAPI('GET', static::$endpoint.'/'."{$this->model->_id}/show/csv");
+    $response = $this->requestResource('GET', static::$endpoint.'/'."{$this->model->_id}/show/csv");
     $content = $response->getContent();
 
     // Checks that the response is correct.
diff --git a/app/tests/routes/QueryTest.php b/app/tests/routes/QueryTest.php
new file mode 100644
index 0000000000..055aee6d89
--- /dev/null
+++ b/app/tests/routes/QueryTest.php
@@ -0,0 +1,119 @@
+<?php namespace Tests\Routes;
+
+class QueryTest extends \Tests\StatementsTestCase {
+  use RouteTestTrait;
+
+  public function setup() {
+    parent::setup();
+  }
+
+  protected function requestAnalytics($params = []) {
+    $content = null;
+    return $this->request('GET', 'api/v1/query/analytics', $params, $this->getServer($this->lrs), $content);
+  }
+
+  public function testAnalyticsDefault() {
+    $response = $this->requestAnalytics();
+    $data = $response->getData();
+    $this->assertEquals($data->version, 'v1');
+    $this->assertEquals($data->route, 'api/v1/query/analytics');
+  }
+
+  public function testAnalyticsTime() {
+    $response = $this->requestAnalytics(['type' => 'time']);
+    $data = $response->getData()->data;
+    $this->assertEquals(count($this->statements), $data[0]->count);
+  }
+
+  public function testUserQuery() {
+    $response = $this->requestAnalytics(['type' => 'user']);
+    $data = $response->getData();
+
+    $this->assertEquals(true, is_object($data));
+    $this->assertEquals(true, isset($data->data));
+    $this->assertEquals(true, is_array($data->data));
+    $this->assertEquals(count($this->statements), count($data->data));
+
+    // Asserts correct properties on data[0].
+    $this->assertEquals(true, isset($data->data[0]->count));
+    $this->assertEquals(true, isset($data->data[0]->dates));
+    $this->assertEquals(true, isset($data->data[0]->data));
+
+    // Asserts correct property types on data[0].
+    $this->assertEquals(true, is_numeric($data->data[0]->count));
+    $this->assertEquals(true, is_array($data->data[0]->dates));
+    $this->assertEquals(true, is_object($data->data[0]->data));
+
+    // Asserts correct values on data[0].
+    $this->assertEquals(count($this->statements), $data->data[0]->count);
+    $this->assertEquals(count($this->statements), count($data->data[0]->dates));
+
+    // Asserts correct properties on data[0]->data.
+    $this->assertEquals(true, isset($data->data[0]->data->mbox));
+    $this->assertEquals(true, isset($data->data[0]->data->objectType));
+
+    // Asserts correct property types on data[0]->data.
+    $this->assertEquals(true, is_string($data->data[0]->data->mbox));
+    $this->assertEquals(true, is_string($data->data[0]->data->objectType));
+
+    // Asserts correct property values on data[0]->data.
+    $this->assertEquals('mailto:test@example.com', $data->data[0]->data->mbox);
+    $this->assertEquals('Agent', $data->data[0]->data->objectType);
+  }
+
+  public function testAnalyticsVerb() {
+    $response = $this->requestAnalytics(['type' => 'verb']);
+    $data = $response->getData()->data;
+    $this->assertEquals($data[0]->data->id, 'http://www.example.com/verbs/test');
+  }
+
+  public function testAnalyticsDay() {
+    $response = $this->requestAnalytics(['interval' => 'day']);
+    $data = $response->getData()->data;
+    $this->assertEquals(count($this->statements), count($data));
+  }
+
+  public function testAnalyticsMonth() {
+    $response = $this->requestAnalytics(['interval' => 'month']);
+    $data = $response->getData()->data;
+    $this->assertEquals(count($this->statements), count($data));
+  }
+
+  public function testAnalyticsYear() {
+    $response = $this->requestAnalytics(['interval' => 'year']);
+    $data = $response->getData()->data;
+    $this->assertEquals(count($this->statements), count($data));
+  }
+
+  public function testAnalyticsSince() {
+    $date = date('Y-m-d', strtotime("-1 day"));
+    $response = $this->requestAnalytics(['since' => $date]);
+    $data = $response->getData()->data;
+    $this->assertEquals(count($this->statements), $data[0]->count);
+  }
+
+  public function testAnalyticsEmptySince() {
+    $date = date('Y-m-d', strtotime("+1 day"));
+    $response = $this->requestAnalytics(['since' => $date]);
+    $data = $response->getData()->data;
+    $this->assertEquals(true, empty($data));
+  }
+
+  public function testAnalyticsUntil() {
+    $date = date('Y-m-d', strtotime("+1 day"));
+    $response = $this->requestAnalytics(['until' => $date]);
+    $data = $response->getData()->data;
+    $this->assertEquals(count($this->statements), $data[0]->count);
+  }
+
+  public function testAnalyticsEmptyUntil() {
+    $date = date('Y-m-d', strtotime("-1 day"));
+    $response = $this->requestAnalytics(['until' => $date]);
+    $data = $response->getData()->data;
+    $this->assertEquals(true, empty($data));
+  }
+
+  public function tearDown() {
+    parent::tearDown();
+  }
+}
diff --git a/app/tests/API/ReportsTest.php b/app/tests/routes/ReportsTest.php
similarity index 51%
rename from app/tests/API/ReportsTest.php
rename to app/tests/routes/ReportsTest.php
index da9509a636..862560a43f 100644
--- a/app/tests/API/ReportsTest.php
+++ b/app/tests/routes/ReportsTest.php
@@ -1,4 +1,4 @@
-<?php namespace Tests\API;
+<?php namespace Tests\Routes;
 
 class ReportsTest extends ResourcesTestCase {
   static protected $endpoint = '/api/v1/reports';
@@ -17,21 +17,21 @@ class ReportsTest extends ResourcesTestCase {
   ];
 
   public function testRun() {
-    $response = $this->requestAPI('GET', static::$endpoint.'/'."{$this->model->_id}/run");
+    $response = $this->requestResource('GET', static::$endpoint.'/'."{$this->model->_id}/run");
     $content = $this->getContentFromResponse($response);
 
     // Checks that the response is correct.
-    $this->assertEquals(self::$statements, count($content), 'Incorrect amount of statements.');
-    $this->assertEquals(200, $response->getStatusCode(), 'Incorrect status code.');
+    $this->assertEquals(count($this->statements), count($content));
+    $this->assertEquals(200, $response->getStatusCode());
   }
 
   public function testGraph() {
-    $response = $this->requestAPI('GET', static::$endpoint.'/'."{$this->model->_id}/graph");
+    $response = $this->requestResource('GET', static::$endpoint.'/'."{$this->model->_id}/graph");
     $content = $this->getContentFromResponse($response);
 
     // Checks that the response is correct.
-    $this->assertEquals(true, isset($content[0]['count']), 'Incorrectly formed content.');
-    $this->assertEquals(self::$statements, $content[0]['count'], 'Incorrect amount of statements.');
-    $this->assertEquals(200, $response->getStatusCode(), 'Incorrect status code.');
+    $this->assertEquals(true, isset($content[0]['count']));
+    $this->assertEquals(count($this->statements), $content[0]['count']);
+    $this->assertEquals(200, $response->getStatusCode());
   }
 }
diff --git a/app/tests/API/ResourcesTest.php b/app/tests/routes/ResourcesTestCase.php
similarity index 76%
rename from app/tests/API/ResourcesTest.php
rename to app/tests/routes/ResourcesTestCase.php
index e74d627776..3d398d25e0 100644
--- a/app/tests/API/ResourcesTest.php
+++ b/app/tests/routes/ResourcesTestCase.php
@@ -1,7 +1,10 @@
-<?php namespace Tests\API;
+<?php namespace Tests\Routes;
+use \Tests\StatementsTestCase as StatementsTestCase;
 use \Illuminate\Http\JsonResponse as JsonResponse;
 
-abstract class ResourcesTestCase extends TestCase {
+abstract class ResourcesTestCase extends StatementsTestCase {
+  use RouteTestTrait;
+
   static protected $model_class = '...';
   protected $data = [];
   protected $model = null;
@@ -23,8 +26,13 @@ protected function createModel($data) {
     return $model;
   }
 
+  protected function requestResource($method = 'GET', $uri = '', $content = '') {
+    $params = [];
+    return $this->request($method, $uri, $params, $this->getServer($this->lrs), $content);
+  }
+
   public function testIndex() {
-    $response = $this->requestAPI('GET', static::$endpoint);
+    $response = $this->requestResource('GET', static::$endpoint);
     $content = $this->getContentFromResponse($response);
     $model = $this->getModelFromContent($content[0]);
 
@@ -35,7 +43,7 @@ public function testIndex() {
   }
 
   public function testStore() {
-    $response = $this->requestAPI('POST', static::$endpoint, json_encode($this->data));
+    $response = $this->requestResource('POST', static::$endpoint, json_encode($this->data));
     $model = $this->getModelFromResponse($response);
 
     // Checks that the response is correct.
@@ -45,7 +53,7 @@ public function testStore() {
 
   public function testUpdate() {
     $data = array_merge($this->data, $this->update);
-    $response = $this->requestAPI('PUT', static::$endpoint.'/'.$this->model->_id, json_encode($data));
+    $response = $this->requestResource('PUT', static::$endpoint.'/'.$this->model->_id, json_encode($data));
     $model = $this->getModelFromResponse($response);
 
     // Checks that the response is correct.
@@ -54,7 +62,7 @@ public function testUpdate() {
   }
 
   public function testShow() {
-    $response = $this->requestAPI('GET', static::$endpoint.'/'.$this->model->_id);
+    $response = $this->requestResource('GET', static::$endpoint.'/'.$this->model->_id);
     $model = $this->getModelFromResponse($response);
 
     // Checks that the response is correct.
@@ -63,7 +71,7 @@ public function testShow() {
   }
 
   public function testDestroy() {
-    $response = $this->requestAPI('DELETE', static::$endpoint.'/'.$this->model->_id);
+    $response = $this->requestResource('DELETE', static::$endpoint.'/'.$this->model->_id);
     $content = $this->getContentFromResponse($response);
 
     // Checks that the response is correct.
@@ -87,7 +95,7 @@ protected function getContentFromResponse(JsonResponse $response) {
   }
 
   public function tearDown() {
-    //$this->model->delete();
+    $this->model->delete();
     parent::tearDown();
   }
 }
diff --git a/app/tests/routes/RouteTestTrait.php b/app/tests/routes/RouteTestTrait.php
new file mode 100644
index 0000000000..0bd89961ec
--- /dev/null
+++ b/app/tests/routes/RouteTestTrait.php
@@ -0,0 +1,21 @@
+<?php namespace Tests\Routes;
+
+trait RouteTestTrait {
+
+  protected function getServer(\Lrs $lrs, $version = '1.0.1') {
+    return [
+      'PHP_AUTH_USER' => $lrs->api['basic_key'],
+      'PHP_AUTH_PW' => $lrs->api['basic_secret'],
+      'HTTP_X-Experience-API-Version' => $version
+    ];
+  }
+
+  protected function request($method = 'GET', $uri = '', $params = [], $server = [], $content = null) {
+    $files = [];
+    $changeHistory = true;
+    \Route::enableFilters();
+    
+    // http://laravel.com/api/4.2/Illuminate/Foundation/Testing/TestCase.html#method_call
+    return $this->call($method, $uri, $params, $files, $server, $content, $changeHistory);
+  }
+}
diff --git a/app/tests/StatementRefTest.php b/app/tests/routes/StatementRefTest.php
similarity index 74%
rename from app/tests/StatementRefTest.php
rename to app/tests/routes/StatementRefTest.php
index c8b849dc57..19cfe381a6 100644
--- a/app/tests/StatementRefTest.php
+++ b/app/tests/routes/StatementRefTest.php
@@ -1,36 +1,20 @@
-<?php
+<?php namespace Tests\Routes;
+use \Tests\StatementsTestCase;
 
-class StatementRefTest extends TestCase {
+class StatementRefTest extends StatementsTestCase {
+  use RouteTestTrait;
 
   public function setUp() {
     parent::setUp();
-    Route::enableFilters();
-
-    // Authentication as super user.
-    $user = User::firstOrCreate(['email' => 'test@example.com']);
-    Auth::login($user);
-    $this->createLrs();
-  }
-
-  private function sendStatements($statements) {
-    $auth = [
-      'api_key' => $this->lrs->api['basic_key'],
-      'api_secret' => $this->lrs->api['basic_secret'],
-    ];
-    $headers = $this->makeRequestHeaders($auth);
-    $statements = json_encode($statements);
-    return $this->call('POST', '/data/xAPI/statements', [], [], $headers, $statements);
   }
 
-  protected function generateStatement($statement) {
-    return array_merge($statement, [
-      'actor' => [
-        'mbox' => 'mailto:test@example.com'
-      ],
-      'verb' => [
-        'id' => 'http://www.example.com/verbs/test',
-      ],
-    ]);
+  private function requestStatements($statements) {
+    $content = json_encode($statements);
+    $method = 'POST';
+    $uri = '/data/xAPI/statements';
+    $params = $files = [];
+    $server = $this->getServer($this->lrs);
+    return $this->request($method, $uri, $params, $server, $content);
   }
 
   private function createReferenceStatement($reference_id, $statement = []) {
@@ -77,7 +61,12 @@ private function checkStatement($id, $expected_references = [], $expected_referr
       return $ref['statement']['id'];
     }, $referrers);
 
-    $this->assertEmpty(array_diff($expected_referrers, $referrers));
+    $diff = array_diff($expected_referrers, $referrers);
+    $this->assertEquals(true, empty($diff) || count($diff) === 0,
+      json_encode($diff).
+      json_encode($expected_referrers).
+      json_encode($referrers)
+    );
   }
 
   private function generateUUID($id) {
@@ -87,7 +76,7 @@ private function generateUUID($id) {
   }
 
   public function testInsert1() {
-    $this->sendStatements([
+    $this->requestStatements([
       $this->createIdStatement('A', $this->createReferenceStatement('E'))
     ]);
 
@@ -95,11 +84,11 @@ public function testInsert1() {
   }
 
   public function testInsert2() {
-    $this->sendStatements([
+    $this->requestStatements([
       $this->createIdStatement('A', $this->createReferenceStatement('E'))
     ]);
 
-    $this->sendStatements([
+    $this->requestStatements([
       $this->createIdStatement('C', $this->createReferenceStatement('A')),
       $this->createIdStatement('D', $this->createReferenceStatement('B'))
     ]);
@@ -110,16 +99,16 @@ public function testInsert2() {
   }
 
   public function testInsert3() {
-    $this->sendStatements([
+    $this->requestStatements([
         $this->createIdStatement('A', $this->createReferenceStatement('E'))
     ]);
 
-    $this->sendStatements([
+    $this->requestStatements([
         $this->createIdStatement('C', $this->createReferenceStatement('A')),
         $this->createIdStatement('D', $this->createReferenceStatement('B'))
     ]);
 
-    $this->sendStatements([
+    $this->requestStatements([
       $this->createIdStatement('B', $this->createReferenceStatement('A'))
     ]);
 
@@ -130,20 +119,20 @@ public function testInsert3() {
   }
 
   public function testInsert4() {
-    $this->sendStatements([
+    $this->requestStatements([
         $this->createIdStatement('A', $this->createReferenceStatement('E'))
     ]);
 
-    $this->sendStatements([
+    $this->requestStatements([
         $this->createIdStatement('C', $this->createReferenceStatement('A')),
         $this->createIdStatement('D', $this->createReferenceStatement('B'))
     ]);
 
-    $this->sendStatements([
+    $this->requestStatements([
         $this->createIdStatement('B', $this->createReferenceStatement('A'))
     ]);
 
-    $this->sendStatements([
+    $this->requestStatements([
       $this->createIdStatement('E', $this->createReferenceStatement('D'))
     ]);
 
@@ -156,7 +145,6 @@ public function testInsert4() {
 
   public function tearDown() {
     parent::tearDown();
-    if ($this->lrs)  $this->lrs->delete();
   }
 
 }
diff --git a/app/tests/routes/StatementsTest.php b/app/tests/routes/StatementsTest.php
new file mode 100644
index 0000000000..505b0161fd
--- /dev/null
+++ b/app/tests/routes/StatementsTest.php
@@ -0,0 +1,156 @@
+<?php namespace Tests\Routes;
+use \Tests\StatementsTestCase as StatementsTestCase;
+
+class StatementsTest extends StatementsTestCase {
+  use RouteTestTrait;
+
+  public function setup() {
+    parent::setup();
+  }
+
+  protected function getPipeline() {
+    return '[{"$match":{"active":true}},{"$project":{"_id":0,"statement":1}}]';
+  }
+
+  protected function requestStatements($uri = '', $params = []) {
+    $method = 'GET';
+    $content = null;
+    $uri = '/api/v1/statements/'.$uri;
+    $server = $this->getServer($this->lrs);
+    return $this->request($method, $uri, $params, $server, $content);
+  }
+
+  public function testAggregate() {
+    $response = $this->requestStatements('aggregate', [
+      'pipeline' => $this->getPipeline()
+    ]);
+
+    // Checks that the response is correct.
+    $this->assertEquals(200, $response->getStatusCode());
+    $this->assertEquals(true, method_exists($response, 'getContent'));
+
+    // Checks that the content is correct.
+    $content = json_decode($response->getContent());
+    $this->assertEquals(true, is_object($content));
+    $this->assertEquals(true, isset($content->result));
+    $this->assertEquals(true, is_array($content->result));
+    $this->assertEquals(count($this->statements), count($content->result));
+    $this->assertEquals(true, is_object($content->result[0]));
+    $this->assertEquals(true, isset($content->result[0]->statement));
+    $this->assertEquals(true, is_object($content->result[0]->statement));
+    $this->assertEquals(true, isset($content->result[0]->statement->actor));
+    $this->assertEquals(true, isset($content->ok));
+    $this->assertEquals(true, is_numeric($content->ok));
+    $this->assertEquals(1, $content->ok);
+  }
+
+  public function testAggregateTime() {
+    $response = $this->requestStatements('aggregate/time', [
+      'match' => '{"active": true}'
+    ]);
+
+    // Checks that the response is correct.
+    $this->assertEquals(200, $response->getStatusCode());
+    $this->assertEquals(true, method_exists($response, 'getContent'));
+
+    // Checks that the content is correct.
+    $content = json_decode($response->getContent());
+    $this->assertEquals(true, is_object($content));
+    $this->assertEquals(true, isset($content->result));
+    $this->assertEquals(true, is_array($content->result));
+    $this->assertEquals(1, count($content->result));
+    $this->assertEquals(true, is_object($content->result[0]));
+    $this->assertEquals(true, isset($content->result[0]->count));
+    $this->assertEquals(true, is_numeric($content->result[0]->count));
+    $this->assertEquals(count($this->statements), is_numeric($content->result[0]->count));
+    $this->assertEquals(true, isset($content->result[0]->date));
+    $this->assertEquals(true, is_array($content->result[0]->date));
+    $this->assertEquals(true, isset($content->ok));
+    $this->assertEquals(true, is_numeric($content->ok));
+    $this->assertEquals(1, $content->ok);
+  }
+
+  public function testAggregateObject() {
+    $response = $this->requestStatements('aggregate/object', [
+      'match' => '{"active": true}'
+    ]);
+
+    // Checks that the response is correct.
+    $this->assertEquals(200, $response->getStatusCode());
+    $this->assertEquals(true, method_exists($response, 'getContent'));
+
+    // Checks that the content is correct.
+    $content = json_decode($response->getContent());
+    $this->assertEquals(true, is_object($content));
+    $this->assertEquals(true, isset($content->result));
+    $this->assertEquals(true, is_array($content->result));
+    $this->assertEquals(1, count($content->result));
+    $this->assertEquals(true, is_object($content->result[0]));
+    $this->assertEquals(true, isset($content->result[0]->count));
+    $this->assertEquals(true, is_numeric($content->result[0]->count));
+    $this->assertEquals(count($this->statements), is_numeric($content->result[0]->count));
+    $this->assertEquals(true, isset($content->result[0]->data));
+    $this->assertEquals(true, is_array($content->result[0]->data));
+    $this->assertEquals(count($this->statements), count($content->result[0]->data));
+    $this->assertEquals(true, isset($content->result[0]->data[0]));
+    $this->assertEquals(true, is_object($content->result[0]->data[0]));
+    $this->assertEquals(true, isset($content->result[0]->data[0]->actor));
+    $this->assertEquals(true, is_object($content->result[0]->data[0]->actor));
+    $this->assertEquals(true, isset($content->ok));
+    $this->assertEquals(true, is_numeric($content->ok));
+    $this->assertEquals(1, $content->ok);
+  }
+
+  public function testWhere() {
+    $response = $this->requestStatements('where', [
+      'filter' => '[[{"active", true}]]',
+      'limit' => 1,
+      'page' => 1
+    ]);
+
+    // Checks that the response is correct.
+    $this->assertEquals(200, $response->getStatusCode());
+    $this->assertEquals(true, method_exists($response, 'getContent'));
+
+    // Checks that the content is correct.
+    $content = json_decode($response->getContent());
+    $this->assertEquals(true, is_object($content));
+
+    // Checks set props.
+    $this->assertEquals(true, isset($content->total));
+    $this->assertEquals(true, isset($content->per_page));
+    $this->assertEquals(true, isset($content->current_page));
+    $this->assertEquals(true, isset($content->last_page));
+    $this->assertEquals(true, isset($content->from));
+    $this->assertEquals(true, isset($content->to));
+    $this->assertEquals(true, isset($content->data));
+
+    // Checks prop types.
+    $this->assertEquals(true, is_numeric($content->total));
+    $this->assertEquals(true, is_numeric($content->per_page));
+    $this->assertEquals(true, is_numeric($content->current_page));
+    $this->assertEquals(true, is_numeric($content->last_page));
+    $this->assertEquals(true, is_numeric($content->from));
+    $this->assertEquals(true, is_numeric($content->to));
+    $this->assertEquals(true, is_array($content->data));
+
+    // Checks prop content.
+    $this->assertEquals(count($this->statements), $content->total);
+    $this->assertEquals(1, $content->per_page);
+    $this->assertEquals(1, $content->current_page);
+    $this->assertEquals(count($this->statements), $content->last_page);
+    $this->assertEquals(1, $content->from);
+    $this->assertEquals(1, $content->to);
+    $this->assertEquals(1, count($content->data));
+    $this->assertEquals(true, isset($content->data[0]));
+    $this->assertEquals(true, is_object($content->data[0]));
+    $this->assertEquals(true, isset($content->data[0]->statement));
+    $this->assertEquals(true, is_object($content->data[0]->statement));
+    $this->assertEquals(true, isset($content->data[0]->statement->actor));
+    $this->assertEquals(true, is_object($content->data[0]->statement->actor));
+  }
+
+  public function tearDown() {
+    parent::tearDown();
+  }
+}
diff --git a/app/tests/xAPI/Statement/Validation/BaseStatementValidationTest.php b/app/tests/xAPI/Statement/Validation/BaseStatementValidationTest.php
deleted file mode 100644
index 812e4f979d..0000000000
--- a/app/tests/xAPI/Statement/Validation/BaseStatementValidationTest.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-/**
- * References
- *
- *  - https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md
- *  - http://zackpierce.github.io/xAPI-Validator-JS/
- */
-
-use app\locker\statements\xAPIValidation as xAPIValidation;
-
-abstract class BaseStatementValidationTest extends PHPUnit_Framework_TestCase
-{
-  protected $json_input;
-
-  protected function getFixturePath()
-  {
-    return __DIR__ . '/../../../Fixtures/Statements';
-  }
-
-  protected function exec($path)
-  {
-    $json = file_get_contents($path);
-    $this->json_input = json_decode($json, true);
-    $auth = isset($this->json_input['authority']) ? $this->json_input['authority'] : [
-      'name' => "John Smith",
-      'mbox' => "mailto:test@learninglocker.co.uk",
-      'objectType' => "Agent"
-    ];
-    $manager = new xAPIValidation();
-    return $manager->runValidation($this->json_input, $auth);
-  }
-
-}
diff --git a/app/tests/xAPI/Statement/Validation/StatementValidationActorTest.php b/app/tests/xAPI/Statement/Validation/StatementValidationActorTest.php
deleted file mode 100644
index 6e8049f58d..0000000000
--- a/app/tests/xAPI/Statement/Validation/StatementValidationActorTest.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-require_once __DIR__ . '/BaseStatementValidationTest.php';
-
-class StatementValidationActorTest extends BaseStatementValidationTest
-{
-
-  public function testMissingActor()
-  {
-    $results = $this->exec($this->getFixturePath() . '/Invalid/Actor/missing-actor.json');
-    $this->assertEquals('failed', $results['status']);
-    $this->assertEquals(
-      \Lang::get('xAPIValidation.errors.required', array(
-        'key' => 'actor',
-        'section' => 'core statement'
-      )), trim($results['errors'][0])
-    );
-  }
-
-  public function testGroupMissingMember()
-  {
-    $results = $this->exec($this->getFixturePath() . '/Invalid/Actor/Group/missing-member.json');
-    $this->assertEquals('failed', $results['status']);
-    $this->assertEquals(\Lang::get('xAPIValidation.errors.required', array(
-      'key' => 'member',
-      'section' => 'actor'
-    )), trim($results['errors'][0]));
-  }
-
-  public function testGroupMemberObjectTypeIsNotAgent()
-  {
-    $results = $this->exec($this->getFixturePath() . '/Invalid/Actor/Group/Member/object-type-is-not-agent.json');
-    $this->assertEquals('failed', $results['status']);
-    $this->assertEquals(\Lang::get('xAPIValidation.errors.group.groups'), trim($results['errors'][0]));
-  }
-
-  public function testMbox()
-  {
-    $results = $this->exec($this->getFixturePath() . '/Invalid/Actor/Mbox/invalid-format.json');
-    $this->assertEquals('failed', $results['status']);
-    $this->assertEquals(
-      \Lang::get('xAPIValidation.errors.format', array(
-        'key' => 'mbox',
-        'section' => 'actor'
-      )), trim($results['errors'][0]));
-  }
-
-}
diff --git a/app/tests/xAPI/Statement/Validation/StatementValidationAttachmentTest.php b/app/tests/xAPI/Statement/Validation/StatementValidationAttachmentTest.php
deleted file mode 100644
index e29664ec5e..0000000000
--- a/app/tests/xAPI/Statement/Validation/StatementValidationAttachmentTest.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<?php
-
-require_once __DIR__ . '/BaseStatementValidationTest.php';
-
-class StatementValidationAttachmentTest extends BaseStatementValidationTest
-{
-
-  public function testContentType()
-  {
-    $results = $this->exec($this->getFixturePath() . '/Invalid/Attachment/content-type-is-not-string.json');
-    $this->assertEquals('failed', $results['status']);
-    $this->assertEquals(
-      \Lang::get('xAPIValidation.errors.type', array(
-        'key' => 'contentType',
-        'section' => 'attachment',
-        'type' => 'Internet Media Type'
-      )), trim($results['errors'][0])
-    );
-  }
-
-  public function testLength()
-  {
-    $results = $this->exec($this->getFixturePath() . '/Invalid/Attachment/length-is-not-integer.json');
-    $this->assertEquals('failed', $results['status']);
-    $this->assertEquals(
-      \Lang::get('xAPIValidation.errors.type', array(
-        'key' => 'length',
-        'section' => 'attachment',
-        'type' => 'number'
-      )), trim($results['errors'][0])
-    );
-  }
-
-  public function testSha2Required()
-  {
-    $results = $this->exec($this->getFixturePath() . '/Invalid/Attachment/missing-sha2.json');
-    $this->assertEquals('failed', $results['status']);
-    $this->assertEquals(
-      \Lang::get('xAPIValidation.errors.required', array(
-        'key' => 'sha2',
-        'section' => 'attachment'
-      )), trim($results['errors'][0])
-    );
-  }
-
-  public function testDisplayValid()
-  {
-    $results = $this->exec($this->getFixturePath() . '/Invalid/Attachment/display.json');
-    $this->assertEquals('failed', $results['status']);
-    $this->assertEquals(\Lang::get('xAPIValidation.errors.langMap', array(
-        'key' => 'display',
-        'section' => 'attachment'
-      )), trim($results['errors'][0])
-    );
-  }
-
-  public function testSha2Valid()
-  {
-    $results = $this->exec($this->getFixturePath() . '/Invalid/Attachment/sha2-is-not-valid.json');
-    $this->assertEquals('failed', $results['status']);
-    $this->assertEquals(\Lang::get('xAPIValidation.errors.base64', array(
-        'key' => 'sha2',
-        'section' => 'attachment'
-      )), trim($results['errors'][0])
-    );
-  }
-
-  public function testUsageType()
-  {
-    $results = $this->exec($this->getFixturePath() . '/Invalid/Attachment/missing-usage-type.json');
-    $this->assertEquals('failed', $results['status']);
-    $this->assertEquals(
-      \Lang::get('xAPIValidation.errors.required', array(
-        'key' => 'usageType',
-        'section' => 'attachment'
-      )), trim($results['errors'][0])
-    );
-  }
-
-}
diff --git a/app/tests/xAPI/Statement/Validation/StatementValidationAuthorityTest.php b/app/tests/xAPI/Statement/Validation/StatementValidationAuthorityTest.php
deleted file mode 100644
index 0a6a53364c..0000000000
--- a/app/tests/xAPI/Statement/Validation/StatementValidationAuthorityTest.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-require_once __DIR__ . '/BaseStatementValidationTest.php';
-
-class StatementValidationAuthorityTest extends BaseStatementValidationTest
-{
-
-  public function testAuthority()
-  {
-    $results = $this->exec($this->getFixturePath() . '/Invalid/Authority/Member/wrong-object-type.json');
-    $this->assertEquals('failed', $results['status']);
-    $this->assertEquals(\Lang::get('xAPIValidation.errors.group.groups'), trim($results['errors'][0]));
-  }
-
-}
diff --git a/app/tests/xAPI/Statement/Validation/StatementValidationObjectTest.php b/app/tests/xAPI/Statement/Validation/StatementValidationObjectTest.php
deleted file mode 100644
index 25aa9e01ae..0000000000
--- a/app/tests/xAPI/Statement/Validation/StatementValidationObjectTest.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-/**
- * @todo An LRS MUST NOT take action in the event it perceives an activity id is
- *          being used by multiple authors and/or organizations.
- */
diff --git a/app/tests/xAPI/Statement/Validation/StatementValidationTest.php b/app/tests/xAPI/Statement/Validation/StatementValidationTest.php
deleted file mode 100644
index b83160532c..0000000000
--- a/app/tests/xAPI/Statement/Validation/StatementValidationTest.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-require_once __DIR__ . '/BaseStatementValidationTest.php';
-
-class StatementValidationTest extends BaseStatementValidationTest
-{
-
-  /**
-   * @dataProvider dataProviderSimple
-   */
-  public function testSimple($path)
-  {
-    $results = $this->exec($path);
-    $this->assertEquals('passed', $results['status']);
-    $this->assertEmpty($results['errors']);
-
-    $short_path = substr($path, strpos($path, '/Fixtures/Statements/Valid/') + 27, -5);
-    $extra_method = str_replace(['//', '/', '-'], ' ', trim($short_path, '/'));
-    $extra_method = 'extraChecking' . str_replace(' ', '', ucwords($extra_method));
-    if (method_exists($this, $extra_method)) {
-      $this->{$extra_method}($results);
-    }
-  }
-
-  protected function extraCheckingActorGroup($results)
-  {
-    $this->assertEquals($this->json_input['actor']['member'], $results['statement']['actor']['member']);
-  }
-
-  public function dataProviderSimple()
-  {
-    $data = [];
-
-    foreach (['', 'Actor', 'Object', 'Verb/Display'] as $k) {
-      foreach (glob($this->getFixturePath() . "/Valid/{$k}/*.json") as $file) {
-        $data[][] = $file;
-      }
-    }
-
-    return $data;
-  }
-
-}