Database Connectivity

The Database Schema

Now that the application’s routes and two controllers have been setup and wired together, we’ll flesh out NotesController so that the notes can be saved in the database. But to do that, we first need to create the database schema by creating ownnotes/appinfo/database.xml, with the following content:

<database>
    <name>*dbname*</name>
    <create>true</create>
    <overwrite>false</overwrite>
    <charset>utf8</charset>
    <table>
        <name>*dbprefix*ownnotes_notes</name>
        <declaration>
            <field>
                <name>id</name>
                <type>integer</type>
                <notnull>true</notnull>
                <autoincrement>true</autoincrement>
                <unsigned>true</unsigned>
                <primary>true</primary>
                <length>8</length>
            </field>
            <field>
                <name>title</name>
                <type>text</type>
                <length>200</length>
                <default></default>
                <notnull>true</notnull>
            </field>
            <field>
                <name>user_id</name>
                <type>text</type>
                <length>200</length>
                <default></default>
                <notnull>true</notnull>
            </field>
            <field>
                <name>content</name>
                <type>clob</type>
                <default></default>
                <notnull>true</notnull>
            </field>
        </declaration>
    </table>
</database>

The schema consists of one table: ownnotes_notes, which has four fields:

  • id: An integer
  • title: A text field
  • user_id: A text field
  • content: A CLOB field

With the file created, the version tag in ownnotes/appinfo/info.xml needs to be increased. This causes ownCloud to trigger the update process when you next load (or reload) the ownCloud UI. Part of the update process includes run database migrations, which will create the database table defined in the migration above.

Data Entities

Now that the tables are created, we want to map the database search results to a PHP object. That way, we’re able to manage the data more precisely. To do that, create an entity in new file, called: ownnotes/lib/Db/Note.php:

<?php
namespace OCA\OwnNotes\Db;

use JsonSerializable;
use OCP\AppFramework\Db\Entity;

class Note extends Entity implements JsonSerializable {

    protected $title;
    protected $content;
    protected $userId;

    public function jsonSerialize() {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'content' => $this->content
        ];
    }

}

Note

The id field exists in the Entity

We also define a jsonSerializable method and implement the interface, so that we’re able to transform the entity to JSON, making it easy to persist and cache the information.

Data Mappers

Entities are returned from so-called data mappers. Data mappers are:

A layer of Mappers (473) that moves data between objects and a database while keeping them independent of each other and the mapper itself.

Let’s create one in ownnotes/lib/Db/NoteMapper.php and add a find and findAll method:

<?php
namespace OCA\OwnNotes\Db;

use OCP\IDb;
use OCP\AppFramework\Db\Mapper;

class NoteMapper extends Mapper {

    public function __construct(IDb $db) {
        parent::__construct($db, 'ownnotes_notes', '\OCA\OwnNotes\Db\Note');
    }

    public function find($id, $userId) {
        $sql = 'SELECT * FROM *PREFIX*ownnotes_notes WHERE id = ? AND user_id = ?';
        return $this->findEntity($sql, [$id, $userId]);
    }

    public function findAll($userId) {
        $sql = 'SELECT * FROM *PREFIX*ownnotes_notes WHERE user_id = ?';
        return $this->findEntities($sql, [$userId]);
    }

}

Note

The first parent constructor parameter is the database connection object (or database handle), the second one is the database table and the third is the entity which the result should be mapped onto. Insert, delete and update methods are already implemented.

Connecting Databases & Controllers

Now the mapper is finished and can be passed into the controller. You can do so 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. To do so, add a $UserId parameter to the constructor, which is case-sensitive. Open ownnotes/lib/Controller/NoteController.php and change it to the following:

<?php
 namespace OCA\OwnNotes\Controller;

 use Exception;

 use OCP\IRequest;
 use OCP\AppFramework\Http;
 use OCP\AppFramework\Http\DataResponse;
 use OCP\AppFramework\Controller;

 use OCA\OwnNotes\Db\Note;
 use OCA\OwnNotes\Db\NoteMapper;

 class NoteController extends Controller {

     private $mapper;
     private $userId;

     public function __construct($AppName, IRequest $request, NoteMapper $mapper, $UserId){
         parent::__construct($AppName, $request);
         $this->mapper = $mapper;
         $this->userId = $UserId;
     }

 }

With the constructor defined, we now need to flesh out the rest of the methods, which we previously didn’t define bodies for. In index, below, we’ll return a DataResponse object, which contains the result of using the Data Mapper’s findAll method.

This method, which is supplied with the current user’s id, retrieves all notes created by that user. A DataResponse object is used to return generic data responses. It provides a more generic response than JSONResponse, which also works with JSON data.

/**
 * @NoAdminRequired
 */
public function index() {
    return new DataResponse($this->mapper->findAll($this->userId));
}

Next, we’ll flesh out the show function. This function will retrieve and return the details for a specific note. It does so by using the data mapper’s find method, which is supplied with the note’s and user’s ids. If the note cannot be retrieved, then a DataResponse is returned, which results in a 404 Not Found response.

/**
 * @NoAdminRequired
 *
 * @param int $id
 */
public function show($id) {
    try {
        return new DataResponse($this->mapper->find($id, $this->userId));
    } catch(Exception $e) {
        return new DataResponse([], Http::STATUS_NOT_FOUND);
    }
}

Next, we’ll flesh out the create method, so that we can create notes. This method receives the note’s title and content from the route and sets them, along with the current user’s id, on a new Note entity object. The function returns the result of calling the data mapper’s insert method, which attempts to persist the Note entity in the database.

/**
 * @NoAdminRequired
 *
 * @param string $title
 * @param string $content
 */
public function create($title, $content) {
    $note = new Note();
    $note->setTitle($title);
    $note->setContent($content);
    $note->setUserId($this->userId);

    return new DataResponse($this->mapper->insert($note));
}

Next we’ll flesh out the update function, which updates an existing note. Similar to the create method, it receives the note’s id, title, and content from the route. It then attempts to retrieve the note, and throws an exception if it’s unable to do so. If it can retrieve it, it then updates the title and content, and returns the response from calling the data mapper’s update function.

/**
 * @NoAdminRequired
 *
 * @param int $id
 * @param string $title
 * @param string $content
 */
public function update($id, $title, $content) {
    try {
        $note = $this->mapper->find($id, $this->userId);
    } catch(Exception $e) {
        return new DataResponse([], Http::STATUS_NOT_FOUND);
    }
    $note->setTitle($title);
    $note->setContent($content);
    return new DataResponse($this->mapper->update($note));
}

Finally, we’ll flesh out the destroy function, which deletes an existing note. This, like update, will first attempt to retrieve a note, based on the supplied id, and throw an exception if it’s not able to be found. If it’s able to be found, it will then be passed to the data mapper’s delete function, which will delete the note from the database.

/**
 * @NoAdminRequired
 *
 * @param int $id
 */
public function destroy($id) {
    try {
        $note = $this->mapper->find($id, $this->userId);
    } catch(Exception $e) {
        return new DataResponse([], Http::STATUS_NOT_FOUND);
    }
    $this->mapper->delete($note);
    return new DataResponse($note);
}

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

Decoupling Controllers and Increasing Reusability

Let’s now say that our app is now on the ownCloud Marketplace, 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. So we need to add another layer in between, a layer called Service.

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

<?php
namespace OCA\OwnNotes\Service;

use Exception;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCA\OwnNotes\Db\Note;
use OCA\OwnNotes\Db\NoteMapper;

class NoteService {

    private $mapper;

    public function __construct(NoteMapper $mapper){
        $this->mapper = $mapper;
    }

    public function findAll($userId) {
        return $this->mapper->findAll($userId);
    }

    private function handleException ($e) {
        if ($e instanceof DoesNotExistException ||
            $e instanceof MultipleObjectsReturnedException) {
            throw new NotFoundException($e->getMessage());
        } else {
            throw $e;
        }
    }

    public function find($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);
        }
    }

    public function create($title, $content, $userId) {
        $note = new Note();
        $note->setTitle($title);
        $note->setContent($content);
        $note->setUserId($userId);
        return $this->mapper->insert($note);
    }

    public function update($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);
        }
    }

    public function delete($id, $userId) {
        try {
            $note = $this->mapper->find($id, $userId);
            $this->mapper->delete($note);
            return $note;
        } catch(Exception $e) {
            $this->handleException($e);
        }
    }

}

Following that, create an exception class in ownnotes/lib/Service/ServiceException.php:

<?php
namespace OCA\OwnNotes\Service;

use Exception;

class ServiceException extends Exception {}

Then, create another one in ownnotes/lib/Service/NotFoundException.php:

<?php
namespace OCA\OwnNotes\Service;

class NotFoundException extends ServiceException {}

Remember how we had all those ugly try/catch blocks that where checking for DoesNotExistException and simply returned a 404 response? Let’s also refactor these into a reusable class.

Specifically, we’ll use a trait, so that we can inherit methods without having to create a large inheritance hierarchy. This will be important later on when you’ve got controllers that inherit from the ApiController class instead. The trait is created in ownnotes/lib/Controller/Errors.php:

<?php

namespace OCA\OwnNotes\Controller;

use Closure;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCA\OwnNotes\Service\NotFoundException;

trait Errors {

    protected function handleNotFound (Closure $callback) {
        try {
            return new DataResponse($callback());
        } catch(NotFoundException $e) {
            $message = ['message' => $e->getMessage()];
            return new DataResponse($message, Http::STATUS_NOT_FOUND);
        }
    }

}

Now we can wire up the trait and the service inside the NoteController:

<?php
namespace OCA\OwnNotes\Controller;

use OCP\IRequest;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Controller;
use OCA\OwnNotes\Service\NoteService;

class NoteController extends Controller {

    private $service;
    private $userId;

    use Errors;

    public function __construct($AppName, IRequest $request,
                                NoteService $service, $UserId){
        parent::__construct($AppName, $request);
        $this->service = $service;
        $this->userId = $UserId;
    }

    /**
     * @NoAdminRequired
     */
    public function index() {
        return new DataResponse($this->service->findAll($this->userId));
    }

    /**
     * @NoAdminRequired
     *
     * @param int $id
     */
    public function show($id) {
        return $this->handleNotFound(function () use ($id) {
            return $this->service->find($id, $this->userId);
        });
    }

    /**
     * @NoAdminRequired
     *
     * @param string $title
     * @param string $content
     */
    public function create($title, $content) {
        return $this->service->create($title, $content, $this->userId);
    }

    /**
     * @NoAdminRequired
     *
     * @param int $id
     * @param string $title
     * @param string $content
     */
    public function update($id, $title, $content) {
        return $this->handleNotFound(function () use ($id, $title, $content) {
            return $this->service->update($id, $title, $content, $this->userId);
        });
    }

    /**
     * @NoAdminRequired
     *
     * @param int $id
     */
    public function destroy($id) {
        return $this->handleNotFound(function () use ($id) {
            return $this->service->delete($id, $this->userId);
        });
    }

}

As a result of these changes, the only reason that the controller needs to be changed is when request/response related things change.

All documentation licensed under the Creative Commons Attribution 3.0 Unported license.