the zen of lithium

118
THE OF

Upload: nate-abele

Post on 15-Jan-2015

3.400 views

Category:

Technology


5 download

DESCRIPTION

"The Zen of Lithium" provides an overview of some of the philosophies behind the Lithium framework

TRANSCRIPT

Page 1: The Zen of Lithium

THE

OF

Page 2: The Zen of Lithium

• Former lead developer, CakePHP

• Co-founder & lead developer of Lithium for ~2 years

• Original BostonPHP framework bake-off champ!

• Twitter: @nateabele

Page 3: The Zen of Lithium

• Started as a series of test scripts on early dev builds of PHP 5.3

• Released as “Cake3” in July ‘09

• Spun off as Lithium in October ’09

• Based on 5 years’ experience developing a high-adoption web framework

Page 4: The Zen of Lithium

ARCHITECTURE

Page 5: The Zen of Lithium

Procedural Object-Oriented

Page 6: The Zen of Lithium

Procedural Object-Oriented

Page 7: The Zen of Lithium

PARADIGMS

Event-Driven

Aspect-Oriented

Declarative

Procedural

Functional

Object-Oriented

Page 8: The Zen of Lithium

PARADIGM HUBRIS

The Fall of Rome

Page 9: The Zen of Lithium
Page 10: The Zen of Lithium

+ $

Page 11: The Zen of Lithium

ZEND FRAMEWORK 1.5

Page 12: The Zen of Lithium

$transport = new Zend_Mail_Transport_Smtp('smtp.gmail.com', array( 'auth' => 'login', 'username' => 'foo', 'password' => 'bar', 'ssl' => 'ssl', 'port' => 465,)); $mailer = new Zend_Mail();$mailer->setDefaultTransport($transport);

Page 13: The Zen of Lithium

class Container {

public function getMailTransport() { return new Zend_Mail_Transport_Smtp('smtp.gmail.com', array( 'auth' => 'login', 'username' => 'root', 'password' => 'sekr1t', 'ssl' => 'ssl', 'port' => 465, )); } public function getMailer() { $mailer = new Zend_Mail(); $mailer->setDefaultTransport($this->getMailTransport()); return $mailer; }}

Page 14: The Zen of Lithium

class Container {

protected $parameters = array(); public function __construct(array $parameters = array()) { $this->parameters = $parameters; } public function getMailTransport() { return new Zend_Mail_Transport_Smtp('smtp.gmail.com', array( 'auth' => 'login', 'username' => $this->parameters['mailer.username'], 'password' => $this->parameters['mailer.password'], 'ssl' => 'ssl', 'port' => 465, )); } public function getMailer() { $mailer = new Zend_Mail(); $mailer->setDefaultTransport($this->getMailTransport()); return $mailer; }}

Page 15: The Zen of Lithium

$container = new Container(array( 'mailer.username' => 'root', 'mailer.password' => 'sekr1t', 'mailer.class' => 'Zend_Mail',));

$mailer = $container->getMailer();

Page 16: The Zen of Lithium

class Container extends sfServiceContainer {

static protected $shared = array(); protected function getMailTransportService() { return new Zend_Mail_Transport_Smtp('smtp.gmail.com', array( 'auth' => 'login', 'username' => $this['mailer.username'], 'password' => $this['mailer.password'], 'ssl' => 'ssl', 'port' => 465, )); } protected function getMailerService() { if (isset(self::$shared['mailer'])) { return self::$shared['mailer']; } $class = $this['mailer.class']; $mailer = new $class(); $mailer->setDefaultTransport($this->getMailTransportService()); return self::$shared['mailer'] = $mailer; }}

Page 17: The Zen of Lithium

sfServiceContainerAutoloader::register(); $sc = new sfServiceContainerBuilder(); $sc->register('mail.transport', 'Zend_Mail_Transport_Smtp')-> addArgument('smtp.gmail.com')-> addArgument(array( 'auth' => 'login', 'username' => '%mailer.username%', 'password' => '%mailer.password%', 'ssl' => 'ssl', 'port' => 465, ))->setShared(false); $sc->register('mailer', '%mailer.class%')-> addMethodCall('setDefaultTransport', array( new sfServiceReference('mail.transport') ));

Page 18: The Zen of Lithium

<?xml version="1.0" ?> <container xmlns="http://symfony-project.org/2.0/container"> <parameters> <parameter key="mailer.username">root</parameter> <parameter key="mailer.password">sekr1t</parameter> <parameter key="mailer.class">Zend_Mail</parameter> </parameters> <services> <service id="mail.transport" class="Zend_Mail_Transport_Smtp" shared="false"> <argument>smtp.gmail.com</argument> <argument type="collection"> <argument key="auth">login</argument> <argument key="username">%mailer.username%</argument> <argument key="password">%mailer.password%</argument> <argument key="ssl">ssl</argument> <argument key="port">465</argument> </argument> </service> <service id="mailer" class="%mailer.class%"> <call method="setDefaultTransport"> <argument type="service" id="mail.transport" /> </call> </service> </services></container>

Page 19: The Zen of Lithium

Dependency injection container

+ Service container

+ Service container builder

+ XML

Page 20: The Zen of Lithium

==

Page 21: The Zen of Lithium

mail()

Page 22: The Zen of Lithium
Page 23: The Zen of Lithium

All problems in computer science can be solved by another level of indirection. Except for the problem of too many layers of indirection.

THE MORAL

“”

— Butler Lampson / David Wheeler

Page 24: The Zen of Lithium

GREAT ARCHITECTURE

The Guggenheim Fallingwater

Page 25: The Zen of Lithium
Page 26: The Zen of Lithium

WHO WON?

Page 27: The Zen of Lithium

JET LI AS HOU YUANJIA

Jet Li’s Fearless

Page 28: The Zen of Lithium

BRUCE LEE

Page 29: The Zen of Lithium

JEET KUNE DOThe Way of the Intercepting Fist

Page 30: The Zen of Lithium

GOALS

• Understand a variety of paradigms & their strengths

• Respect context when choosing paradigms / techniques

• Be simple as possible (but no simpler)

Page 31: The Zen of Lithium

PROBLEMS

• Managing configuration

• Staying flexible

• Extending internals

• Easy things: easy; hard things: possible

Page 32: The Zen of Lithium

CONFIGURATION

Page 33: The Zen of Lithium

require dirname(__DIR__) . '/config/bootstrap.php';

echo lithium\action\Dispatcher::run( new lithium\action\Request());

webroot/index.php

Page 34: The Zen of Lithium

require __DIR__ . '/bootstrap/libraries.php';

require __DIR__ . '/bootstrap/errors.php';

require __DIR__ . '/bootstrap/cache.php';

require __DIR__ . '/bootstrap/connections.php';

require __DIR__ . '/bootstrap/action.php';

require __DIR__ . '/bootstrap/session.php';

require __DIR__ . '/bootstrap/g11n.php';

require __DIR__ . '/bootstrap/media.php';

require __DIR__ . '/bootstrap/console.php';

config/bootstrap.php

Page 35: The Zen of Lithium

config/bootstrap/libraries.php

use lithium\core\Libraries;

Libraries::add('lithium');

Libraries::add('app', array('default' => true));

Libraries::add('li3_docs');

Page 36: The Zen of Lithium

config/bootstrap/cache.php

use lithium\storage\Cache;

Cache::config(array( 'local' => array('adapter' => 'Apc'), 'distributed' => array( 'adapter' => 'Memcache', 'host' => '127.0.0.1:11211' ), 'default' => array('adapter' => 'File')));

Page 37: The Zen of Lithium

config/bootstrap/connections.php

use lithium\data\Connections;

Connections::config(array( 'default' => array( 'type' => 'MongoDb', 'database' => 'my_mongo_db' ), 'legacy' => array( 'type' => 'database', 'adapter' => 'MySql', 'login' => 'bobbytables', 'password' => 's3kr1t', 'database' => 'my_mysql_db' )));

Page 38: The Zen of Lithium

config/bootstrap/session.php

use lithium\security\Auth;

Auth::config(array( 'customer' => array( 'adapter' => 'Form', 'model' => 'Customers', 'fields' => array('email', 'password') ), 'administrator' => array( 'adapter' => 'Http', 'method' => 'digest', 'users' => array('nate' => 'li3') )));

Page 39: The Zen of Lithium

use lithium\storage\Cache;

Cache::config(array( 'default' => array( 'development' => array('adapter' => 'Apc'), 'production' => array( 'adapter' => 'Memcache', 'host' => '127.0.0.1:1121' ) )));

MULTIPLE ENVIRONMENTS?

Page 40: The Zen of Lithium

use lithium\storage\Cache;

Cache::config(array( 'default' => array( 'development' => array('adapter' => 'Apc'), 'production' => array( 'adapter' => 'Memcache', 'host' => '127.0.0.1:1121' ) )));

MULTIPLE ENVIRONMENTS?

Page 41: The Zen of Lithium

namespace lithium\net\http;

use lithium\core\Libraries;

class Service extends \lithium\core\Object {

protected $_classes = array( 'media' => 'lithium\net\http\Media', 'request' => 'lithium\net\http\Request', 'response' => 'lithium\net\http\Response', );

public function __construct(array $config = array()) { $defaults = array( 'scheme' => 'http', 'host' => 'localhost', // ... ); parent::__construct($config + $defaults); }

protected function _init() { // ... }}

Page 42: The Zen of Lithium

$service = new Service(array( 'scheme' => 'https', 'host' => 'web.service.com', 'username' => 'user', 'password' => 's3kr1t'));

Page 43: The Zen of Lithium

$service = new Service(array( 'scheme' => 'https', 'host' => 'web.service.com', 'username' => 'user', 'password' => 's3kr1t'));{ “lithium\\net\\http\\Service”: { “scheme”: “https”, “host”: “web.service.com”, “username”: “user”, “password”: “s3kr1t” }}

Page 44: The Zen of Lithium

$service = new Service(array( 'scheme' => 'https', 'host' => 'web.service.com', 'username' => 'user', 'password' => 's3kr1t', 'classes' => array( 'request' => 'my\custom\Request' )));

Page 45: The Zen of Lithium

FLEXIBILITY

Page 46: The Zen of Lithium

HELPERS

<?=$this->form->text('email'); ?>

Page 47: The Zen of Lithium

HELPERS

<?=$this->form->text('email'); ?>

<input type="text" name="email" id="MemberEmail" value="[email protected]" />

Page 48: The Zen of Lithium

HELPERS

<?=$this->form->field('name', array( 'wrap' => array('class' => 'wrapper'))); ?>

Page 49: The Zen of Lithium

HELPERS

<?=$this->form->field('name', array( 'wrap' => array('class' => 'wrapper'))); ?>

<div class="wrapper"> <label for="MemberName">Name</label> <input type="text" name="name" id="MemberName" /> <div class="error">You don't have a name?</div></div>

Page 50: The Zen of Lithium

HELPERS

<?=$this->form->field('name', array( 'wrap' => array('class' => 'item'), 'template' => '<li{:wrap}>{:error}{:label}{:input}</li>')); ?>

Page 51: The Zen of Lithium

HELPERS

<?=$this->form->field('name', array( 'wrap' => array('class' => 'item'), 'template' => '<li{:wrap}>{:error}{:label}{:input}</li>')); ?>

<li class="item"> <div class="error">You don't have a name?</div> <label for="MemberName">Name</label> <input type="text" name="name" id="MemberName" /></div>

Page 52: The Zen of Lithium

HELPERS

$this->form->config(array('templates' => array( 'field' => "<li{:wrap}>{:error}{:label}{:input}</li>")));

Page 53: The Zen of Lithium

HELPERS

<input type="text" name="email" id="MemberEmail" value="[email protected]" />

Page 54: The Zen of Lithium

HELPERS

$form = $this->form;

$this->form->config(array('attributes' => array( 'id' => function($method, $name, $options) use (&$form) { if ($method != 'text' && $method != 'select') { return; } $model = null;

if ($binding = $form->binding()) { $model = basename(str_replace('\\', '/', $binding->model())) . '_'; } return Inflector::underscore($model . $name); })));

Page 55: The Zen of Lithium

HELPERS

$form = $this->form;

$this->form->config(array('attributes' => array( 'id' => function($method, $name, $options) use (&$form) { if ($method != 'text' && $method != 'select') { return; } $model = null;

if ($binding = $form->binding()) { $model = basename(str_replace('\\', '/', $binding->model())) . '_'; } return Inflector::underscore($model . $name); })));

Page 56: The Zen of Lithium

HELPERS

<input type="text" name="email" id="member_email" value="[email protected]" />

Page 57: The Zen of Lithium

THE MEDIA CLASS

class WeblogController < ActionController::Base

def index @posts = Post.find :all

respond_to do |format| format.html format.xml { render :xml => @posts.to_xml } format.rss { render :action => "feed.rxml" } end endend

Page 58: The Zen of Lithium

THE MEDIA CLASS

class WeblogController < ActionController::Base

def index @posts = Post.find :all

respond_to do |format| format.html format.xml { render :xml => @posts.to_xml } format.rss { render :action => "feed.rxml" } end endend !

Page 59: The Zen of Lithium

THE MEDIA CLASS

<?php echo $javascript->object($data); ?>

Page 60: The Zen of Lithium

THE MEDIA CLASS

<?php echo $javascript->object($data); ?>!

Page 61: The Zen of Lithium

THE MEDIA CLASS

lithium\net\http\Media {

$formats = array( 'html' => array(...), 'json' => array(...), 'xml' => array(...), '...' );}

array( 'posts' => ...)

Page 62: The Zen of Lithium

THE MEDIA CLASS

lithium\net\http\Media {

$formats = array( 'html' => array(...), 'json' => array(...), 'xml' => array(...), '...' );}

array( 'posts' => ...)

Page 63: The Zen of Lithium

THE MEDIA CLASSMedia::type('mobile', array('text/html'), array( 'view' => 'lithium\template\View', 'paths' => array( 'template' => array( '{:library}/views/{:controller}/{:template}.mobile.php', '{:library}/views/{:controller}/{:template}.html.php', ), 'layout' => array( '{:library}/views/layouts/{:layout}.mobile.php', '{:library}/views/layouts/{:layout}.html.php', ), 'element' => array( '{:library}/views/elements/{:template}.mobile.php' '{:library}/views/elements/{:template}.html.php' ) ), 'conditions' => array('mobile' => true)));

Page 64: The Zen of Lithium

THE MEDIA CLASSMedia::type('mobile', array('text/html'), array( 'view' => 'lithium\template\View', 'paths' => array( 'template' => array( '{:library}/views/{:controller}/{:template}.mobile.php', '{:library}/views/{:controller}/{:template}.html.php', ), 'layout' => array( '{:library}/views/layouts/{:layout}.mobile.php', '{:library}/views/layouts/{:layout}.html.php', ), 'element' => array( '{:library}/views/elements/{:template}.mobile.php' '{:library}/views/elements/{:template}.html.php' ) ), 'conditions' => array('mobile' => true)));

Page 65: The Zen of Lithium

THE MEDIA CLASS

Media::type('ajax', array('text/html'), array( 'view' => 'lithium\template\View', 'paths' => array( 'template' => array( '{:library}/views/{:controller}/{:template}.ajax.php', '{:library}/views/{:controller}/{:template}.html.php', ), 'layout' => false, 'element' => array( '{:library}/views/elements/{:template}.ajax.php' '{:library}/views/elements/{:template}.html.php' ) ), 'conditions' => array('ajax' => true)));

Page 66: The Zen of Lithium

THE MEDIA CLASS

Media::type('ajax', array('text/html'), array( 'view' => 'lithium\template\View', 'paths' => array( 'template' => array( '{:library}/views/{:controller}/{:template}.ajax.php', '{:library}/views/{:controller}/{:template}.html.php', ), 'layout' => false, 'element' => array( '{:library}/views/elements/{:template}.ajax.php' '{:library}/views/elements/{:template}.html.php' ) ), 'conditions' => array('ajax' => true)));

Page 67: The Zen of Lithium

CONDITIONS?

'conditions' => array('ajax' => true)

==

$request->is('ajax')

==

$_SERVER['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'

Page 68: The Zen of Lithium

CONDITIONS?

$request->detect('iPhone', array('HTTP_USER_AGENT', '/iPhone/'));

$isiPhone = $request->is('iPhone');

$request->detect('custom', function($request) { if ($value = $request->env("HTTP_WHATEVER")) { // Do something with $value } return false;});

Page 69: The Zen of Lithium

ROUTING

Router::connect('/{:controller}/{:action}/{:id:[0-9]+}', array( 'id' => null));

new Route(array( 'template' => '/{:controller}/{:action}/{:id:[0-9]+}', 'pattern' => '@^(?:/(?P[^\\/]+))(?:/(?P[^\\/]+)?)?(?:/(?P[0-9]+)?)?$@', 'params' => array('id' => null, 'action' => 'index'), // ... 'subPatterns' => array('id' => '[0-9]+'), 'persist' => array('controller')));

Router::connect(new CustomRoute($params));

Page 70: The Zen of Lithium

ROUTE HANDLERS

Router::connect('/{:user}/{:controller}/{:action}');

Page 71: The Zen of Lithium

ROUTE HANDLERS

Router::connect('/{:user}/{:controller}/{:action}', array(), function($request) { if (!Users::count(array('conditions' => array('user' => $request->user)))) { return false; } return $request;});

Page 72: The Zen of Lithium

ROUTE HANDLERS

Router::connect('/{:user}/{:controller}/{:action}', array(), function($request) { if (!Users::count(array('conditions' => array('user' => $request->user)))) { return false; } return $request;});

Router::connect('/', array(), function($request) { if (Session::read('user')) { $location = 'Accounts::index'; } else { $location = 'Users::add'; } return new Response(array('status' => 302, 'location' => $location));});

Page 73: The Zen of Lithium

ROUTE HANDLERS

Router::connect( '/photos/view/{:id:[0-9a-f]{24}}.jpg', array(), function($request) { return new Response(array( 'headers' => array('Content-type' => 'image/jpeg'), 'body' => Photos::first($request->id)->file->getBytes() )); });

Page 74: The Zen of Lithium

MICRO-APPS

Router::connect('/posts.json', array(), function($request) { return new Response(array( 'headers' => array('Content-type' => 'application/json'), 'body' => Posts::all()->to('json') ));});

Router::connect('/posts/{:id}.json', array(), function($request) { return new Response(array( 'headers' => array('Content-type' => 'application/json'), 'body' => Posts::first($request->id)->to('json') ));});

Page 75: The Zen of Lithium

EXTENSIBILITY

Page 76: The Zen of Lithium

HELPERS

<?=$this->html->*() ?>

lithium\template\helper\Html

Page 77: The Zen of Lithium

HELPERS

<?=$this->html->*() ?>

lithium\template\helper\Html

app\extensions\helper\Html

Page 78: The Zen of Lithium

MODELS

namespace app\models;

class Posts extends \lithium\data\Model {

}

Page 79: The Zen of Lithium

MODELS

namespace app\models;

class Posts extends \lithium\data\Model {

protected $_meta = array( 'key' => 'custom_id', 'source' => 'custom_posts_table' 'connection' => 'legacy_mysql_db' );}

Page 80: The Zen of Lithium

MODELS

namespace app\models;

class Posts extends \lithium\data\Model {

protected $_meta = array( 'key' => array( 'custom_id', 'other_custom_id' ) );}

Page 81: The Zen of Lithium

MODELS

Posts::create(array( 'title' => 'My first post ever', 'body' => 'Wherein I extoll the virtues of Lithium'));

// ...

$post->save();

Page 82: The Zen of Lithium

MODELS

$post->tags = 'technology,PHP,news';$post->save();

// ...

foreach ($post->tags as $tag) { #FALE}

Page 83: The Zen of Lithium

MODELS

foreach ($post->tags() as $tag) { // ...}

namespace app\models;

class Posts extends \lithium\data\Model {

public function tags($entity) { return explode(',', $entity->tags); }}

Page 84: The Zen of Lithium

MODELS

foreach ($post->tags() as $tag) { // ...}

namespace app\models;

class Posts extends \lithium\data\Model {

public function tags($entity) { return explode(',', $entity->tags); }}

Page 85: The Zen of Lithium

MODELS

namespace app\models;

class Posts extends \lithium\data\Model {

public static function expire() { return static::update( array('expired' => true), array('updated' => array( '<=' => strtotime('3 months ago') )) ); }}

$didItWork = Posts::expire();

Page 86: The Zen of Lithium

ENTITIES & COLLECTIONS

$posts = Posts::findAllBySomeCondition();

Page 87: The Zen of Lithium

ENTITIES & COLLECTIONS

$posts = Posts::findAllBySomeCondition();

$posts->first(function($post) { return $post->published == true;});

$posts->each(function($post) { return $post->counter++;});

$ids = $posts->map(function($post) { return $post->id;});

Page 88: The Zen of Lithium

RELATIONSHIPS

namespace app\models;

class Posts extends \lithium\data\Model {

public $belongsTo = array('Users');}

Page 89: The Zen of Lithium

RELATIONSHIPS

namespace app\models;

class Posts extends \lithium\data\Model {

public $belongsTo = array('Author' => array( 'class' => 'Users' ));}

Page 90: The Zen of Lithium

RELATIONSHIPS

namespace app\models;

class Posts extends \lithium\data\Model {

public $belongsTo = array('Author' => array( 'class' => 'Users', 'conditions' => array('active' => true), 'key' => 'author_id' ));}

Page 91: The Zen of Lithium

RELATIONSHIPS

namespace app\models;

class Posts extends \lithium\data\Model {

public $belongsTo = array('Author' => array( 'class' => 'Users', 'conditions' => array('active' => true), 'key' => array( 'author_id' => 'id', 'other_key' => 'other_id' ) ));}

Page 92: The Zen of Lithium

NO HASANDBELONGSTOMANY!!

Page 93: The Zen of Lithium

DOCUMENT DATABASES

$post = Posts::create(array( 'title' => "New post", 'body' => "Something worthwhile to read", 'tags' => array('PHP', 'tech'), 'author' => array('name' => 'Nate')));

Page 94: The Zen of Lithium

DOCUMENT DATABASES

$posts = Posts::all(array('conditions' => array( 'tags' => array('PHP', 'tech'), 'author.name' => 'Nate')));

Page 95: The Zen of Lithium

DOCUMENT DATABASES

$ages = Users::all(array( 'group' => 'age', 'reduce' => 'function(obj, prev) { prev.count++; }', 'initial' => array('count' => 0)));

Page 96: The Zen of Lithium

THE QUERY API

$query = new Query(array( 'type' => 'read', 'model' => 'app\models\Post', 'fields' => array('Post.title', 'Post.body'), 'conditions' => array('Post.id' => new Query(array( 'type' => 'read', 'fields' => array('post_id'), 'model' => 'app\models\Tagging', 'conditions' => array('Tag.name' => array('foo', 'bar', 'baz')), )))));

Page 97: The Zen of Lithium

FILTERS

Page 98: The Zen of Lithium

$post = Posts::first($id);

Page 99: The Zen of Lithium

Posts::applyFilter('find', function($self, $params, $chain) { $key = // Make a cache key from $params['options']

if ($result = Cache::read('default', $key)) { return $result; } $result = $chain->next($self, $params, $chain);

Cache::write('default', $key, $result); return $result;});

Page 100: The Zen of Lithium

find()

caching

logging

Page 101: The Zen of Lithium

find()

caching

logging

Page 102: The Zen of Lithium

Posts::applyFilter('find', function($self, $params, $chain) { $key = // Make a cache key from $params['options']

if ($result = Cache::read('default', $key)) { return $result; } $result = $chain->next($self, $params, $chain);

Cache::write('default', $key, $result); return $result;});

Page 103: The Zen of Lithium

THE TALK OF THE TOWN

Page 104: The Zen of Lithium

CAN I USE IT IN PRODUCTION?

Page 105: The Zen of Lithium

GIMMEBAR.COM

Sean Coates

Page 106: The Zen of Lithium

MAPALONG.COM

Chris Shiflett

Andrei Zmievski

Page 107: The Zen of Lithium

TOTSY.COM

Mitch Pirtle

Page 108: The Zen of Lithium

. . .AND MANY OTHERS

Page 109: The Zen of Lithium

David Coallier• President, PEAR Group

• CTO, Echolibre / Orchestra.io

After looking at Lithium I’ve come to realize how far ahead it is compared to other frameworks from a technologist's point of view.

“”

Page 110: The Zen of Lithium

Helgi Þormar Þorbjörnsson

• Developer, PEAR Installer

• PEAR Core Dev, 8 years

It’s the f*****g epiphany of modern!“ ”

Page 111: The Zen of Lithium

Fahad Ibnay Heylaal

• Creator, Croogo CMS

I believe the future is in Lithium. give it time to grow, and the developers behind it are awesome.

“”

Page 112: The Zen of Lithium

1 .0?

Page 113: The Zen of Lithium

SO CLOSE!!

Page 114: The Zen of Lithium

TOMORROW...

Page 115: The Zen of Lithium

0.10

Page 116: The Zen of Lithium

THANKS!!

@nateabele

[email protected]

http://nateabele.com/

Find me later :

Page 118: The Zen of Lithium

PHOTO CREDITS

http://www.flickr.com/photos/mkebbe/28298461/

http://www.flickr.com/photos/josefeliciano/3849557951/

http://www.flickr.com/photos/cku/1386908692/

http://www.flickr.com/photos/macten/4611148426/

http://www.rustybrick.com/prototype-js-vs-jquery-comparison.html

http://www.flickr.com/photos/cadsonline/4321530819/

http://www.flickr.com/photos/maiptitfleur/4942829255/