Skip to content

Developing custom Aspect Types

Ian Monroe edited this page May 7, 2018 · 13 revisions

Getting started

AspectTypes are objects which allow you to customize how a particular Aspect behaves. By creating custom AspectTypes, you can extend the utility of Coldreader by making new ways of capturing and displaying information.

An AspectType specifies overrides for an Aspect's core functions.

Custom aspect types are easy and quick to develop.

The best way to make your own custom aspect types is to build them within a standard composer-friendly PHP package.

Included with Coldreader is a custom Artisan command to get you started quickly.

From the command-line, simply run:

$ php artisan coldreader:new aspect_type

That command will prompt you for all the information needed to get a running start. Once you've run through that command, you will find that it creates a new directory for you on the following path:

<coldreader root>/packages/<vendor>/<package>

By default, the Artisan command sets everything up for building a new aspect type, however, you can also use it to generate the boilerplate you'll want for developing Search Providers as well. (Coming soon.)

Walkthrough

For the remainder of this tutorial, we're going to pretend we are working on an aspect type called Diary. For the sake of (my) convenience, we'll put it in the imonroe namespace. We will use the name cr_diary_aspect for the name of the package.

In the root directory of your Coldreader installation, run the following:

$ php artisan coldreader:new cr_diary_aspect

You will be prompted for some information:

What is the vendor name for this package?:
> imonroe

What is the package name for this package?:
> cr_diary_aspect

Give me a short description of the package.:
> A demonstration of how to build a custom Aspect Type for Coldreader

What is your name?
> Ian Monroe

What is your email address?:
> [email protected]

What is your github username?:
> imonroe

At that point, the software will generate all the boilerplate you'll need to get started. It will update your composer.json file, and will add a Service Provider to your config/app.php file. This is a great time to initialize a git repo to track our work.

$ cd packages/imonroe/cr_diary_aspect
$ git init
$ git add .
$ git commit -m 'initial commit'

Now you're all set. Set up a Github repo and set it as the origin if you like; all the .md files are already there, and sharing is caring.

Package file structure

Here's how it's going to look when we get started.

├── resources
│   ├── assets     
│   │   ├── css
│   │   └── js   <--- if you use Vue.JS components, this is a good place to put them.
│   └── views   <--- if you'll be using any views, here's where to put 'em. Standard Laravel Blade templates.
├── src
│   ├── CrdiaryaspectAspect.php   <--- Where the action happens.
│   ├── cr_diary_aspectServiceProvider.php   <--- Service Provider.
│   └── Http
│       ├── Controllers
│       │   └── CrdiaryaspectController.php   <--- Controller.
│       └── routes.php   <--- If you need custom routes, register them here.
└── tests
    └── CrdiaryaspectTest.php   <--- Including tests is awesome.

[temporary hack] The first thing we are going to want to do change around a few names, since aspect type classnames have to follow a particular pattern. We are ultimately going to want to call our new aspect type "Diary", and the pattern is to upper-camel-case the label you wish it to have, and append 'Aspect'. Thus, when creating an aspect type you want to read as, "My Favorites", you would name the class MyFavoritesAspect. Again, for convenience, we'll rename the file to match.

$ cd src/
$ mv CrdiaryaspectAspect.php DiaryAspect.php

Edit the DiaryAspect.php file in your favorite editor. First thing first, change the name of the class to DiaryAspect.

namespace imonroe\cr_diary_aspect;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use imonroe\crps\Aspect;

class DiaryAspect extends Aspect{

    function __construct()
    {
	parent::__construct();
    }

    public function notes_schema()
    {
	$settings = json_decode(parent::notes_schema(), true);
	return json_encode($settings);
    }

    public function create_form($subject_id, $aspect_type_id=null)
    {
        return parent::create_form($subject_id, $this->aspect_type);
    }

    public function edit_form($id)
    {
	return parent::edit_form($id);
    }

    public function display_aspect()
    {
        $parent_output = parent::display_aspect();
        $new_output = '<p>Some markup.</p>';
	return $output;
    }

    public function parse()
    {
        return parent::parse();
	//if (empty($this->last_parsed) || strtotime($this->last_parsed) < strtotime('now -1 hour') ){
	    // do something?
	//}
	//$this->last_parsed = Carbon::now();
	//$this->update_aspect();
    }
    public function pre_save(Request &$request)
    {
        return false;
    }
    public function post_save(Request &$request)
    {
        return false;
    }
    public function pre_update(Request &$request)
    {
        return false;
    }
    public function post_update(Request &$request)
    {
        return false;
    }
    public function pre_delete(Request &$request)
    {
        return false;
    }
}

The create_form() method

This method determines what sort of form the user is presented with when creating a new Aspect of our custom AspectType.

The edit_form() method

This method determines the form presented to the user when they edit an Aspect of our custom AspectType.

The display_aspect() method

This method controls how our Aspect is displayed in the context of a Subject.

Updating existing Aspects

After you make changes to an existing Aspect, always use the update_aspect() method to save your changes to storage.

The aspect_notes field

The aspect_notes field is used to store metadata about a given Aspect. It is stored as a JSON-encoded array. The structure of the aspect_notes field is determined by the notes_schema() function on the Aspect. The notes_schema() function returns a JSON string that specifies what the prototype of that structure should look like. While it is often more useful to work with the settings as a PHP array, it is stored in the database as JSON information. For instance, if you wish to store a key called number_of_items with a default value of 5, you might override the notes_schema function thusly:

public function notes_schema(){
	$settings = json_decode(parent::notes_schema(), true);
	$settings['number_of_items'] = '5';
	return json_encode($settings);
}

To access the metadata as a standard PHP array, the easiest way is:

$aspect = new MyCustomAspect;
$settings = $aspect->get_aspect_notes_array();

In this example, to modify the value of number_of_items, you could then do something like:

$settings['number_of_items'] = 10;
$aspect->aspect_notes = json_encode($settings);
$aspect->update_aspect();

Hooks

You will notice that there are five functions you can override which will allow you to hook into the life cycle of an Aspect.

  • pre_save() fires before the Aspect is saved.
  • post_save() fires after the Aspect is saved.
  • pre_update() fires before the Aspect is updated.
  • post_update() fires after the Aspect is updated.
  • pre_delete() fires before the Aspect is deleted.

These work exactly as you would expect, and will fire at the obvious times.

The Diary Aspect Type

We envision an Aspect Type which will keep a running log, journal, or diary. The user will be able to append new text to the end of the document, and will be able to review previous entries, but older entries will be locked from editing. The Diary will be displayed with the newest entry first, and will display entries from newest to oldest.

One way of building out this structure is to make a JSON-encoded array that is stored in the aspect_data field. We will need a simple interface to allow us to create and edit new entries. The create_form method allows us to build the form we will need to create a new Aspect of this type, and the edit_form will be used to add new entries to the Diary. We also need a display_aspect method which will allow us to get quick access to our create form, and a way of displaying past entries as well.

We'll use a structure like this to store our data for a single entry:

{
    'entry_date': '<some timestamp>',
    'entry_md': '<entry text>'
}

We know only one user will be using Aspects of this type at a time, so we can rely on the timestamp as a key. We will store the entire Diary aspect as an array of these single entries. We will store the text plain, but we will decode it as Markdown when we display it; that keeps it simple and intuitive.

So first, we will build the form we will use to enter a new Diary entry. Modify the create_form() function so that it looks like:

    public function create_form($subject_id, $aspect_type_id=null)
    {
        $form = \BootForm::horizontal(['url' => '/aspect/create', 'method' => 'post', 'files' => false]);
        $form .= \BootForm::hidden('subject_id', $subject_id);
        $form .= \BootForm::hidden('aspect_type', $aspect_type_id);
        $form .= \BootForm::text('title', 'Diary Title');
        $form .= $this->notes_fields();
        $form .= \BootForm::submit('Submit', ['class' => 'btn btn-primary']);
        $form .= \BootForm::close();
        return $form;
    }

A few interesting things to note here. First of all, you'll note that we are building our form using the BootForm library. This convenient library lets us construct Bootstrap 3-compatible forms without having to do all the markup ourselves. All the functionality of that library is available for you to use in constructing forms quickly (see the documentation for details). However, you need not use it; you may construct any kind of form with any kind of markup you wish. Just return the markup as a string from the create_form method, and it will show up correctly. One particularly handy thing is that you may construct your forms as single-file Vue.JS components as well. Just make sure you have correctly registered the components you wish to use. See the section on Vue.JS integration for more information (coming soon.)

As you can see from the code, all we are really doing here is assigning a title to this aspect. Each Aspect Type may be attached to a given subject an arbitrary number of times, so we'll want a title to remember which one is which.

Another interesting feature here is the line: $form .= $this->notes_fields(); The aspect_notes field (discussed above) may contain an array of metadata we wish to store with the Aspect. In that case, the notes_fields() function will output (Bootstrap 3-formatted) form fields which correspond to the entries in that array. It takes care of the markup, and ensures the metadata fields will get stored properly in the database when they're submitted.

You will notice that this form saves no information about the aspect_data field; we are going to structure that manually as JSON data, so we will leave it empty for now.

Now we will need a way to create new entries for the Diary. This is also a good time to delete out the hooks at the bottom of the class, so let's do that now.

Delete the pre_save, post_save, pre_update, post_update, and pre_delete functions, and then create a new function:

    public function new_entry_form()
    {
        $form = \BootForm::horizontal(['url' => '/aspect/'.$this->id.'/edit', 'method' => 'post']);
        $form .= \BootForm::hidden('aspect_id', $this->id);
        $form .= \BootForm::hidden('aspect_type', $this->aspect_type()->id);
        $form .= \BootForm::textarea('aspect_data', 'New Diary Entry', $aspect->aspect_data);
        $form .= $this->notes_fields();
        $form .= \BootForm::submit('Submit', ['class' => 'btn btn-primary']);
        $form .= \BootForm::close();
        return $form;
    }

Now, modify the display_aspect function like so:

    public function display_aspect()
    {
        $output = $this->new_entry_form();
	return $output;
    }

We may adjust this again later, perhaps to display the last entry, for instance. For now, this will do.

Clone this wiki locally