Show HN: Ham, a fast PHP router/microframework I hacked up last night
github.com
github.com
On the technical side, I have come to prefer using straight regular expressions for routes, rather than a dsl or made-up routing syntax a la rails. It's a relatively unknown feature of preg, but you can have named captures, that pretty much gives you all that you need. For example, here's a couple of routes from a recent application of mine:
$GLOBALS['ROUTES']['~^GET/devices/(?P<device_id>\d+)~'] = "devices/show";
$GLOBALS['ROUTES']['~^GET/devices~'] = 'devices/index';
My front controller is then just a loop over the routes, that will resolve first match.For example, a GET request to `/devices/1234` will resolve to the file `handlers/devices/show.php`, where `device_id` is available as a parameter.
Yes, the syntax is a bit awkward, but it's computationally efficient without a cache/compilation step and it's a standard language, rather than a proprietary one.
The lower bound is linear in the length of the route if all routes participate in your regular expression and if you have it pre-compiled to, for example, a DFA or an LL parser ahead of time.
It's a red herring anyway, so I sort of regret bringing it up. Much more important is the other point - that this is a standard tool, rather than a tailored one.
I have gone through your code a bit and think that you should consider error handling first. Some methods return strings on error but arrays on success but this is not validated before using the value as an array (this results in the error reported before). I guess when no caching system is available the Dummy will break your code as well as it only returns false.
Also, I think using "False" instead of "false" results in two internal checks when PHP tries to determine if something is false or true, instead of one. Not sure if that is still the case with the latest PHP version, though. I reckon using "false" would be better.
$data = $cache->get("key");
if(!$data) { // do the stuff if cache misses }
Though yes, I do need to go through and make sure it's extremely robust, as I did hack it up in an evening :)
$data = $cache->get('key',function() {
// do the stuff if cache misses
return $some_value_to_be_cached;
});
? It's quite handy and it forces you to separate actual logic to get the value, which is good by itself.On that note, I adapted a rule of an open source project (can't quite remember which) that said to never use !$var to validate your data. One should always use isset($found['args']) or empty($found['args']) or for example is_array($found) && count($found) > 0 to check.
This is why:
!array() -> true
!null -> true
!false -> true
!0 -> true
!"0" -> true (!!?)
!"" -> true
It also uses the same $app content, and but has - at the moment - a bit more evaluating of values.
But keep at it, well done.
I need to start writing tests and docs really, but as I said, it was something I hacked up in an evening :)
It's interesting to see the growing trend in this direction for PHP.
I wrote one a little while back[1] that does even less than this :) The only goal being to provide a simple way to execute classes from the web or command line with a default autoloading implementation and a way to inject new ones[2]
Personally a lot of my current direction in PHP comes from watching what's happening the node.js community - I like the way that there are a lot of small-ish packages/modules that are pretty independent.
I've written a couple of packages for RocketSled already which you can see on my github page if you're interested.
[1] https://github.com/iaindooley/RocketSled
[2] There are some obvious improvements that could be made to the current implementation which always parses the package tree ie. caching
In fact, if you want routes you'd have to build it as a package and in FACT you could just create a class like this:
class RocketHam implements rocketsled\Runnable
{
public function run()
{
$app = new Ham();
$app->config_from_file('settings.php');
$app->route('/pork', function($app) {
return "Delicious pork.";
});
$hello = function($app, $name='world') {
return $app->render('hello.html', array(
'name' => $name
));
};
$app->route('/hello/<string>', $hello);
$app->route('/', $hello);
$app->run();
}
}
then put your ham/ directory in packages with a file rs.config.php: spl_autoload_register(function($class)
{
if($class == 'Ham')
require_once(dirname(__FILE__).'/ham.php');
}
then use .htaccess or something similar to route all requests to:index.php?r=RocketHam
EDITS: oops sorry not used to formatting code on here
The __invoke method means I can use my apps AS closures which is totally sweet.
Edit: Done - BSD 2-clause.
You should evolve this into a solid/flexible micro framework.
It will be exciting to see how Ham comes up. Will try to use it in some hobby project.
It helps when you need it and lets you do things your way at the same time.
The only caveat is there's basically only one guy developing it.
The most important one is that everything is done within an $app context. This means that theoretically you could build two apps, and I later will be creating functionality to "mount" apps on paths, like in Flask Blueprints.
Second one is that I am going to be extending it (but keeping it lean) with other microframework-y goodness, patterns, ways to use PHP's native stuff in elegant ways, etc.
Essentially, klein is a router, but Ham will be a microframework, providing patterns for application development with it, extensions to work with other libraries, etc, ala Flask.
I'm definitely looking at Klein and seeing what I can learn from it, what can be done better, etc.
Slightly off-topic but besides Doctrine, RedBean and PHP ActiveRecord, is there any good and well-tested 'ORM' or some DataMapper/ActiveRecord implementation for PHP?
(I actually like PHP ActiveRecord but had some odd problems with it a while ago and well, there's 60+ issues on GitHub and no proper release since 2010.)
Really cool framework and congratulations on hacking this up in a night!
1/ It needs PHP >= 5.3 I guess (anonymous functions ?).
2/ It needs APC (use of apc_exists ?)
3/ I stopped at "Cannot use string offset as an array in ham\ham.php on line 42" (seems cache related).
On my laptop:
Xcache: ~700 microseconds APC: ~1,300 microseconds Dummy (no caching): ~3,712 microseconds
Routes are converted to regex and cached so this process
does not need to happen every request.
But, regex will again degrade performance.This - regex is slow to match anchored strings on a 120 character long URI - is a myth.
www.lithify.me
Your search for a modular and lightweight framework has come to an end.
$app->route('/pork', function($app) {
return "Delicious pork.";
});Yes, their are tradeoffs, but they don't overcome the benefits, not the least of which is simplicity.