On the server side we need to register a callback that is executed once the request comes in. The callback itself will be a method on a controller and the controller will be connected to the URL with a route. The controller and route for the page are already set up in ownnotes/appinfo/routes.php:

This route calls the controller OCA\OwnNotes\PageController->index() method which is defined in ownnotes/controller/pagecontroller.php. The controller returns a template, in this case ownnotes/templates/main.php:

Note

@NoAdminRequired and @NoCSRFRequired in the comments above the method turn off security checks, see Controllers

Since the route which returns the intial HTML has been taken care of, the controller which handles the AJAX requests for the notes needs to be set up. Create the following file: ownnotes/controller/notecontroller.php with the following content:

The parameters are extracted from the request body and the url using the controller method’s variable names. Since PHP does not support type hints for primitive types such as ints and booleans, we need to add them as annotations in the comments. In order to type cast a parameter to an int, add @param int $parameterName

Now the controller methods need to be connected to the corresponding URLs in the ownnotes/appinfo/routes.php file:

The first parent constructor parameter is the database layer, the second one is the database table and the third is the entity on which the result should be mapped onto. Insert, delete and update methods are already implemented.

The mapper which provides the database access is finished and can be passed into the controller.

You can pass in the mapper by adding it as a type hinted parameter. ownCloud will figure out how to assemble them by itself. Additionally we want to know the userId of the currently logged in user. Simply add a $UserId parameter to the constructor (case sensitive!). To do that open ownnotes/controller/notecontroller.php and change it to the following:

The actual exceptions are OCP\AppFramework\Db\DoesNotExistException and OCP\AppFramework\Db\MultipleObjectsReturnedException but in this example we will treat them as the same. DataResponse is a more generic response than JSONResponse and also works with JSON.

This is all that is needed on the server side. Now let’s progress to the client side.

Let’s say our app is now on the app store and and we get a request that we should save the files in the filesystem which requires access to the filesystem.

The filesystem API is quite different from the database API and throws different exceptions, which means we need to rewrite everything in the NoteController class to use it. This is bad because a controller’s only responsibility should be to deal with incoming Http requests and return Http responses. If we need to change the controller because the data storage was changed the code is probably too tightly coupled and we need to add another layer in between. This layer is called Service.

Let’s take the logic that was inside the controller and put it into a separate class inside ownnotes/service/noteservice.php:

<?phpnamespaceOCA\OwnNotes\Service;useException;useOCP\AppFramework\Db\DoesNotExistException;useOCP\AppFramework\Db\MultipleObjectsReturnedException;useOCA\OwnNotes\Db\Note;useOCA\OwnNotes\Db\NoteMapper;classNoteService{private$mapper;publicfunction__construct(NoteMapper$mapper){$this->mapper=$mapper;}publicfunctionfindAll($userId){return$this->mapper->findAll($userId);}privatefunctionhandleException($e){if($einstanceofDoesNotExistException||$einstanceofMultipleObjectsReturnedException){thrownewNotFoundException($e->getMessage());}else{throw$e;}}publicfunctionfind($id,$userId){try{return$this->mapper->find($id,$userId);// in order to be able to plug in different storage backends like files// for instance it is a good idea to turn storage related exceptions// into service related exceptions so controllers and service users// have to deal with only one type of exception}catch(Exception$e){$this->handleException($e);}}publicfunctioncreate($title,$content,$userId){$note=newNote();$note->setTitle($title);$note->setContent($content);$note->setUserId($userId);return$this->mapper->insert($note);}publicfunctionupdate($id,$title,$content,$userId){try{$note=$this->mapper->find($id,$userId);$note->setTitle($title);$note->setContent($content);return$this->mapper->update($note);}catch(Exception$e){$this->handleException($e);}}publicfunctiondelete($id,$userId){try{$note=$this->mapper->find($id,$userId);$this->mapper->delete($note);return$note;}catch(Exception$e){$this->handleException($e);}}}

Following up create the exceptions in ownnotes/service/serviceexception.php:

Remember how we had all those ugly try catches that where checking for DoesNotExistException and simply returned a 404 response? Let’s also put this into a reusable class. In our case we chose a trait so we can inherit methods without having to add it to our inheritance hierarchy. This will be important later on when you’ve got controllers that inherit from the ApiController class instead.

Tests are essential for having happy users and a carefree life. No one wants their users to rant about your app breaking their ownCloud or being buggy. To do that you need to test your app. Since this amounts to a ton of repetitive tasks, we need to automate the tests.

A unit test is a test that tests a class in isolation. It is very fast and catches most of the bugs, so we want many unit tests.

Because ownCloud uses Dependency Injection to assemble your app, it is very easy to write unit tests by passing mocks into the constructor. A simple test for the update method can be added by adding this to ownnotes/tests/unit/controller/NoteControllerTest.php:

<?phpnamespaceOCA\OwnNotes\Controller;usePHPUnit_Framework_TestCase;useOCP\AppFramework\Http;useOCP\AppFramework\Http\DataResponse;useOCA\OwnNotes\Service\NotFoundException;classNoteControllerTestextendsPHPUnit_Framework_TestCase{protected$controller;protected$service;protected$userId='john';protected$request;publicfunctionsetUp(){$this->request=$this->getMockBuilder('OCP\IRequest')->getMock();$this->service=$this->getMockBuilder('OCA\OwnNotes\Service\NoteService')->disableOriginalConstructor()->getMock();$this->controller=newNoteController('ownnotes',$this->request,$this->service,$this->userId);}publicfunctiontestUpdate(){$note='just check if this value is returned correctly';$this->service->expects($this->once())->method('update')->with($this->equalTo(3),$this->equalTo('title'),$this->equalTo('content'),$this->equalTo($this->userId))->will($this->returnValue($note));$result=$this->controller->update(3,'title','content');$this->assertEquals($note,$result->getData());}publicfunctiontestUpdateNotFound(){// test the correct status code if no note is found$this->service->expects($this->once())->method('update')->will($this->throwException(newNotFoundException()));$result=$this->controller->update(3,'title','content');$this->assertEquals(Http::STATUS_NOT_FOUND,$result->getStatus());}}

We can and should also create a test for the NoteService class:

<?phpnamespaceOCA\OwnNotes\Service;usePHPUnit_Framework_TestCase;useOCP\AppFramework\Db\DoesNotExistException;useOCA\OwnNotes\Db\Note;classNoteServiceTestextendsPHPUnit_Framework_TestCase{private$service;private$mapper;private$userId='john';publicfunctionsetUp(){$this->mapper=$this->getMockBuilder('OCA\OwnNotes\Db\NoteMapper')->disableOriginalConstructor()->getMock();$this->service=newNoteService($this->mapper);}publicfunctiontestUpdate(){// the existing note$note=Note::fromRow(['id'=>3,'title'=>'yo','content'=>'nope']);$this->mapper->expects($this->once())->method('find')->with($this->equalTo(3))->will($this->returnValue($note));// the note when updated$updatedNote=Note::fromRow(['id'=>3]);$updatedNote->setTitle('title');$updatedNote->setContent('content');$this->mapper->expects($this->once())->method('update')->with($this->equalTo($updatedNote))->will($this->returnValue($updatedNote));$result=$this->service->update(3,'title','content',$this->userId);$this->assertEquals($updatedNote,$result);}/** * @expectedException OCA\OwnNotes\Service\NotFoundException */publicfunctiontestUpdateNotFound(){// test the correct status code if no note is found$this->mapper->expects($this->once())->method('find')->with($this->equalTo(3))->will($this->throwException(newDoesNotExistException('')));$this->service->update(3,'title','content',$this->userId);}}

You need to adjust the ownnotes/tests/unit/controller/PageControllerTest file to get the tests passing: remove the testEcho method since that method is no longer present in your PageController and do not test the user id parameters since they are not passed anymore

Integration tests are slow and need a fully working instance but make sure that our classes work well together. Instead of mocking out all classes and parameters we can decide whether to use full instances or replace certain classes. Because they are slow we don’t want as many integration tests as unit tests.

In our case we want to create an integration test for the udpate method without mocking out the NoteMapper class so we actually write to the existing database.

To do that create a new file called ownnotes/tests/integration/NoteIntegrationTest.php with the following content:

<?phpnamespaceOCA\OwnNotes\Controller;useOCP\AppFramework\Http\DataResponse;useOCP\AppFramework\App;useTest\TestCase;useOCA\OwnNotes\Db\Note;classNoteIntregrationTestextendsTestCase{private$controller;private$mapper;private$userId='john';publicfunctionsetUp(){parent::setUp();$app=newApp('ownnotes');$container=$app->getContainer();// only replace the user id$container->registerService('UserId',function($c){return$this->userId;});$this->controller=$container->query('OCA\OwnNotes\Controller\NoteController');$this->mapper=$container->query('OCA\OwnNotes\Db\NoteMapper');}publicfunctiontestUpdate(){// create a new note that should be updated$note=newNote();$note->setTitle('old_title');$note->setContent('old_content');$note->setUserId($this->userId);$id=$this->mapper->insert($note)->getId();// fromRow does not set the fields as updated$updatedNote=Note::fromRow(['id'=>$id,'user_id'=>$this->userId]);$updatedNote->setContent('content');$updatedNote->setTitle('title');$result=$this->controller->update($id,'title','content');$this->assertEquals($updatedNote,$result->getData());// clean up$this->mapper->delete($result->getData());}}

To run the integration tests change into the ownnotes directory and run:

A RESTful API allows other apps such as Android or iPhone apps to access and change your notes. Since syncing is a big core component of ownCloud it is a good idea to add (and document!) your own RESTful API.

Because we put our logic into the NoteService class it is very easy to reuse it. The only pieces that need to be changed are the annotations which disable the CSRF check (not needed for a REST call usually) and add support for CORS so your API can be accessed from other webapps.

With that in mind create a new controller in ownnotes/controller/noteapicontroller.php:

Since the NoteApiController is basically identical to the NoteController, the unit test for it simply inherits its tests from the NoteControllerTest. Create the file ownnotes/tests/unit/controller/NoteApiControllerTest.php:

To create a modern webapp you need to write JavaScript. You can use any JavaScript framework but for this tutorial we want to keep it as simple as possible and therefore only include the templating library handlebarsjs. Download the file into ownnotes/js/handlebars.js and include it at the very top of ownnotes/templates/main.php before the other scripts and styles:

The template file ownnotes/templates/part.navigation.php contains the navigation. ownCloud defines many handy CSS styles which we are going to reuse to style the navigation. Adjust the file to contain only the following code:

When the page is loaded we want all the existing notes to load. Furthermore we want to display the current note when you click on it in the navigation, a note should be deleted when we click the deleted button and clicking on New note should create a new note. To do that open ownnotes/js/script.js and replace the example code with the following:

(function(OC,window,$,undefined){'use strict';$(document).ready(function(){vartranslations={newNote:$('#new-note-string').text()};// this notes object holds all our notesvarNotes=function(baseUrl){this._baseUrl=baseUrl;this._notes=[];this._activeNote=undefined;};Notes.prototype={load:function(id){varself=this;this._notes.forEach(function(note){if(note.id===id){note.active=true;self._activeNote=note;}else{note.active=false;}});},getActive:function(){returnthis._activeNote;},removeActive:function(){varindex;vardeferred=$.Deferred();varid=this._activeNote.id;this._notes.forEach(function(note,counter){if(note.id===id){index=counter;}});if(index!==undefined){// delete cached active note if necessaryif(this._activeNote===this._notes[index]){deletethis._activeNote;}this._notes.splice(index,1);$.ajax({url:this._baseUrl+'/'+id,method:'DELETE'}).done(function(){deferred.resolve();}).fail(function(){deferred.reject();});}else{deferred.reject();}returndeferred.promise();},create:function(note){vardeferred=$.Deferred();varself=this;$.ajax({url:this._baseUrl,method:'POST',contentType:'application/json',data:JSON.stringify(note)}).done(function(note){self._notes.push(note);self._activeNote=note;self.load(note.id);deferred.resolve();}).fail(function(){deferred.reject();});returndeferred.promise();},getAll:function(){returnthis._notes;},loadAll:function(){vardeferred=$.Deferred();varself=this;$.get(this._baseUrl).done(function(notes){self._activeNote=undefined;self._notes=notes;deferred.resolve();}).fail(function(){deferred.reject();});returndeferred.promise();},updateActive:function(title,content){varnote=this.getActive();note.title=title;note.content=content;return$.ajax({url:this._baseUrl+'/'+note.id,method:'PUT',contentType:'application/json',data:JSON.stringify(note)});}};// this will be the view that is used to update the htmlvarView=function(notes){this._notes=notes;};View.prototype={renderContent:function(){varsource=$('#content-tpl').html();vartemplate=Handlebars.compile(source);varhtml=template({note:this._notes.getActive()});$('#editor').html(html);// handle savesvartextarea=$('#app-content textarea');varself=this;$('#app-content button').click(function(){varcontent=textarea.val();vartitle=content.split('\n')[0];// first line is the titleself._notes.updateActive(title,content).done(function(){self.render();}).fail(function(){alert('Could not update note, not found');});});},renderNavigation:function(){varsource=$('#navigation-tpl').html();vartemplate=Handlebars.compile(source);varhtml=template({notes:this._notes.getAll()});$('#app-navigation ul').html(html);// create a new notevarself=this;$('#new-note').click(function(){varnote={title:translations.newNote,content:''};self._notes.create(note).done(function(){self.render();$('#editor textarea').focus();}).fail(function(){alert('Could not create note');});});// show app menu$('#app-navigation .app-navigation-entry-utils-menu-button').click(function(){varentry=$(this).closest('.note');entry.find('.app-navigation-entry-menu').toggleClass('open');});// delete a note$('#app-navigation .note .delete').click(function(){varentry=$(this).closest('.note');entry.find('.app-navigation-entry-menu').removeClass('open');self._notes.removeActive().done(function(){self.render();}).fail(function(){alert('Could not delete note, not found');});});// load a note$('#app-navigation .note > a').click(function(){varid=parseInt($(this).parent().data('id'),10);self._notes.load(id);self.render();$('#editor textarea').focus();});},render:function(){this.renderNavigation();this.renderContent();}};varnotes=newNotes(OC.generateUrl('/apps/ownnotes/notes'));varview=newView(notes);notes.loadAll().done(function(){view.render();}).fail(function(){alert('Could not load notes');});});})(OC,window,jQuery);