keeping it small: getting to know the slim micro framework
TRANSCRIPT
Keeping it smallGetting to know the Slim micro framework
@JeremyKendall
Jeremy Kendall
Jeremy Kendall
I love to code
Jeremy Kendall
I love to code
I’m terribly forgetful
Jeremy Kendall
I love to code
I’m terribly forgetful
I take pictures
Jeremy Kendall
I love to code
I’m terribly forgetful
I take pictures
I work at OpenSky
Micro framework?
Micro framework?
Concise codebase
Micro framework?
Concise codebase
Clear codebase
Micro framework?
Concise codebase
Clear codebase
Addresses a small set of use cases
Micro framework?
Concise codebase
Clear codebase
Addresses a small set of use cases
Addresses those use cases well
What is Slim?
What is Slim?
Inspired by Sinatra
What is Slim?
Inspired by Sinatra
Favors cleanliness over terseness
What is Slim?
Inspired by Sinatra
Favors cleanliness over terseness
Favors common cases over edge cases
Installing Slim
RTFM
RTFM ;-)
Don’t forget .htaccess!
RewriteEngine OnRewriteCond %{REQUEST_FILENAME} !-fRewriteRule ^ index.php [QSA,L]
http://docs.slimframework.com/pages/routing-url-rewriting/
Hello world<?php
require '../vendor/autoload.php';
$app = new \Slim\Slim();
$app->get('/hello/:name', function ($name) { echo "Hello, $name";});
$app->run();
Hello world<?php
require '../vendor/autoload.php';
$app = new \Slim\Slim();
$app->get('/hello/:name', function ($name) { echo "Hello, $name";});
$app->run();
Hello world<?php
require '../vendor/autoload.php';
$app = new \Slim\Slim();
$app->get('/hello/:name', function ($name) { echo "Hello, $name";});
$app->run();
Hello world<?php
require '../vendor/autoload.php';
$app = new \Slim\Slim();
$app->get('/hello/:name', function ($name) { echo "Hello, $name";});
$app->run();
Hello world<?php
require '../vendor/autoload.php';
$app = new \Slim\Slim();
$app->get('/hello/:name', function ($name) { echo "Hello, $name";});
$app->run();
Let’s look at a Slim application
Flaming Archer
Flaming Archer
wat
“Great repository names are short and memorable. Need inspiration? How about flaming-archer.”
Flaming Archer
Flaming ArcherPhoto 365 project
Flaming ArcherPhoto 365 project
Built in 4 days (Saturday through Tuesday)
Flaming ArcherPhoto 365 project
Built in 4 days (Saturday through Tuesday)
Basic application — a few bells, no whistles
Flaming ArcherPhoto 365 project
Built in 4 days (Saturday through Tuesday)
Basic application — a few bells, no whistles
Routing
Flaming ArcherPhoto 365 project
Built in 4 days (Saturday through Tuesday)
Basic application — a few bells, no whistles
Routing
Twig views
Flaming ArcherPhoto 365 project
Built in 4 days (Saturday through Tuesday)
Basic application — a few bells, no whistles
Routing
Twig views
Middleware
4 views
phploc --exclude vendor,tests,templates .
phploc 1.6.4 by Sebastian Bergmann.
Directories: 7Files: 13
Lines of Code (LOC): 876 Cyclomatic Complexity / Lines of Code: 0.04Comment Lines of Code (CLOC): 272Non-Comment Lines of Code (NCLOC): 604
Configuration
return array( 'slim' => array( 'templates.path' => __DIR__ . '/templates', 'log.level' => 4, 'log.enabled' => true, 'log.writer' => new Slim\Extras\Log\DateTimeFileWriter( array( 'path' => __DIR__ . '/logs', 'name_format' => 'y-m-d' ) ) ), 'twig' => array( // . . . ), 'cookies' => array( // . . . ), 'flickr.api.key' => 'FLICKR API KEY', 'pdo' => array( // . . . ));
return array( 'slim' => array( 'templates.path' => __DIR__ . '/templates', 'log.level' => 4, 'log.enabled' => true, 'log.writer' => new Slim\Extras\Log\DateTimeFileWriter( array( 'path' => __DIR__ . '/logs', 'name_format' => 'y-m-d' ) ) ), 'twig' => array( // . . . ), 'cookies' => array( // . . . ), 'flickr.api.key' => 'FLICKR API KEY', 'pdo' => array( // . . . ));
Slim
return array( 'slim' => array( 'templates.path' => __DIR__ . '/templates', 'log.level' => 4, 'log.enabled' => true, 'log.writer' => new Slim\Extras\Log\DateTimeFileWriter( array( 'path' => __DIR__ . '/logs', 'name_format' => 'y-m-d' ) ) ), 'twig' => array( // . . . ), 'cookies' => array( // . . . ), 'flickr.api.key' => 'FLICKR API KEY', 'pdo' => array( // . . . ));
Slim
Views
return array( 'slim' => array( 'templates.path' => __DIR__ . '/templates', 'log.level' => 4, 'log.enabled' => true, 'log.writer' => new Slim\Extras\Log\DateTimeFileWriter( array( 'path' => __DIR__ . '/logs', 'name_format' => 'y-m-d' ) ) ), 'twig' => array( // . . . ), 'cookies' => array( // . . . ), 'flickr.api.key' => 'FLICKR API KEY', 'pdo' => array( // . . . ));
Slim
Views
Cookies
return array( 'slim' => array( 'templates.path' => __DIR__ . '/templates', 'log.level' => 4, 'log.enabled' => true, 'log.writer' => new Slim\Extras\Log\DateTimeFileWriter( array( 'path' => __DIR__ . '/logs', 'name_format' => 'y-m-d' ) ) ), 'twig' => array( // . . . ), 'cookies' => array( // . . . ), 'flickr.api.key' => 'FLICKR API KEY', 'pdo' => array( // . . . ));
Slim
Views
Cookies
My stuff
$config = require_once __DIR__ . '/../config.php';
// Prepare app$app = new Slim\Slim($config['slim']);
$config = require_once __DIR__ . '/../config.php';
// Prepare app$app = new Slim\Slim($config['slim']);
Config array goes here
Routing
Routing
$app->get('/', function () use ($app, $service) { $images = $service->findAll(); $app->render('index.html', array('images' => $images)); });
Routing
$app->get('/', function () use ($app, $service) { $images = $service->findAll(); $app->render('index.html', array('images' => $images)); });
HTTP Method
Routing
$app->get('/', function () use ($app, $service) { $images = $service->findAll(); $app->render('index.html', array('images' => $images)); });
HTTP Method Resource URI
Routing
$app->get('/', function () use ($app, $service) { $images = $service->findAll(); $app->render('index.html', array('images' => $images)); });
HTTP Method Resource URI Anonymous Function
Routing
$app->get('/', function () use ($app, $service) { $images = $service->findAll(); $app->render('index.html', array('images' => $images)); });
Routing
$app->get('/', function () use ($app, $service) { $images = $service->findAll(); $app->render('index.html', array('images' => $images)); });
Grabs all the pics
Routing
$app->get('/', function () use ($app, $service) { $images = $service->findAll(); $app->render('index.html', array('images' => $images)); });
Grabs all the pics
Passes array of image data to index.html
GET
$app->get('/:day', function($day) use ($app, $service) { $image = $service->find($day); if (!$image) { $app->notFound(); }
$app->render('images.html', $image); })->conditions(array('day' => '([1-9]\d?|[12]\d\d|3[0-5]\d|36[0-6])'));
GET
$app->get('/:day', function($day) use ($app, $service) { $image = $service->find($day); if (!$image) { $app->notFound(); }
$app->render('images.html', $image); })->conditions(array('day' => '([1-9]\d?|[12]\d\d|3[0-5]\d|36[0-6])'));
URL parameter
GET
$app->get('/:day', function($day) use ($app, $service) { $image = $service->find($day); if (!$image) { $app->notFound(); }
$app->render('images.html', $image); })->conditions(array('day' => '([1-9]\d?|[12]\d\d|3[0-5]\d|36[0-6])'));
URL parameter... gets passed as an
argument
GET
$app->get('/:day', function($day) use ($app, $service) { $image = $service->find($day); if (!$image) { $app->notFound(); }
$app->render('images.html', $image); })->conditions(array('day' => '([1-9]\d?|[12]\d\d|3[0-5]\d|36[0-6])'));
URL parameter... gets passed as an
argument
Condition
GET
$app->get('/:day', function($day) use ($app, $service) { $image = $service->find($day); if (!$image) { $app->notFound(); }
$app->render('images.html', $image); })->conditions(array('day' => '([1-9]\d?|[12]\d\d|3[0-5]\d|36[0-6])'));
URL parameter... gets passed as an
argument
Condition 1 to 366
GET
$app->get('/:day', function($day) use ($app, $service) { $image = $service->find($day); if (!$image) { $app->notFound(); }
$app->render('images.html', $image); })->conditions(array('day' => '([1-9]\d?|[12]\d\d|3[0-5]\d|36[0-6])'));
404!
POST (with redirect)
$app->post('/admin/add-photo', function() use ($app, $service) { $data = $app->request()->post(); $service->save($data); $app->redirect('/admin'); });
POST (with redirect)
$app->post('/admin/add-photo', function() use ($app, $service) { $data = $app->request()->post(); $service->save($data); $app->redirect('/admin'); });
$_POST data is in the request object
POST (with redirect)
$app->post('/admin/add-photo', function() use ($app, $service) { $data = $app->request()->post(); $service->save($data); $app->redirect('/admin'); });
$_POST data is in the request object
302 Redirect
Multiple methods
$app->map('/login', function() { // Login })->via('GET', 'POST');
Multiple methods
$app->map('/login', function() { // Login })->via('GET', 'POST');
Not an HTTP Method
Multiple methods
$app->map('/login', function() { // Login })->via('GET', 'POST');
Not an HTTP Method
via() is the awesome sauce
Logging and flash messaging
$app->post('/admin/clear-cache', function() use ($app) {
$log = $app->getLog(); $cleared = null; $clear = $app->request()->post('clear');
if ($clear == 1) { if (apc_clear_cache('user')) { $cleared = 'Cache was successfully cleared!'; } else { $cleared = 'Cache was not cleared!'; $log->error('Cache not cleared'); } }
$app->flash('cleared', $cleared); $app->redirect('/admin'); });
$app->post('/admin/clear-cache', function() use ($app) {
$log = $app->getLog(); $cleared = null; $clear = $app->request()->post('clear');
if ($clear == 1) { if (apc_clear_cache('user')) { $cleared = 'Cache was successfully cleared!'; } else { $cleared = 'Cache was not cleared!'; $log->error('Cache not cleared'); } }
$app->flash('cleared', $cleared); $app->redirect('/admin'); });
Get the log from $app
$app->post('/admin/clear-cache', function() use ($app) {
$log = $app->getLog(); $cleared = null; $clear = $app->request()->post('clear');
if ($clear == 1) { if (apc_clear_cache('user')) { $cleared = 'Cache was successfully cleared!'; } else { $cleared = 'Cache was not cleared!'; $log->error('Cache not cleared'); } }
$app->flash('cleared', $cleared); $app->redirect('/admin'); });
Get the log from $app
Error!
$app->post('/admin/clear-cache', function() use ($app) {
$log = $app->getLog(); $cleared = null; $clear = $app->request()->post('clear');
if ($clear == 1) { if (apc_clear_cache('user')) { $cleared = 'Cache was successfully cleared!'; } else { $cleared = 'Cache was not cleared!'; $log->error('Cache not cleared'); } }
$app->flash('cleared', $cleared); $app->redirect('/admin'); });
Get the log from $app
Error!
Flash message available in the next request.
Middleware
“. . . a Slim application can have middleware that may inspect, analyze, or modify the application environment, request, and response before and/or after the Slim application is invoked.”
http://docs.slimframework.com/pages/middleware-overview/
Hooks
slim.before
Hooks
slim.before
slim.before.router
Hooks
slim.before
slim.before.router
slim.before.dispatch
Hooks
slim.before
slim.before.router
slim.before.dispatch
slim.after.dispatch
Hooks
slim.before
slim.before.router
slim.before.dispatch
slim.after.dispatch
slim.after.router
Hooks
slim.before
slim.before.router
slim.before.dispatch
slim.after.dispatch
slim.after.router
slim.after
Hooks
slim.before
slim.before.router
slim.before.dispatch
slim.after.dispatch
slim.after.router
slim.after
Hooks
class MyMiddleware extends \Slim\Middleware{ public function call() { //The Slim application $app = $this->app;
//The Environment object $env = $app->environment();
//The Request object $req = $app->request();
//The Response object $res = $app->response();
//Optionally call the next middleware $this->next->call(); }}
class MyMiddleware extends \Slim\Middleware{ public function call() { //The Slim application $app = $this->app;
//The Environment object $env = $app->environment();
//The Request object $req = $app->request();
//The Response object $res = $app->response();
//Optionally call the next middleware $this->next->call(); }}
Extend this
class MyMiddleware extends \Slim\Middleware{ public function call() { //The Slim application $app = $this->app;
//The Environment object $env = $app->environment();
//The Request object $req = $app->request();
//The Response object $res = $app->response();
//Optionally call the next middleware $this->next->call(); }}
Extend this
Define call()
class MyMiddleware extends \Slim\Middleware{ public function call() { //The Slim application $app = $this->app;
//The Environment object $env = $app->environment();
//The Request object $req = $app->request();
//The Response object $res = $app->response();
//Optionally call the next middleware $this->next->call(); }}
Extend this
Define call()
Inspect, analyze, and modify!
class MyMiddleware extends \Slim\Middleware{ public function call() { //The Slim application $app = $this->app;
//The Environment object $env = $app->environment();
//The Request object $req = $app->request();
//The Response object $res = $app->response();
//Optionally call the next middleware $this->next->call(); }}
Extend this
Define call()
On to the next!
Inspect, analyze, and modify!
Middleware + Hooks = WIN
Navigation example
namespace Tsf\Middleware;
use \Zend\Authentication\AuthenticationService;
class Navigation extends \Slim\Middleware{
/** * @var \Zend\Authentication\AuthenticationService */ private $auth;
public function __construct(AuthenticationService $auth) { $this->auth = $auth; }
public function call() { // . . . }
}
namespace Tsf\Middleware;
use \Zend\Authentication\AuthenticationService;
class Navigation extends \Slim\Middleware{
/** * @var \Zend\Authentication\AuthenticationService */ private $auth;
public function __construct(AuthenticationService $auth) { $this->auth = $auth; }
public function call() { // . . . }
}
extends
namespace Tsf\Middleware;
use \Zend\Authentication\AuthenticationService;
class Navigation extends \Slim\Middleware{
/** * @var \Zend\Authentication\AuthenticationService */ private $auth;
public function __construct(AuthenticationService $auth) { $this->auth = $auth; }
public function call() { // . . . }
}
Constructor injection FTW
extends
public function call(){ $app = $this->app; $auth = $this->auth; $req = $app->request();
$home = array('caption' => 'Home', 'href' => '/'); $admin = array('caption' => 'Admin', 'href' => '/admin'); $login = array('caption' => 'Login', 'href' => '/login'); $logout = array('caption' => 'Logout', 'href' => '/logout'); if ($auth->hasIdentity()) { $navigation = array($home, $admin, $logout); } else { $navigation = array($home, $login); }
// . . .}
public function call(){ $app = $this->app; $auth = $this->auth; $req = $app->request();
$home = array('caption' => 'Home', 'href' => '/'); $admin = array('caption' => 'Admin', 'href' => '/admin'); $login = array('caption' => 'Login', 'href' => '/login'); $logout = array('caption' => 'Logout', 'href' => '/logout'); if ($auth->hasIdentity()) { $navigation = array($home, $admin, $logout); } else { $navigation = array($home, $login); }
// . . .}
Arrays of nav items
public function call(){ $app = $this->app; $auth = $this->auth; $req = $app->request();
$home = array('caption' => 'Home', 'href' => '/'); $admin = array('caption' => 'Admin', 'href' => '/admin'); $login = array('caption' => 'Login', 'href' => '/login'); $logout = array('caption' => 'Logout', 'href' => '/logout'); if ($auth->hasIdentity()) { $navigation = array($home, $admin, $logout); } else { $navigation = array($home, $login); }
// . . .}
Arrays of nav items
Nav differs based on auth status
public function call(){ // . . .
$this->app->hook('slim.before.router', function () use (...) {
foreach ($navigation as &$link) { if ($link['href'] == $req->getPath()) { $link['class'] = 'active'; } else { $link['class'] = ''; } }
$app->view() ->appendData(array('navigation' => $navigation)); } );
$this->next->call();}
public function call(){ // . . .
$this->app->hook('slim.before.router', function () use (...) {
foreach ($navigation as &$link) { if ($link['href'] == $req->getPath()) { $link['class'] = 'active'; } else { $link['class'] = ''; } }
$app->view() ->appendData(array('navigation' => $navigation)); } );
$this->next->call();}
Delicious hook goodness
public function call(){ // . . .
$this->app->hook('slim.before.router', function () use (...) {
foreach ($navigation as &$link) { if ($link['href'] == $req->getPath()) { $link['class'] = 'active'; } else { $link['class'] = ''; } }
$app->view() ->appendData(array('navigation' => $navigation)); } );
$this->next->call();}
Delicious hook goodness
Match dispatched path
public function call(){ // . . .
$this->app->hook('slim.before.router', function () use (...) {
foreach ($navigation as &$link) { if ($link['href'] == $req->getPath()) { $link['class'] = 'active'; } else { $link['class'] = ''; } }
$app->view() ->appendData(array('navigation' => $navigation)); } );
$this->next->call();}
Delicious hook goodness
Match dispatched path
Append $navigation to
view
public function call(){ // . . .
$this->app->hook('slim.before.router', function () use (...) {
foreach ($navigation as &$link) { if ($link['href'] == $req->getPath()) { $link['class'] = 'active'; } else { $link['class'] = ''; } }
$app->view() ->appendData(array('navigation' => $navigation)); } );
$this->next->call();}
Delicious hook goodness
Match dispatched path
Append $navigation to
viewOn to the next!
Views
Two great tastes that taste great together
Twig
Twig
Concise
Twig
Concise
Template oriented
Twig
Concise
Template oriented
Fast
Twig
Concise
Template oriented
Fast
Multiple inheritance
Twig
Concise
Template oriented
Fast
Multiple inheritance
Blocks
Twig
Concise
Template oriented
Fast
Multiple inheritance
Blocks
Automatic escaping
layout.html and
index.html
layout.html
<title>{% block page_title %} {% endblock %}</title>
<ul class="nav"> {% for link in navigation %} <li class="{{link.class}}"> <a href="{{link.href}}">{{link.caption}}</a> </li> {% endfor %}</ul>
<h1>365 Days of Photography</h1><h3>Photographer: Jeremy Kendall</h3>{% block content %} {% endblock %}<hr />
index.html
{% extends 'layout.html' %}
{% block page_title %}365.jeremykendall.net{% endblock %}
{% block content %}{% for image in images %}<div class="row"> <div class="span6"> <h2><a href="/{{image.day}}">{{image.day}}/365</a></h2> <p> <a href="/{{image.day}}"> <img src="{{image.sizes.size.5.source}}" /> </a> </p> <p>Posted {{image.posted|date("m/d/Y")}}</p> </div></div>{% else %}<p>No images in project</p>{% endfor %}{% endblock %}
{% extends 'layout.html' %}
{% block page_title %}365.jeremykendall.net{% endblock %}
{% block content %}{% for image in images %}<div class="row"> <div class="span6"> <h2><a href="/{{image.day}}">{{image.day}}/365</a></h2> <p> <a href="/{{image.day}}"> <img src="{{image.sizes.size.5.source}}" /> </a> </p> <p>Posted {{image.posted|date("m/d/Y")}}</p> </div></div>{% else %}<p>No images in project</p>{% endfor %}{% endblock %}
extends
{% extends 'layout.html' %}
{% block page_title %}365.jeremykendall.net{% endblock %}
{% block content %}{% for image in images %}<div class="row"> <div class="span6"> <h2><a href="/{{image.day}}">{{image.day}}/365</a></h2> <p> <a href="/{{image.day}}"> <img src="{{image.sizes.size.5.source}}" /> </a> </p> <p>Posted {{image.posted|date("m/d/Y")}}</p> </div></div>{% else %}<p>No images in project</p>{% endfor %}{% endblock %}
extends
<title />
{% extends 'layout.html' %}
{% block page_title %}365.jeremykendall.net{% endblock %}
{% block content %}{% for image in images %}<div class="row"> <div class="span6"> <h2><a href="/{{image.day}}">{{image.day}}/365</a></h2> <p> <a href="/{{image.day}}"> <img src="{{image.sizes.size.5.source}}" /> </a> </p> <p>Posted {{image.posted|date("m/d/Y")}}</p> </div></div>{% else %}<p>No images in project</p>{% endfor %}{% endblock %}
extends
<title />
{% extends 'layout.html' %}
{% block page_title %}365.jeremykendall.net{% endblock %}
{% block content %}{% for image in images %}<div class="row"> <div class="span6"> <h2><a href="/{{image.day}}">{{image.day}}/365</a></h2> <p> <a href="/{{image.day}}"> <img src="{{image.sizes.size.5.source}}" /> </a> </p> <p>Posted {{image.posted|date("m/d/Y")}}</p> </div></div>{% else %}<p>No images in project</p>{% endfor %}{% endblock %}
extends
<title />iterator
{% extends 'layout.html' %}
{% block page_title %}365.jeremykendall.net{% endblock %}
{% block content %}{% for image in images %}<div class="row"> <div class="span6"> <h2><a href="/{{image.day}}">{{image.day}}/365</a></h2> <p> <a href="/{{image.day}}"> <img src="{{image.sizes.size.5.source}}" /> </a> </p> <p>Posted {{image.posted|date("m/d/Y")}}</p> </div></div>{% else %}<p>No images in project</p>{% endfor %}{% endblock %}
extends
<title />iterator
else
{% extends 'layout.html' %}
{% block page_title %}365.jeremykendall.net{% endblock %}
{% block content %}{% for image in images %}<div class="row"> <div class="span6"> <h2><a href="/{{image.day}}">{{image.day}}/365</a></h2> <p> <a href="/{{image.day}}"> <img src="{{image.sizes.size.5.source}}" /> </a> </p> <p>Posted {{image.posted|date("m/d/Y")}}</p> </div></div>{% else %}<p>No images in project</p>{% endfor %}{% endblock %}
extends
<title />iterator
elseformat
login.html
{% extends 'layout.html' %}
{% block page_title %}365.jeremykendall.net | Login{% endblock %}
{% block content %}<div class="row"> <div class="span4"> <h2>Login</h2> {% if flash.error %} <p style="color: red;">{{flash.error}}</p> {% endif %} <form name="login" id="login" class="well" method="post"> // Login form . . . </form> </div></div>
{% endblock %}
{% extends 'layout.html' %}
{% block page_title %}365.jeremykendall.net | Login{% endblock %}
{% block content %}<div class="row"> <div class="span4"> <h2>Login</h2> {% if flash.error %} <p style="color: red;">{{flash.error}}</p> {% endif %} <form name="login" id="login" class="well" method="post"> // Login form . . . </form> </div></div>
{% endblock %}
The other viewswould be redundant
GOTO 0
Small but powerfulGOTO 0
Small but powerful
Excellent tools to write elegant code
GOTO 0
Small but powerful
Excellent tools to write elegant code
Routing, middleware & hooks, views
GOTO 0
Small but powerful
Excellent tools to write elegant code
Routing, middleware & hooks, views
I just scratched the surface
GOTO 0
Read
Slim: slimframework.com
Twig: twig.sensiolabs.org
Composer: getcomposer.org
MicroPHP Manifesto: microphp.org
Flaming Archer: http://git.io/rH0nrg
Questions?