Please note: this documentation is seriously incomplete. If you'd like to help out, please fork the project on GitHub. Thanks!

  1. Getting Started
  2. Project Configuration
    1. VirtualHost Setup
    2. Subfolder Setup
    3. Directory Permissions
  3. Project Modes
  4. Settings
  5. Creating an app
    1. Routes
    2. Actions
    3. Views
    4. Models
      1. Objects
      2. Tables
      3. A note on data persistence
      4. CRUD functionality: Objects Vs Tables
      5. Defining your model's fields
  6. A more advanced app: Latest News
    1. Dynamic path parameters
  7. Sessions
  8. Cookies
  9. Testing
    1. Test fixtures
    2. Headless browser testing
    3. Selenium testing
  10. Caching
  11. Logging
  12. CLI Tools

Getting Started

First things first - you'll need to grab the latest copy of the codebase. Follow the quick install instructions to clone the framework & the library.

Project Configuration

When developing a site you'll want to ensure you're in either build or test mode, both of which will enable developer friendly error messages and debug-level logging. The recommended way of doing this is to set up an Apache Virtualhost to house your newly created project.

VirtualHost Setup

<VirtualHost *:80>
    ServerName jaoss-website.build
    ServerAlias jaoss-website.test
    SetEnvIf Host jaoss-website.build PROJECT_MODE=build
    SetEnvIf Host jaoss-website.test PROJECT_MODE=test

    DocumentRoot /var/www/jaoss-website/public

    <Directory /var/www/jaoss-website/public>
        DirectoryIndex index.php
        AllowOverride All
        Order allow,deny
        Allow from all
    </Directory>
</VirtualHost>

If you're using made up domains as in the example above, don't forget to add them to your /etc/hosts file and make sure the IP points to your webserver (127.0.0.1 if you're working locally).

Subfolder Setup

If you can't be bothered to set up a VirtualHost, or you really want to run your project as a subfolder (say, on a Wordpress installation), then you'll have to add a SetEnv directive to the root folder's .htaccess file (not the one in the public folder). Just make sure you remove it when you deploy to a demo / live environment - the default mode if no environment variable is defined is live:

# this will only ever kick in if the preferred VirtualHost set up hasn't been followed
# and the codebase is just being accessed directly in a subfolder. It's here as backup.
RewriteEngine On
RewriteRule ^(.*)$ public/$1 [L]
SetEnv PROJECT_MODE build

Note that this approach has some big drawbacks in that the .htaccess file is version controlled, and it does now allow simultaneously running multiple modes. Unless you need the project to be a subfolder, the VirtualHost route is strongly recommended.

Directory Permissions

Your web user will need to be able to write to log/ and tmp/ (both relative to the project root). If you're set up in build mode when you first run your project with any luck you should get some nice developer friendly errors if either directory can't be written to, but if not then try making them writable and trying again. Once these are set up be sure to keep an eye on the logs in the log directory - it's recommended to always have a terminal open running tail -f log/debug.log.

Project Modes

There are five supported project modes, each of which inherits any settings from the mode loaded before it - though these settings can be overridden at each level. The modes are as follows:

  1. live
  2. demo
  3. build
  4. test
  5. ci

The mode your application is running in depends on the PROJECT_MODE environment variable set - see the earlier Project Configuration section for details on how to configure this.

Settings

On its own, PROJECT_MODE doesn't really mean a lot. Its core purpose is to control which of the settings/*.ini files are loaded. Settings are loaded in the order noted above up to and including the ini file matching the current mode. Therefore if your PROJECT_MODE is set to build, the framework will attempt to load (in order):

  1. settings/live.ini
  2. settings/demo.ini
  3. settings/build.ini

Consequently, if your PROJECT_MODE was test, settings/test.ini would be loaded as well, whereas if the mode was live then only settings/live.ini would be loaded.

As noted earlier, settings cascade down through each mode such that those declared in the last loaded ini file will take precedence over any which have previously been loaded. This allows the bulk of the settings to be declared in settings/live.ini and refined at each level as required.

Creating an app

Once you've got a project set up you're ready to dive in - and where better than creating an app of your own? Before we do this, let's define exactly what we mean by an 'app' in the context of a jaoss project.

An app is a collection of one or more controllers, views, and / or models relating to one subject.

The term subject covers quite a broad range - suitable examples would be a 'blog' app or a 'users' app. That said, apps deliberately do not railroad the developer down any particular path, so ultimately an app is whatever you choose it to be - your entire project could be one app - it would just be rather unweildy and you'd struggle to re-use many of its components in other projects.

All of this leans towards treating apps almost as you would folders - the only restriction is that app folders should always be lowercase. Let's get on with it and create our first app folder and setting up some simple routing - we're going to make a very basic 'static' app which will simply render some very basic views for known URLs. Let us begin:

nick@nick-desktop:~/www/demosite$ mkdir static

Don't worry, it gets a little more involving than this...

Routes

First things first - routes are referred to as paths in the codebase. The terms are interchangeable, but we'll stick to paths in this tutorial to keep inline with the classes and methods you'll encounter when working with jaoss.

One of the first things jaoss will do when processing a request is try to work out what apps to load, and what paths to load for each app. It does this by looking for a paths.php file in each subfolder in the apps/ directory. If it doesn't find this file it'll carry on regardless, though it will log to log/verbose.log if you've got verbose logging enabled (you won't, yet).

Let's create paths.php and add a single path to it. Add the following lines to apps/static/paths.php:

This snippet adds a path which will match a URL of /about (the first argument) to a controller action of the same name (the second argument). It takes additional optional arguments such as the controller to use and its location, which if omitted are worked out automagically based on the location of the paths.php file which was loaded.

There are two main methods available for loading paths, though loadPaths as used above is the most flexible and most consice (particularly when adding a large number of paths).

The Path Manager object is responsible for loading all available paths for each app as well as matching a request URL (e.g. /about) to a path. It also keeps track of which paths it's already tried to match - in short, it's a crucial part of a jaoss request.

Actions

Enough talk - head on over to http://<your-website>.build/about and see what happens. With any luck, you'll get a developer friendly error which says something like:

Could not find controller class 'StaticController'

Controller class StaticController could not be found.

The path searched was /var/www/yoursite/apps/static/controllers/static.php.

Go ahead and create the file it's after in apps/static/controllers/static.php. Note that the filename is a lowercased version of the controller name, minus the 'controller' part since this is assumed from the fact the file is inside a 'controllers' directory. Populate your newly created file as follows:

Upon refreshing the page you should now be presented with a Template Not Found error which gives you a clear indication as to what jaoss was expecting to find. If your controller action does not explicitly return anything, jaoss will attempt to render a template matching the name of the action it just executed (in our case, about.tpl). Let's create that file now - the error should tell you where it was looking for it, the first of which should be /var/www/yoursite/apps/static/views, so we'll put it in there.

Views

Jaoss uses the Smarty template engine to render views. There are no plans to support other mechanisms (such as Twig) just yet, though of course any forks are welcome to add this. Create apps/static/views/about.tpl and add some basic content to it:

Refresh the page and you should see the fruits of your labour, with the <h1> sporting the content you assigned from the controller.

These three simple steps of creating a path, an action and a view don't seem like much but they pave the way for incredibly powerful, dynamic and flexible applications - and along with models form the backbone of any application you're likely to create.

Models

Models are, as in any MVC framework, a fundamental part of most applications you'll make. This isn't the place to discuss what models are conceptually - but it is the place to discuss their implementation in jaoss, so let's do that. Each model you'll create is split into two distinct classes:

  1. Objects - a single entity - e.g. one person, one news article, one photo
  2. Tables - a collection of objects
Objects

An object — a single entity — knows how to do things relating to itself, but not its peers. For example, an object might declare a method getDisplayName() which might try and return a friendly name for a person (e.g. Nick Payne, or Nick, or @makeusabrew if I hadn't entered my full details). It would not, however, declare a method called findByName($forename, $surname) - this should instead be left to the model's corresponding Table.

Objects do know how to create, update or delete their equivalent row in the database table which represents them via their save and delete methods.

Tables

A table represents the concept of a collection of objects. As such, the vast majority of methods you'll declare in your table will be similar to findBy...() or getBy...() (no particular naming convention is enforced) which will in most cases return an array of Objects. Principally, table methods deal with reading either single or multiple objects.

A note on data persistence

One area where you may find jaoss differs to other frameworks is the fairly tight coupling between models and the data storage underpinning them - the only currently supported mapping being to a MySQL database. In general, tight coupling is bad, but jaoss is a framework driven by necessity, and thus far decoupling models from their database backend has not been necessary. Additionally, CRUD functionality is not isolated to one class or the other, hence they are both database aware. This behaviour is discussed above and summarised in the following table:

CRUD functionality: Object Vs Table
  Class DB Method(s)
Create Object save
Read Table read, find, findAll + your own
Update Object save
Delete Object delete

This might lead you to think that the responsibilities are somewhat skewed and that the Table class is rather superflous - but it has one key property which is the corner stone of all models: the $meta array. It's this array which holds the definition for the fields (or columns) which represent and define your model's data.

Defining your model's fields

You define your model's fields as a nested array inside your table's $meta array. Arguably, these fields could be declared as a property of the object instead - after all, it's objects which actually have fields with values populated from database tables. However, therein lies the point: if you've got an object then you want data, not data about data. Similarly it is the (database) table which physically defines the fields we can store, so it is the (model) Table which defines them in our application. Lastly, if you have an array of hundreds or thousands of objects, the last thing you need is a useless array of meta data duplicated across each and every one.

Fields should never be declared directly inside the meta array - instead they should be declared in a namespaced columns array to allow for other meta data to be added in future*. For example:

* this is pretty much the only example of 'we might need this...' you'll find in the framework. Everything else is there because it is used.

A list of available column types and other keys is discussed later - the important aspect for now is to remember that fields must be declared within a columns array.

We could discuss the concepts of models in a lot more detail, but let's just get stuck in with a real world example. Let's create a more advanced app which will deal with models and more advanced routing.

A more advanced app: Latest News

The app we just created was pretty boring, and not exactly very dynamic. Let's look at creating the staple of many websites; some news. To do this, we're going to want a model to represent our news stories, and some more dynamic routes to cater for the fact our data is also dynamic. Our news is going to be user generated - anyone can anonymously submit news providing they enter an email address*. We're going to need:

  • An 'add article' page
  • A 'view article' page
  • An index page showing snippets of the 5 most recently created articles

*obviously, in a real application this would lead to complete carnage without some form of user authentication or article moderation - topics which are covered later.

Let's dive straight in by creating our model to represent our articles:

Let's create the MySQL table to store our model. We're going to use the CLI Tools to do this, though of course we could just create it manually. Notice in the snippet below we manually pass the PROJECT_MODE environment variable as by default the CLI tools work in test mode, whereas we want to work in build mode for now. If you don't want to have to type PROJECT_MODE=build each time you invoke the cli tools you can of course run export PROJECT_MODE=build on a bash like shell.

nick@nick-desktop:~/www/demosite$ PROJECT_MODE=build ./jcli create table Articles
PROJECT_MODE set to build

Looking for model in project apps directory...
Found Articles model!
[PDOException] SQLSTATE[28000] [1045] Access denied for user 'youruser'@'localhost' (using password: YES)

Ah. That's not quite what we wanted to see. We haven't given jaoss our access credentials to our database yet so it's using the defaults found in settings/live.ini which are of course incorrect. Rather than alter them directly, we're going to override the relevant settings in settings/build.ini since that's the mode we're working in. Create a database for your project if you haven't already (I'm using demosite_build) and then update build.ini:

You should always create a new database user for each project you create and make sure it has ALL PRIVILEGES on the relevant database. Do not simply re-use your root login - for starters, everyone on the project probably has different root passwords (or they should!) and besides, you really don't want to expose this information anyway.

Let's run the create table command again. You should see something like:

nick@nick-desktop:~/www/demosite$ PROJECT_MODE=build ./jcli create table Articles
PROJECT_MODE set to build

Looking for model in project apps directory...
Found Articles model!
Table articles created on database [demosite_build]
Don't forget to add this new table to any test fixtures!
Done (0.082 secs)

You can optionally pass the --output-only flag to this command to see what SQL would be executed instead of actually running it. This is useful to verify that the table schema is as you'd expect it to be before committing to it.

Perfect - we've now defined our basic model and created a database table to store it in. Let's dive in and create a path which will allow users to add articles to the site. While we're at it, we'll also create a placeholder news controller and a placeholder view containing a form so a user can add an article:

If you load http://<your-website>.build/articles/add in your browser you should see your simple - and completely empty - form. Notice that you can submit the form and the page will still render even though the request method of the form is set to POST; by default, paths respond to any request method, though you can restrict this.

Let's turn our attention to that form, because without it we can't get very far. The framework ships with a useful helper in its default application called field.tpl, which although not pretty to look at, is incredibly useful when dealing with forms and in particular forms which need to be generated based on a model's fields. Let's refactor our view it in incremental stages to shed a bit of light on how the field helper works. Firstly, let's add some fields in manually:

Refresh the page - you'll see a list of labels and text inputs corresponding to the field names you passed through when including each field template. Naturally, the label names and the field types aren't quite right, so let's sort that out:

Which should end up looking something like:

We're getting there - but this probably all feels a little bit like déjà vu because we're pretty much just re-declaring the contents of our table's columns array. In fact, the first thing the field helper does is look for a smarty variable of that name, which if it finds it then uses to render the appropriate label, input type and attributes for the given field. Let's make that information available to smarty and refactor our view:

Notice that our view is now much simpler again - all we have to do is tell the field helper which field we're interesting in rendering, and it does the rest. The result looks like this:

Perfect - we've even got the proper label names too. We're now ready to beef up our action to handle the post request, validate the user's input, and if it is valid then create a new article. If we do create a new article, we'll redirect the user straight to view it. If we don't, we'll render the form again and notify the user of any errors with their input. This entire flow is shown in the snippet below:

As you can see, without the over-commenting and unnecessary temporary variables (shown for clarity), the actual logic involved in this common flow is minimal.

If you're using a modern browser there's a good chance you won't even be able to submit the form if your input isn't valid. This is because the field helper uses HTML5 input attributes where possible, preventing you from submitting the form with empty / invalid data. If you want to verify your server-side error handling is working, try overriding the email field helper include with a type of 'text' and then entering an invalid email address. You'll hit the form again this time with the error rendered next to the input - this is taken care of by the field helper.

Go ahead and fill out the form with some valid data and hit the submit button. Strangely, if everything worked you'll be greeted with a developer error, but fear not - this is only because we're trying to redirect to an action which doesn't exist yet (view_article). As a brief aside, let's take a look at the output in debug.log for a POST request to our add_article handler:

06/10/2011 11:22:13 (DEBUG)   - matched pattern [^/articles/add$] against URL [/articles/add] (location [apps/news] controller [News]
06/10/2011 11:22:13 (DEBUG)   - Init [NewsController->add_article]
06/10/2011 11:22:13 (DEBUG)   - Start [NewsController->add_article]
06/10/2011 11:22:13 (DEBUG)   - Validate::required(test) [title] - [OK]
06/10/2011 11:22:13 (DEBUG)   - Validate::required(this is my test article) [content] - [OK]
06/10/2011 11:22:13 (DEBUG)   - Validate::required(nick@paynedigital.com) [author_email] - [OK]
06/10/2011 11:22:13 (DEBUG)   - Validate::email(nick@paynedigital.com) [author_email] - [OK]
06/10/2011 11:22:13 (DEBUG)   - Article::setValues() returning [true] with error count [0]
06/10/2011 11:22:13 (WARN)    - Handling error of type [CoreException] with message [No Path found for options] and code [11]

Remember: tail -f log/debug.log is your friend!

So - our trusty users can now add articles to our website, but they can't yet see them. Let's tackle that next.

Dynamic path parameters

So, we need to create a path which will handle requests to view an actual article. Unlike those we've looked at thus far, this path is going to have to be able to accept a dynamic parameter - in this case, the ID of the article we wish to view. We do this using a combination of regular expressions and named subpatterns. You don't have to know a massive amount about either - in fact, the routes we've been entering prior to this point have been simple regular expressions all along - you just didn't know it. Let's add our new path now:

All we're doing is capturing a named sub pattern called id which, if matched, will always be an integer as denoted by \d+. Any sub patterns captured in your paths are available in your controller via the getMatch method. Let's create our new action and view to render our articles - notice that we've stripped out the comments and unnecessary variables from add_article to save a bit of space:

And voila!

There are several projects available on Github to learn from. Check out the Payne Digital source, the basic framework template or even this website.

Throughout this tutorial, many topics will link directly to the relevant class / folder / template relating to the discussion. These links are worth taking a look at in context to get a better understanding of what exactly the tutorial is describing. They will be shown like this.