PunyMVC

I love PHP frameworks like CakePHP, but sometimes they seem a little bloated. I’ve seen some “lightweight” frameworks like MicroMVC (which brags about being “less than 150kB”) and TinyMVC (which is still over 30kB zipped). For those of you who want a lighter-than-air, no-nonsense, zero-learning-curve, lightning-fast MVC framework in less than 4/5 of a kilobyte (uncompressed), I present PunyMVC:

<?function v($c,$a,$vars){if(is_file("views/$c/$a.php")){ob_start();include"views/$c/$a.php";$v=ob_get_contents();ob_end_clean();}return isset($v)?$v:";}function p($content,$l){if(isset($l)){include"layouts/$l.php";}else{echo$content;}}include'config/config.php';$b=strlen(dirname($_SERVER['PHP_SELF']));$u=explode('/',substr($_SERVER['REDIRECT_URL'],$b>1?$b+1:1));if(!($c=$u[0])){include'pages/index.php';die();}elseif(!preg_match('/\W/',$c.isset($u[1])?isset($u[1]):'')&&is_file("controllers/$c.php")){include"controllers/$c.php";$d=$c.'Controller';$a=isset($u[1])&&!empty($u[1])?$u[1]:'index';if(class_exists($d)&&in_array($a,get_class_methods($d))){$o=new$d;p(v($c,$a,$o->$a(array_slice($u,2))),isset($o->layout)?$o->layout:null);die();}}header('HTTP/1.1 404 Not Found');include'pages/404.php';

Yep, that’s it. Of course, I had to minify the code in order to squash it down to under a kilobyte; if you scroll to the bottom of this post, you can download a copy of the developer-friendly unminified version (which includes all of the following sample code as well). Anyway, here’s how to use it:

  • Step 1: Save the code blob above as index.php and place it in your webroot.
  • Step 2: Make an .htaccess file that will tell Apache to filter all URLs through PunyMVC (unless a file exists):
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . index.php [L]
  • Step 3: Make some subdirectories:

$ mkdir config controllers layouts models pages views

  • Step 4: Place an .htaccess file in each of the subdirectories for security. The files can just contain the line “deny from all”.
  • Step 5: Make a default index page:

$ echo “Welcome to my awesome website.” > pages/index.php

  • Step 6: Make a “404 not found” page:

$ echo “Page not found.” > pages/404.php

  • Step 7: Make a controller. Here’s a sample controller called Posts:
<?php // controllers/posts.php
class PostsController {
  public $layout = 'simple';
  private $Posts;

  public function __construct() {
    require_once 'models/posts.php';
    $this->Posts = new PostsModel();
  }

  public function index() {
    $posts = $this->Posts->find();
    return array('posts' => $posts);
  }

  public function view($args) {
    if (ctype_digit($args[0])) {
      $post = $this->Posts->find(array('_id' => intval($args[0])));
    }

    if (!isset($post) || empty($post)) {
      include 'pages/404.php';
      die();
    }

    return array('post' => $post[$args[0]]);
  }
}
  • Step 8: Make a model. Here’s a sample model (also called Posts). Just for fun, we’ll use MongoDB instead of a traditional RDBMS like MySQL:
<?php // models/posts.php
class PostsModel {
  private $mongo;
  private $mongoDb;
  private $posts;

  public function __construct() {
    $this->mongo = new Mongo(MONGO_SERVER);
    $this->mongoDb = $this->mongo->{MONGO_DATABASE};
    $this->posts = $this->mongoDb->posts;
  }

  public function find($query = array(), $fields = array()) {
    # Very simple example, we just pass the query straight to Mongo
    return iterator_to_array($this->posts->find($query, $fields));
  }
}
  • Step 9: Make some views. Here are two sample views for the actions in our Posts controller:
<?php // views/posts/index.php ?>
<?php $dir = dirname($_SERVER['PHP_SELF']); ?>
Here are the posts:<br /><br />

<?php foreach ($vars['posts'] as $post) : ?>
  <?php $post_uri = sprintf('%s/posts/view/%u', $dir, $post['_id']) ?>
  <a href="<?php echo $post_uri ?>"><?php echo $post['title'] ?></a>
  by <?php echo $post['author'] ?><br />
<?php endforeach ?>
<?php // views/posts/view.php ?>
This is a single post:<br />
<br />
Title: <?php echo $vars['post']['title'] ?><br />
Author: <?php echo $vars['post']['author'] ?><br />
Content:<br />
<?php echo $vars['post']['content'] ?><br /><br /><br />
  • Step 10: Make a layout. Layouts are optional (but a good idea). We specified one in the controller, so we’ll make it:
<?php // layouts/simple.php ?>
<html>
 <head>
 </head>
 <body>
  <?php echo $content ?>
 </body>
</html>
  • Step 11: Make a configuration file. You can store anything you want here (I use it for database settings in the demo app):
<?php // config/config.php
define('MONGO_SERVER', 'mongodb://localhost:27017');
define('MONGO_DATABASE', 'punymvc_demo');
  • Step 12: We’re done with the app, but it won’t do much unless we have data. Set up a MongoDB server and dump some posts into a database:
> use punymvc_demo
> db.posts.insert({“_id”:1,”title”:”This is an awesome post”, “author”:”Dave Hensley”, “content”:”This is the first post on my PunyMVC demo blog.”})
> db.posts.insert({“_id”:2,”title”:”This is an even awesomer post”, “author”:”John Smith”, “content”:”Just kidding. Dave’s post was way better than mine.”})

You can download the full demo app here. (Note: The download version is not minified, and includes the license and phpDoc comments — but even with all that bloat, it’s still well under 4K compressed.)