Mini Framework with Aura

Introduction

NOTE: Code sample is here

Micro frameworks like Slim are pretty popular these days. So is the trend to de-couple libraries and packages within projects. I am a huge fan of the Solar Framework for PHP5 and use it almost daily; however, it’s one of those large collections of libraries that is tightly coupled. That’s not necessarily a bad thing. Solar is a great framework, but there are some big pluses to the de-coupled approach. Enter Aura. This was the next natural progression for me, since it’s authored by Paul Jones, the same creator of Solar. Aura is sort of the next major version of Solar, but it’s completely re-written so I prefer to think of it as simply inspired by Solar.

Aura is a collection of independent packages each performing a specific task. Additionally, there is an Aura.System package that combines required packages into a full-stack framework. I haven’t had the chance to dive into the Aura.System package yet, but one of the newest packages, Aura.Dispatcher, really caught my eye. Aura.Dispatcher basically maps objects to request parameters. Imagine a router that maps your HTTP request into parameters. Instruct Aura.Dispatcher what object (or closure) should be instantiated based on those parameters, and then hand the parameters to the dispatcher. Aura.Dispatcher will instantiate the appropriate object and return some result, such as HTML. Looking at the Aura.Dispatcher documentation, you can see immediately that it could be used as a very effective micro framework, or the basis for something bigger.

// taken from the Aura documentation
 
use Aura\Dispatcher\Dispatcher;
 
$dispatcher = new Dispatcher;
$dispatcher->setObjectParam('controller');
 
// Next, we set a closure object into the Dispatcher using setObject():
 
$dispatcher->setObject('blog', function ($id) {
    return "Read blog entry $id";
});
 
// We can now dispatch to that closure by using the name as the value for the controller parameter:
 
$params = [
    'controller' => 'blog',
    'id' => 88,
];
 
$result = $dispatcher->__invoke($params);
echo $result; // Read blog entry 88

I want something somewhere in between micro-framework and a full-stack framework. I am looking for a mini framework. I want a template (view) package, and page controllers. And somewhere in there, I want an ORM to handle the models. I will use Aura for everything, but the ORM. To make things really quick and simple, I chose Paris for the ORM.

So, here is how I got it all going.

Install packages using Composer

I am not going to explain how to use Composer to install packages. Go to the Composer web site for those instructions. What I have here is the composer.json file to show the packages required. Please note the ‘autoload’ section. If you plan on using your own namespace for your code, use something different than ‘Example’.

{
    "require": {
        "aura/dispatcher": "dev-develop-2",
        "aura/router": "1.1.1",
        "aura/view": "1.2.1",
        "j4mie/paris": "1.*"
    },
    "autoload" : {
        "psr-0": {
            "Example\\": "src/"
        }
    }
}

After you install the packages via Composer, you should have a directory structure similar to the following:

/your/path/vendor
-composer.json
-composer.lock
-composer.phar

Next, create two more folders. One called “docroot” and one called “src”. Docroot will be for the web server document root where the index.php file and other assets will live. Src will be for our project-specific vendor code. Inside the docroot folder, create an “index.php” file and a “.htaccess” file.

/docroot
    -.htaccess
    -index.php
/src
/vendor
-composer.json
-composer.lock
-composer.phar

I like to use Apache’s rewrite module to make for tidier URLs. Below is my .htaccess file contents. Note that I am using a RewriteBase /aura. This is because I don’t have the project set into the upper-most folder on the web server. My actual URL will be: http://myhost.domain/aura. You may have something different.

<ifmodule rewrite_module>
    # turn on rewriting
    RewriteEngine On
    # set the base path since not at the root.
    RewriteBase /aura
 
    # keeping the query string intact
    RewriteRule ^$ index.php [QSA]
 
    # keeping the query string intact.
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    # add $1 to ensure we always have PATH_INFO
    RewriteRule ^(.*)$ index.php/$1 [QSA,L]
</ifmodule>

Setup the Database

We are going to create a standard blog application, so we will need a database to go with it. Create a simple database and a blog table.

Here is a create table SQL you can use.

CREATE TABLE IF NOT EXISTS `blog` (
  `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(255) NOT NULL,
  `entry` text NOT NULL,
  `userid` VARCHAR(8) NOT NULL,
  `created` datetime NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

And dump in a few rows of data.

INSERT INTO `blog` (`id`, `title`, `entry`, `userid`, `created`) VALUES
(1, 'First blog', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec eleifend urna id nisi vehicula elementum a eu ipsum.', 'juser', '2013-09-26 12:34:10'),
(2, 'Once upon a time in Prague', 'Integer pulvinar auctor aliquet. Suspendisse vulputate hendrerit tortor eget laoreet.', 'juser', '2013-10-01 13:57:38'),
(3, 'Once upon a time in Sophia', 'Integer pulvinar auctor aliquet. Suspendisse vulputate hendrerit tortor eget laoreet.', 'juser', '2013-10-01 13:58:47');

Bootstrapping

Now it’s time to set up the bootstrap (index.php) file to load the libraries, set up the routing, and set up the dispatcher.

Open the /docroot/index.php file and set up our session, autoloader, and router map.

// start the session
session_start();
 
//composer's autoloader
require '../vendor/autoload.php';
 
// Set up ORM (Paris, Idiorm)
ORM::configure('mysql:host=localhost;dbname=mydb');
ORM::configure('username', 'someuser');
ORM::configure('password', '******');
 
// Router map
$router_map = include '../vendor/aura/router/scripts/instance.php';

Now we set up some named routes. I am not going to detail the mapping of routes much because it’s covered in the Aura.Router documentation. In the samples below, the syntax follows this pattern:

$router_map->add(string route_name, string pattern, array values);

Note that I included the ‘/aura’ in my pattern. If you don’t use /aura in your url, then leave that out! Also notice the PHP 5.4 short array syntax. Did I mention you need PHP 5.4+?

// Routes to match
$router_map->add('home', '/aura', [
    'values'=>[
        'action'=>'index',
        'controller'=>'home'
    ]
]);
// {:controller} and {:id} will automatically become values 
// keyed by 'controller' and 'id' respectively
$router_map->add('read', '/aura/{:controller}/read/{:id}', [
    'values'=>[
        'action'=>'read'
    ]
]);
 
$router_map->add('edit', '/aura/{:controller}/edit/{:id}', [
    'values'=>[
        'action'=>'edit'
    ]
]);
 
$router_map->add('delete', '/aura/{:controller}/delete/{:id}', [
    'values'=>[
        'action'=>'delete'
    ]
]);
 
$router_map->add('add', '/aura/{:controller}/add', [
    'values'=>[
        'action'=>'add'
    ]
]);
 
$router_map->add('browse', '/aura/{:controller}', [
    'values'=>[
        'action'=>'browse'
    ]
]);

The next step is to get the path from the HTTP request and try to match a route using the router_map.

// Request path, with some cleanup
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = rtrim($path, '/');
 
// get the route based on the path and $_SERVER super global
$route = $router_map->match($path, $_SERVER);

Now we have some logic to see if we matched a route. If we did, get the matched route’s parameters to pass off to Aura.Dispatcher. If we didn’t match a route, set some defaults parameters to send them off to Aura.Dispatcher, usually to throw a 404 not found.

// If we matched a route, get it's values, otherwise, set defaults (404)
if ($route) {
    $params = $route->values;
    if (! isset($params['action'])) {
        $params['action'] = 'index';
    }
} else {
    // 404 via the home controller's __invoke() method
    $params = [
        'controller'=>'home', 
        'action'=>null
    ];
}

Now, we start setting up Aura.Dispatcher. Like Aura.Router, I am not going to go into much detail about the various ways you can set it up. That is best left to the Aura.Dispatcher documentation.

First, we are going to tell the dispatcher what parameter it should examine to map to the object it will instantiate. Also, tell the dispatcher what parameter to use for the method it will run. In this example, the parameter name is ‘controller’, and the method parameter name is ‘action’.

// Dispatcher object and method params
$object_param = 'controller';
$method_param = 'action';

For example, given the params:

$params = [
    'controller'=>'blog',
    'action'=>'browse'
];

Aura.Dispatcher will try to instantiate an object mapped to ‘blog’ and then try to run an method called ‘browse’. This should become clearer as we move forward.

The next step is to map some named objects to the dispatcher. So, if the dispatcher does match the ‘blog’ controller parameter, what object should it instantiate? We can use closures or other invokable objects here. See the documentation for more information. In this case, matching a ‘home’ controller parameter tells the dispatcher to return a new \Example\Home() object, passing a few parameters en route. If it matches the ‘blog’ controller parameter, it should return a new \Example\Blog() object.

// Dispatchable (invokable) objects matching a controller name
$objects = [
    'home'=>function() use ($params, $router_map) {
        return new \Example\Home($params['controller'], $params['action'], $router_map);
    },
    'blog'=>function() use ($params, $router_map) {
        return new \Example\Blog($params['controller'], $params['action'], $router_map);
    },
];

Finally, we need to create the dispatcher object and run the dispatcher with the parameters.

// Aura dispatcher
$dispatcher = new Aura\Dispatcher\Dispatcher($objects, $object_param, $method_param);
 
// If we don't have the requested object, send it to the home controller for a 404
if ($dispatcher->hasObject($params['controller'])) {
    // $dispatcher->__invoke($params) is the same as $dispatcher($params);
    $output = $dispatcher($params);
    echo $output;
 
} else {
    $params['controller'] = 'home';
    $params['action'] = null; // this will call \Example\Home::__invoke() and throw a 404
    $output = $dispatcher($params);
    echo $output;
}

That’s it for bootstrapping. Now we need to build our controller objects, views, and models.

Continue reading

Multiple Database Connections with Solar Dependency Injection

In an email the other day, a person asked about being able to connect to multiple databases in Solar. While this is quite easy to do, it probably isn’t officially documented anywhere. If you poke around in Solar’s code, you will soon discover that it can be accomplished using dependency injection (technically this is a combined dependency injection and service locator).

Each of your application’s models extend the Solar_Sql_Model class. If you examine the Solar_Sql_Model class, you’ll notice that one of the config elements is ‘sql’. This is a key that refers to a Solar dependency. The default value for this is key is ‘sql’. That might seem strange at first. How is the string ‘sql’ a dependency? Well, first, we need to dig into the Solar_Sql_Model class. In the _preSetup() method there is a call to Solar::dependency(), which populates the $_sql property using the config[‘sql’] element.

protected function _preSetup()
{
...
// connect to the database
$this->_sql = Solar::dependency('Solar_Sql', $this->_config['sql']);
}

The first parameter is a hint to the the type of object, and the second is either an object OR a string that refers to an object in Solar’s registry. If you remember, the default value of Solar_Sql_Model’s ‘sql’ config element was a string ‘sql’. Therefore, there must be an object in Solar’s registry named ‘sql’. If not, Solar will throw and exception.

If you look at the Solar vendor’s default config (SYSTEM/source/solar/config/default.php) you will see the following:

$config['Solar']['registry_set'] = array(
    'sql'=>'Solar_Sql',
    'user'=>'Solar_User',
    'model_catalog'=>'Solar_Sql_Model_Catalog',
    'mail_transport'=>'Solar_Mail_Transport',
    'controller_front'=>'Solar_Controller_Front',
);

The ‘sql’=>’Solar_Sql’ tells Solar to register a Solar_Sql object in the registry keyed on the string ‘sql’. If you look again at the default.php config file, you should see a config entry for for Solar_Sql where you can define the options for the Solar_Sql object.

/**
* sql adapter to use
*/
$config['Solar_Sql'] = array(
    'adapter' => 'Solar_Sql_Adapter_Sqlite',
);

That is the background information. To summarize what we now know…

  1. Solar’s models connect to the database using a dependency injection/service locator keyed on the string ‘sql’.
  2. Because the default value for the ‘sql’ key is the string ‘sql’, then we know we are looking for something located in Solar’s registry.
  3. Looking at the Solar default.php config file, we see that there is a ‘registry_set’ config element indicating that ‘sql’ should be registered in the registry as a Solar_Sql object.
  4. Finally, the Solar_Sql object is also configured in the default.php file, and in our example, it is configured to use Solar_Sql_Adapter_Sqlite as its adapter class.

Understanding how this works, we can start setting up other sql dependencies to be used by our models. Here is one way you can do it.

First, we need to add another item to our registry. We can call it ‘sql_acme’. One thing to note is that you can specify an array as its value in the registry, not just a class name. It might look like this:

// configuration for the acme Solar_Sql object to be added to the registry
$sql_acme = array(
    'adapter'=>'Solar_Sql_Adapter_Mysql',
    'host'=>'localhost',
    'name'=>'acme',
    'user'=>'juser',
    'pass'=>'S3cr3t'
);
 
// define what will be added to the regsitry
$config['Solar']['registry_set'] = array(
    'sql'=> 'Solar_Sql',
    'sql_acme'=>array('Solar_Sql', $sql_acme),
    'user'=> 'Solar_User',
    'model_catalog'=>'Solar_Sql_Model_Catalog',
    'mail_transport'=>'Solar_Mail_Transport',
    'controller_front'=>'Solar_Controller_Front',
);

Now, we need to tell Solar what registry entry to use for one of our models. You can set that up like this. Assume the model is for a table of articles.

$config['Acme_Model_Articles']['sql'] = 'sql_acme';

That’s all there is to the setup and configuration. There is, however; one important thing to keep in mind. If you are using Solar’s command line tools to create your models then, by default, Solar will use the default ‘sql’ dependency since it can’t read the configuration for a model that doesn’t yet exist. Luckily, you can specify the required configuration options at the command line. For example, if your user credentials and host were the same for both databases, and the only difference was the database name, then you could do this:

$ ./script/solar make-model Acme_Model_Articles --name acme

If you want to know all the options available to the make model (or any) command, use the following:

$ ./script/solar help make-model

So that is one way you can set up multiple databases at the model level in Solar. You can also set up multiple databases for each vendor. I will save that for another post.

SimpleXML + Solar’s Alternate Formats = Easy RSS

Creating your own RSS feeds is generally straightforward, but it’s even easier when you combine PHP’s SimpleXML and Solar’s alternate format mechanism.

If you are new to Solar, you better check out the manual.

Setup Solar

Solar makes it easy to specify an alternate format simply by changing the extension of your request. For example, say you have a list of articles at

http://www.example.com/articles/browse

Here, “articles” refers to the controller, and “browse” refers to the action. By default, no format was explicitly specified, so the default text/html is used when outputting the content to the browser. If you wanted to use an alternate format, you can specify the format by using an extension. Here are a couple of examples:

  • http://www.example.com/articles/browse.rdf (application/rdf+xml)
  • http://www.example.com/articles/browse.xml (application/xml)

Before these will actually work, you have to perform a couple of tasks in your Solar environment.

First, you need to tell Solar that a given format (or formats) are allowed for a given action. Do this by adding code to the top of the Articles.php application controller.

<?php 
protected function _setup()
{
    // chain to parent
    parent::_setup();
 
    // allow xml and rdf formats for the browse action
    $this->_action_format = array(
        'browse'=>array('xml', 'rdf')
    );
}
?>

Second, you need to create an appropriate view for each format specified. In the example above, you need two new views. One for the xml format, and one for the rdf format. The naming convention for each view is as follows:

  • browse.xml.php
  • browse.rdf.php

Now, when browsing to http://www.example.com/articles/browse.xml, Solar will use the browse.xml.php view and output accordingly.

Create Markup with SimpleXML

Before using SimpleXML to format the xml output, it’s assumed that we have a collection of article records to work with, and that they are available to the view as $this->list.

Inside the browse.xml.php file…

<?php
 
// Get the current URI
$uri = Solar::factory('Solar_Uri_Action');
$uri->format = null; // reset the format
$link = $uri->get(true); // get the full uri, including http:// etc
 
$xml = new SimpleXMLElement('<rss version="2.0"></rss>');
$channel = $xml->addChild('channel');
$channel->addChild('title', 'My RSS Feed');
$channel->addChild('link', $link);
$channel->addChild('description', 'My RSS feed about something of interest');
$channel->addChild('language', 'en-ca');
$channel->addChild('pubDate', $this->list[0]->date_added); // assume the list in chronologically ordered
$channel->addChild('lastBuildDate', $this->list[0]->date_added.' MST');
$channel->addChild('webMaster', 'me@example.com');
foreach ($this->list as $item) {
	$uri->path = 'articles/read/' . $item->getPrimaryVal(); // set up the path to each article
	$title = $channel->addChild('item');
	$title->addChild('title', $this->escape($item->title));
	$title->addChild('link', $this->escape($uri->get(true)));
	$title->addChild('description', $this->escape($item->description));
	$title->addChild('pubDate', $this->date($item->date_added, "D, d M Y")); // Solar's date() view helper
}
echo $xml->asXML();
?>

And that’s basically all there is to it! Browse to http://www.example.com/articles/browse.xml and you should see an xml version of the page.

References

Add validation filters before saving in Solar

I have preached about how Solar makes my web development life so much easier, and here is another example of why.

Every once in a while, I have to add or modify my record validation before it is saved in the database. For example, I am working on a publication database where a publication has several availability “flags”, such as

  • available as pdf,
  • available in print,
  • available as pdf by email

In this scenario, all three can be “unchecked”, however; available as pdf and available as pdf by email can NOT both be checked. So, to validate that, I use a pre-save hook in the publication record class and set a filter.

<?php
class Bookstore_Model_Publications_Record extends Bookstore_Sql_Model_Record
{
    protected function _preSave()
    {
        parent::_preSave();
        // Check to make sure that email_pdf and pdf are not both checked
	if ($this->_data['available_pdf'] == '1' && 
            $this->_data['available_pdf_email'] == '1') {
                $this->addFilter(
                    'available_pdf_email', 
                    'validateNotEquals', 
                    'available_pdf'
                );
	    }
	}
    }
}
?>

So, basically, before the record is saved, I look to see if available_pdf == 1 AND available_pdf_email == 1 and if so, add a new validation filter on available_pdf_email. The new filter is called “validateNotEquals” and you pass the name of a field you don’t want to allow it to be the same as. In this case available_pdf.

With this filter added, the record won’t save and will display an error message under available_pdf_email.

Simple.

New Gear

I’ve had to retire my old training watch. It’s a Suunto T3 and has performed well over the last few years. I broke the strap for the second time and it’s over $20 to replace and a special order. That’s a pain in the butt. Also, the heart rate strap seems to have stopped working. So now, it’s just a stopwatch. I didn’t want to spend a bunch of money fixing it and the like so I bought a cheaper replacement. It’s a basic Timex Ironman Road Trainer HRM for only $105 from Mountain Equipment Coop (MEC). It has fewer features, but I think I will survive without them. The one I will miss is the interval timing. I try to run intervals once a week and being able to set two separate interval times is nice (run hard for 2 minutes and rest for 1 minute). Today will be my first day with the new watch so we will see how it goes.

I also replaced our screen tent. My family are tenters, so having a decent shelter for cooking and eating is a must. Our old screen tent was heaving and failing so it was time for a replacement. I picked up a Hootenanny from MEC and set it up yesterday. So far it’s great! Not sure how it will do in a steady rain, but I can always tarp it. Now I just have to get out and use it!

Some like it cold

We have had a pretty bad cold snap for the last few days. A few broken records for low temperature too. The other night, we hit -46.1 C and the wind chill would have made it feel like -59 C. We were the second coldest place recorded on Earth. A location in Siberia recorded -48 C. Brrrrr.

A screenshot from Environment Canada's website showing the minimum temperature

A screenshot from Environment Canada's website showing the minimum temperature

Start Using a Top Quality Framework “Right Now”

Paul M. Jones has what I will call a “fun” post on is blog. There may be some “cheerleading”, but I welcome it. His post points to several areas where the Zend Framework (ZF) developers will be implementing approaches already available in Solar. This is by no means framework-bashing. It’s about letting people know that a top quality framework is available right now. So what are you waiting for? Get started.

Invalid RSS Feeds

I created a simple feed parser for Solar the other day. I am using this on our corporate Intranet. It works pretty well so far and uses Solar_Cache to periodically store the data for fast retrieval. The parser uses PHP’s SimpleXMLElement. Today I found a feed that wasn’t showing up properly. After some digging, it turned out that it was because the feed itself was not valid; it had extra content after the closing rss tag. The last bit of content looked like some sort of stats tracking image and some comments. Invalid RSS feeds are too common, IMHO. The problem is, that the tools we use to parse the feeds get stuck with the job of finding workarounds to the invalid feed data. What would be better is if more people complained that the feeds were invalid, forcing the author to fix them. This would make building simple feed readers a whole lot easier.

Gear Addiction

Ever since I was a kid I have loved hiking and camping gear. Backpacks, ropes, tents, knives and all that great stuff that goes along with camping. Since being a “grown up” with responsibilities, that addiction was replaced with other important things. This summer, however, the gear addiction was somewhat rekindled.

During my week off in the Summer, I spent the majority of my time in the outdoors, taking pictures, and doing fun things like making bows and arrows for my kids (well kind of mostly for me), and watching a lot of Survivorman (I love that Survivorman (Les Stroud) is a Canuck). I became more and more enthusiastic about the outdoors, survival and general camping. I took the family camping a couple of times – once to Jasper and once to Elk Island National Park. Great fun, even though it was terribly cold and rainy in Jasper. In the last several weeks I have pretty much worn out my MEC catalog and visited their web site countless times. I haven’t really even purchased anything (just a tent). I just spend all my time looking and drooling over all the gear. And it’s not that I need much in the way of gear. I just like to look at it! For example, I hate heights and will never really do any climbing, but I am fascinated by climbing gear. My older daughter is starting a climbing class this fall and I think it’s so great. What a cool thing for a 5 year-old to do! Anyway, I think I will stop now and go look at new sleeping bags. Mine is getting pretty worn out.