|
Within the API there are two layers: the delivery and the business logic (core application). The delivery layer follows a Model View Controller (MVC) pattern, with the View consisting of JSON output. The Controllers use a Service Locator to load and execute various tools, taking the API request inputs and returning the requested resources.
Within the core application, we use generally follow the Clean Architecture. The central part of the business logic is defined as use cases and entities. All dependencies flow inwards towards the entities, which have no dependencies. In order to bring user input to the use cases, we create a Data Transfer Object (DTO) and pass this object from the delivery layer into the use case. The DTO is a very simple object which is defined by the use case and contains all of the possible inputs for that specific use case. In order to create the DTO, we use a Parser object. The DTO is then checked for consistency by using a Validator. Once validated, the use case will complete its execution and return the resulting entities back to the delivery layer for conversion via a Formatter. (For a longer overview of clean architecture and how it is implemented, please read Ushahidi Platform: Under the Hood, Part 1). Data flow within the platform can be visualized as:
Note that this diagram is not ideal and will be changing before the final version of the platform. Specifically, data is flowing backwards to the controller and should be flowing directly into the formatter. We will be addressing this in the near future. |
Modeling within Ushahidi is primarily handled at the core application level with entities. Each entity also defines a read-only repository interface for the database access layer. The writable repository interfaces are defined by each use case that requires storage. The readable and writable interfaces are implemented within the delivery layer, to keep the core application free of storage details. Typically, the implementation of multiple related interfaces will be done by a single object. For instance, the user storage object will implement the UserRepository
to read users, as well as the UserRegisterRepository
, UserLoginRepository
, UserUpdateRepository
, etc. Repository read operations will always return an entity, or collection of entities. Writing operations can have various output, depending on the situation.
Verification of incoming data for writing operations is first parsed into a DTO, which is then validated by the use case.
Formatting of entities into consumable resources is done at the delivery layer.
Each request to the API is routed to an API controller, and action. The controller code processes any user input and returns generates a JSON response. The controller actions are mapped based on the type of HTTP request (GET, POST, PUT, DELETE, etc):
HTTP | Description | Action |
---|---|---|
GET /foo | get collection of "foo" entities | action_get_index_collection |
GET /foo/:id | get "foo" entity of "id" | action_get_index |
POST /foo | create a new "foo" entity | action_post_index_collection |
PUT /foo/:id | update "foo" entity of "id" | action_put_index |
DELETE /foo/:id | delete "foo" entity of "id" | action_delete_index |
Every controller that responds to API requests will follow this pattern.
All API controllers are built off a base controller - application/classes/Ushahidi/Api.php
- which handles a large amount of the repeated code in the API. The base controller takes care of:
The frontend for Ushahidi 3.x is a javascript application (in the browser) built on BackboneJS (& Marionette JS). All data for the application is loaded from the API. The javascript itself is broken down into many 'modules' which are loaded by RequireJS.
The javascript application lives in modules/UshahidiUI/media/js. The structure here is:
lib/ - 3rd party libraries (jquery, backbone, etc)
app/ - All custom application modules
collections/ - All collection classes
controllers/ - All controllers, currently only a single controller in Controller.js
routers/ - All routes, currently only a single router in AppRouter.js
views/ - All view classes
templates/ - Handlebarsjs templates used by the views
util/ - small utility modules
App.js
config/Init.js
tests/ - Frontend test (not yet built out)
Init.js is first file loaded by requireJS. It sets up some initial RequireJS config (paths, and shims for non AMD modules), requires other App files and starts the App. It's simple enough to include most of its code below
// Cut down excerpt from Init.js require.config( { baseUrl : "./media/kohana/js/app", paths : { // Set paths to libraries }, shim : { // Shim non AMD modules } } require(["App", "routers/AppRouter", "controllers/Controller", "jquery", "jqueryui", "backbone.validateAll"], function(App, AppRouter, Controller) { App.appRouter = new AppRouter( { controller : new Controller() }); App.start(); window.App = App; }); |
This is the module for the main application object.. there's not actually a lot that goes on here: we create the object, add regions, save our config and oauth objects for later, and add an 'Initializer' callback. The initializer callback is fired when the application starts, and all it does is start Backbone.history..
Once the App class has started, most control is handed over to the router and controller class. The router maps a url to a controller and action, at the moment these mappings are all fairly simple.
// AppRouter.js define(['marionette', 'controllers/Controller'], function(Marionette, Controller) { return Marionette.AppRouter.extend( { appRoutes : { "" : "index", "views/list" : "viewsList", "views/map" : "viewsMap", "posts/:id" : "postDetail", "*path" : "index" } }); }); |
Each route maps directly to a function in the controller. The controller handles switching layouts and regions, creating views and binding models or collections to these.
I've shown part of the Controller.js file below. The 'initialize' function is run when the App first starts. It creates an 'AppLayout' - this is kind of a special view with several regions
// Controller.js Backbone.Marionette.Controller.extend( { initialize : function(options) { this.layout = new AppLayout(); App.body.show(this.layout); var header = new HeaderView(); header.on('workspace:toggle', function () { App.body.$el.toggleClass('active-workspace') }); this.layout.headerRegion.show(header); this.layout.footerRegion.show(new FooterView()); this.layout.workspacePanel.show(new WorkspacePanelView()); App.Collections = {}; App.Collections.Posts = new PostCollection(); App.Collections.Posts.fetch(); App.Collections.Tags = new TagCollection(); App.Collections.Tags.fetch(); App.Collections.Forms = new FormCollection(); App.Collections.Forms.fetch(); App.homeLayout = new HomeLayout(); }, //gets mapped to in AppRouter's appRoutes index : function() { App.vent.trigger("page:change", "index"); this.layout.mainRegion.show(App.homeLayout); App.homeLayout.contentRegion.show(new PostListView({ collection: App.Collections.Posts })); App.homeLayout.mapRegion.show(new MapView()); App.homeLayout.searchRegion.show(new SearchBarView()); }, // etc }); |
All the HTML in the Ushahidi 3.x UI is built using backbone views and handlebars templates. We use MarionetteJS Layouts and Regions to combined many nested views. The base views and regions are managed through AppLayout and and HomeLayout. A region is basically a wrapper around a particular DOM element to make it easy to show/hide a view inside that DOM element. A layout is basically a view with several regions bound to it, ie. we use a view to render some HTML then bind several regions to parts of the view's HTML output.
The AppLayout is the top level view for the application. It populated the <body> tag with HTML and create the header, main, footer, workspace and modal regions. The header, footer and workspace regions mostly keep the same views. The modal region is used for modal popups like create or edit post. There are a few different views/layouts we swap in and out of the main region: HomeLayout, PostDetailLayout, SetListView, etc.
One thing you will notice if you dig into the code, is that the docroot (httpdocs/) actually only contains a small number of files, and all requests are passed to index.php. The frontend (JS/CSS/HTML/etc) is still served up by the same Kohana application as the API. The frontend actually lives in a Kohana module: UshahidiUI. This handles a couple of things:
This controller handles requests for '/' - the main page of a deployment. This controller does very little real work: its builds an array of config data, and renders a single view - 'modules/UshahidiUI/views/index.php'.
abstract class Ushahidi_Controller_Main extends Controller_Template { public $template = 'index'; public function action_index() { $this->template->site = array(); $this->template->site['baseurl'] = Kohana::$base_url; $this->template->site['imagedir'] = Media::uri('/images/'); $this->template->site['cssdir'] = Media::uri('/css/'); $this->template->site['jsdir'] = Media::uri('/js/'); $this->template->site['oauth'] = Kohana::$config->load('ushahidiui.oauth'); } } |
This view generates the starting HTML for our application, including tags to load JS/CSS files and turning that configuration array in a json object.
After the browser loads this view, everything is hanled by the javascript application, and the API.
OAuth - handled as part of the API, mostly abstracted to a module, etc