The Zen of Lithium

Preview:

DESCRIPTION

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

Citation preview

THE

OF

• Former lead developer, CakePHP

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

• Original BostonPHP framework bake-off champ!

• Twitter: @nateabele

• 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

ARCHITECTURE

Procedural Object-Oriented

Procedural Object-Oriented

PARADIGMS

Event-Driven

Aspect-Oriented

Declarative

Procedural

Functional

Object-Oriented

PARADIGM HUBRIS

The Fall of Rome

+ $

ZEND FRAMEWORK 1.5

$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);

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; }}

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; }}

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

$mailer = $container->getMailer();

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; }}

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') ));

<?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>

Dependency injection container

+ Service container

+ Service container builder

+ XML

==

mail()

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

GREAT ARCHITECTURE

The Guggenheim Fallingwater

WHO WON?

JET LI AS HOU YUANJIA

Jet Li’s Fearless

BRUCE LEE

JEET KUNE DOThe Way of the Intercepting Fist

GOALS

• Understand a variety of paradigms & their strengths

• Respect context when choosing paradigms / techniques

• Be simple as possible (but no simpler)

PROBLEMS

• Managing configuration

• Staying flexible

• Extending internals

• Easy things: easy; hard things: possible

CONFIGURATION

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

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

webroot/index.php

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

config/bootstrap/libraries.php

use lithium\core\Libraries;

Libraries::add('lithium');

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

Libraries::add('li3_docs');

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')));

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' )));

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') )));

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?

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?

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() { // ... }}

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

$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” }}

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

FLEXIBILITY

HELPERS

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

HELPERS

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

<input type="text" name="email" id="MemberEmail" value="nate.abele@gmail.com" />

HELPERS

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

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>

HELPERS

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

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>

HELPERS

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

HELPERS

<input type="text" name="email" id="MemberEmail" value="nate.abele@gmail.com" />

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); })));

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); })));

HELPERS

<input type="text" name="email" id="member_email" value="nate.abele@gmail.com" />

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

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 !

THE MEDIA CLASS

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

THE MEDIA CLASS

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

THE MEDIA CLASS

lithium\net\http\Media {

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

array( 'posts' => ...)

THE MEDIA CLASS

lithium\net\http\Media {

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

array( 'posts' => ...)

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)));

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)));

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)));

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)));

CONDITIONS?

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

==

$request->is('ajax')

==

$_SERVER['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'

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;});

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));

ROUTE HANDLERS

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

ROUTE HANDLERS

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

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));});

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() )); });

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') ));});

EXTENSIBILITY

HELPERS

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

lithium\template\helper\Html

HELPERS

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

lithium\template\helper\Html

app\extensions\helper\Html

MODELS

namespace app\models;

class Posts extends \lithium\data\Model {

}

MODELS

namespace app\models;

class Posts extends \lithium\data\Model {

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

MODELS

namespace app\models;

class Posts extends \lithium\data\Model {

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

MODELS

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

// ...

$post->save();

MODELS

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

// ...

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

MODELS

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

namespace app\models;

class Posts extends \lithium\data\Model {

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

MODELS

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

namespace app\models;

class Posts extends \lithium\data\Model {

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

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();

ENTITIES & COLLECTIONS

$posts = Posts::findAllBySomeCondition();

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;});

RELATIONSHIPS

namespace app\models;

class Posts extends \lithium\data\Model {

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

RELATIONSHIPS

namespace app\models;

class Posts extends \lithium\data\Model {

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

RELATIONSHIPS

namespace app\models;

class Posts extends \lithium\data\Model {

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

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' ) ));}

NO HASANDBELONGSTOMANY!!

DOCUMENT DATABASES

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

DOCUMENT DATABASES

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

DOCUMENT DATABASES

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

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')), )))));

FILTERS

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

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;});

find()

caching

logging

find()

caching

logging

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;});

THE TALK OF THE TOWN

CAN I USE IT IN PRODUCTION?

GIMMEBAR.COM

Sean Coates

MAPALONG.COM

Chris Shiflett

Andrei Zmievski

TOTSY.COM

Mitch Pirtle

. . .AND MANY OTHERS

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.

“”

Helgi Þormar Þorbjörnsson

• Developer, PEAR Installer

• PEAR Core Dev, 8 years

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

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.

“”

1 .0?

SO CLOSE!!

TOMORROW...

0.10

THANKS!!

@nateabele

nate.abele@gmail.com

http://nateabele.com/

Find me later :

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/

Recommended