bullet: the functional php micro-framework
TRANSCRIPT
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”
“Simple”“Flexible”
“Artisan”
“Modular”
“Fast”
“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
“PluginBroker”
“TemplateMapResolver”
“AggregateResolver”
“RouteNotFoundStrategy”
“TemplatePathStack”
“RouteStack”
“SharedEventManager”
“DefaultListenerAggregate”
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 Bullet\App(require BULLET_APP_ROOT . 'config.php');!$request = new Bullet\Request();! !// 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)
• Bullet\Response 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 ...!});
// 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);!
…And Links Too
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 Bullet\App();!$app->path('foo', function($request) {! return "foo";!});!$app->path('bar', function($request) {! $res = $this->run('GET', '/foo'); // `Bullet\Response`! 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
Symfony/Component/HttpFoundation/Response.php
HttpFoundation Component Docs
$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;! // ...!}!
Zend Framework 2 - Zend/Http/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;! // ...!}!
Classes for Controllers