Bullet: The Functional
PHP Micro-Framework
Vance Lucas • Co-Founder, Brightbit	

http://bulletphp.com
Who are You?
• Vance Lucas	

• http://vancelucas.com	

• @vlucas (for heckling)	

• PHP since 1999 (PHP3)	

• Brightbit	

• http://brightbit.com	

• Design, Development & Consulting for
web apps, mobile apps and APIs
History & Philosophy	

(Don't worry, it won't be as boring as school was)
MVC Frameworks
• I’ve created LOTS of MVC frameworks.	

• They all sucked.	

• Except maybe one.	

• Alloy Framework
• Released Feb. 2011	

• But it’s dead to me now…
“The same thing, only
different”
“Lightweight”

“Flexible”

“Fast”

“Simple”
“Modular”

“Artisan”
“The fool hath said in
his heart, There is no
better architectural
pattern than MVC”
* may not be exact quote
“I don't like MVC because that's
not how the web works.
Symfony2 is an HTTP framework;
it is a Request/Response
framework. That's the big deal.”
Fabien Potencier	

http://fabien.potencier.org/article/49/what-is-symfony2	

October 25, 2011
Philosophy
• Do more with less (code)	

• Low cognitive overhead/complexity	

• Embrace HTTP	

• Leverage raw PHP without introducing too
many “framework concepts”	


• Only PHP knowledge should be enough	


• Shouldn’t have to “fight the framework”	

• “Micro” != No Structure
“SharedEventManager”
“PluginBroker”

“RouteStack”

“TemplateMapResolver”
“AggregateResolver”
“DefaultListenerAggregate”
“RouteNotFoundStrategy”
“TemplatePathStack”
What is Bullet?
Well, it’s a Micro-framework for starters…
Main Concepts
• Micro-framework	

•

URL Routing, Request, Response, Templates	


• Built around HTTP and defined URIs	

• Parses one URI segment at a time	

• Declarative, functional-style nested routing	

• Leverages closures for structure and scope	

• Less repetitive code, cleaner routes
Guiding Rules
• Only one path segment at a time, and only
Closures can be used	


• Response must be explicitly returned	

• Path must be fully consumed (or error)	

• Handlers for different behavior:	

• Path, Param, Method, Format	

• Method and format handlers only run
when path has been fully consumed
Show me some code!	

!

GET /posts/42
// Bullet index file!
define('BULLET_ROOT', dirname(__DIR__));!
define('BULLET_APP_ROOT', BULLET_ROOT . '/app/');!
define('BULLET_SRC_ROOT', BULLET_APP_ROOT . '/src/');!
!
// Composer Autoloader!
$loader = require BULLET_ROOT . '/vendor/autoload.php';!
!
// Bullet App!
$app = new BulletApp(require BULLET_APP_ROOT . 'config.php');!
$request = new BulletRequest();!
!
// Common include!
require BULLET_APP_ROOT . '/common.php';!
!
// Require all paths/routes!
$routesDir = BULLET_APP_ROOT . '/routes/';!
require $routesDir . 'index.php';!
require $routesDir . 'posts.php';!
require $routesDir . 'events.php';!
require $routesDir . 'users.php';!
!
// Response!
echo $app->run($request);
Bullet Routing	

$app->path('posts', function($req) {!
// Param!
$this->param('int', function($req, $id) {!
$post = Post::find($id);!
check_user_acl_for($post);!
!

!
!
!
!
!
!
!

!
!
!
!
!
!
!

!
!
!
!
!
!

!
!
!
!
!
!

});!
});

// Method!
$this->get(function($req) use($post) {!
// Format!
! ! $this->format('json', function() use($post) {!
! ! ! ! return $post->toArray();!
! ! });!
! ! $this->format('html', function() use($post) {!
! ! ! ! return $this->template('html', …);!
! ! });!
});!
Quick Code
Comparison
Typical Micro-Framework
$app->get('/posts/:id', function($id) use($app) {!
$post = Post::find($id);!
check_user_acl_for($post);!
!
if(is_json()) {!
header("Content-Type: application/json");!
echo json_encode($result);!
exit;!
}!
!

$app->render('posts/view', compact('post'));!
});!
Typical MVC Controller
class BlogController extends BaseController {!
public function getView($slug)!
{!
// Get this blog post data!
$post = $this->post->where('slug', '=', $slug)->first();!

!

!

// Check if the blog post exists!
if (is_null($post)) {!
return App::abort(404);!
}!

// Show the page!
return View::make('site/blog/view_post', compact('post', 'comments',
'canComment'));!
}!
}
Bullet Closure Context	

$app->path('posts', function($req) {!
$this->param('int', function($req, $id) {!
$post = Post::find($id);!
check_user_acl_for($post);!
!

// View (GET)!
$this->get(function($req) use($post) {!
// ...!
});!
!

// Delete!
$this->delete(function($req) use($post) {!
$post->delete();!
// ...!
});!
});!
});
Bullet Route Handlers
Path Handlers
$app->resource('posts', function($request) {!
// ...!
});!
!

$app->path('posts', function($request) {!
// ...!
});!
!

$app->path(['posts', 'articles'], function($req) {!
// ...!
});
Path Handlers
• Return 404 File Not Found if request path
not found	


• Can be nested as deep as you want	

• /admin/articles/3/comments
Param Handlers
$app->param('int', function($request, $id) {!
// ...!
});!
!

$app->param('slug', function($request, $slug) {!
// ...!
});!
!

// CUSTOM alphanumeric handler (returns boolean)!
$app->registerParamType('alphanum',function($value) {!
return ctype_alnum($value);!
});!
$app->param('alphanum', function($request, $alnum) {!
// ...!
});
Param Handlers
• Test function	

• true or scalar value executes route	

• false skips route	

• Value passed in as extra parameter to
handler closure
Method Handlers
$app->resource('articles', function($request) {!
$this->get(function($request) {!
// ...!
});!
!

$this->post(function($request) {!
// ...!
});!
!

$this->delete(function($request) {!
// ...!
});!
});
Method Handlers

• Return 405 Method Not Allowed if
request method not found
Format Handlers
$app->resource('articles', function($request) {!
$this->get(function($request) {!
$this->format(‘json', function($request) {!
// ...!
});!
$this->format(‘html', function($request) {!
// ...!
});!
});!
});
Format Handlers

• Return 406 Not Acceptable if request
format not found
Other Handlers
$app->domain(‘vancelucas.com', function($request) {!
// ...!
});!
!

$app->subdomain(‘api', function($request) {!
// ...!
});
Return Types
• String (“hello world”)	

• Integer (201 - Sends HTTP status code)	

• Boolean False (404 error)	

• Array (auto json_encode + headers)	

• BulletResponse instance	

• Custom obj. (w/custom response handler)
Building the URL you
want should be easy
$app->path('admin', function($req) use($app) {!
some_acl_check__that_throws_exception_if_not();!
!

require 'posts.php'; // For /admin/posts ...!
require 'events.php'; // For /admin/events ...!
require 'comments.php'; // For /admin/comments ...!
});
…And Links Too
// RELATIVE url!
// /posts/25/comments/57,!
// /events/9/comments/57,!
// /comments/57!
echo $app->url('./comments/' . $comment->id);!
!

// ROOT url (always /comments/57)!
echo $app->url('/comments/' . $comment->id);!
Recommended Setup

http://bulletphp.com/docs/organization/
Events
• Global: ‘before’, and ‘after’	

• Dynamic	

• [http_status_code] - 404, 500, etc.	

• [response_format] - json, html, etc.	

• [exception_class] - exception class name
like “InvalidArgumentException” or just
“Exception” to catch all exceptions
HTTP Error Handling

$app->on(404, function($req, $res){!
$response->content($app->template('errors/404'));!
});!
Exception Handling
$app->on('Exception', function($req, $res, Exception $e) {!
if($req->format() === 'json') {!
$data = array(!
'exception' => get_class($e),!
'message' => $e->getMessage()!
);!
if(BULLET_ENV !== 'production') {!
$data['file'] = $e->getFile();!
$data['line'] = $e->getLine();!
$data['trace'] = $e->getTrace();!
}!
!
} else {!
$data = $app->template('errors/exception', ['e' => $e]);!
}!
$res->content($data);!
});!
Nested Sub Requests
$app = new BulletApp();!
$app->path('foo', function($request) {!
return "foo";!
});!
$app->path('bar', function($request) {!
$res = $this->run('GET', '/foo'); // `BulletResponse`!
return $res->content() . 'bar';!
});!
echo $app->run('GET', 'bar'); // output => 'foobar'!
Getting Started
http://bulletphp.com	

!

https://github.com/vlucas/bulletphp	

!

Skeleton App (basic setup / starting point)	

https://github.com/vlucas/bulletphp-skeleton	

!

Obligatory blog example
https://github.com/vlucas/bulletphp-blog-example
MVC Framework	

Anti-Patterns
Some more controversial than others
“REST Controller”	

vs	

“Base Controller”
Can’t use basic PHP
knowledge to change
the flow of your
application
!

$this->forward('someOtherAction' . $params);!
$this->beforeFilter('auth', array(!
'except' => 'getLogin'!
));!
/:controller/:action/:id
$response->setStatusCode(Response::HTTP_NOT_FOUND);!
!

class Response {!
// ...!
const HTTP_CONTINUE = 100;!
const HTTP_SWITCHING_PROTOCOLS = 101;!
const HTTP_PROCESSING = 102;!
const HTTP_OK = 200;!
const HTTP_CREATED = 201;!
const HTTP_ACCEPTED = 202;!
const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;!
const HTTP_NO_CONTENT = 204;!
// ...!
}!

HttpFoundation Component Docs
Symfony/Component/HttpFoundation/Response.php
class Response {!
// ...!
const STATUS_CODE_CUSTOM = 0;!
const STATUS_CODE_100 = 100;!
const STATUS_CODE_101 = 101;!
const STATUS_CODE_102 = 102;!
const STATUS_CODE_200 = 200;!
const STATUS_CODE_201 = 201;!
const STATUS_CODE_202 = 202;!
const STATUS_CODE_203 = 203;!
const STATUS_CODE_204 = 204;!
// ...!
}!

Zend Framework 2 - Zend/Http/Response.php
Classes for Controllers
Questions?
@vlucas | vance@vancelucas.com	

!
!

Rate this talk!	

https://joind.in/10434

Bullet: The Functional PHP Micro-Framework