Post on 10-May-2015
LazyRecordThe Fast PHP ORM
林佑安Yo-An Lin (c9s)
12年4月22⽇日星期⽇日
.metadata
• 林佑安 (c9s)
• 190 repo/projects on GitHub
• Perl programming since 2008
• PHP programming since last year
• c, c++, javascript, obj-c, ruby, python, haskell, java, c#, VB .NET ...
12年4月22⽇日星期⽇日
Why another PHP ORM ?
12年4月22⽇日星期⽇日
PHP ORMs
• Doctrine
12年4月22⽇日星期⽇日
PHP ORMs
• Doctrine
• Propel
12年4月22⽇日星期⽇日
PHP ORMs
• Doctrine
• Propel
• Idiorm / Paris
12年4月22⽇日星期⽇日
Propel / Doctrine
• Propel uses XML Schema file.
12年4月22⽇日星期⽇日
Propel / Doctrine
• Propel uses XML Schema file.
• Doctrine uses XML/YAML/Annotations.
12年4月22⽇日星期⽇日
Propel / Doctrine
• Propel uses XML Schema file.
• Doctrine uses XML/YAML/Annotations.
• Slow & Fat.
12年4月22⽇日星期⽇日
Propel / Doctrine
• Propel uses XML Schema file.
• Doctrine uses XML/YAML/Annotations.
• Slow & Fat.
• Doctrine is too complicated.
12年4月22⽇日星期⽇日
Common characteristic
12年4月22⽇日星期⽇日
• XML for configuration file.
• XML for schema file.
• XML for everything.
• Concepts are from Java, too complicated.
12年4月22⽇日星期⽇日
Propel XML runtime.conf
12年4月22⽇日星期⽇日
<?xml version="1.0"?><config> <log> <ident>propel-bookstore</ident> <type>console</type> <level>7</level> </log> <propel> <datasources default="bookstore"> <datasource id="bookstore"> <adapter>sqlite</adapter> <connection> <classname>DebugPDO</classname> <dsn>mysql:host=localhost;dbname=bookstore</dsn> <user>testuser</user> <password>password</password> <options> <option id="ATTR_PERSISTENT">false</option> </options> <attributes> <option id="ATTR_EMULATE_PREPARES">true</option> </attributes> <settings> <setting id="charset">utf8</setting> <setting id="queries"> <query>set search_path myschema, public</query><!-- automatically set postgresql's search_path --> <query>INSERT INTO BAR ('hey', 'there')</query><!-- execute some other query --> </setting> </settings> </connection> <slaves> <connection> <dsn>mysql:host=slave-server1; dbname=bookstore</dsn> </connection> <connection> <dsn>mysql:host=slave-server2; dbname=bookstore</dsn> </connection> </slaves> </datasource> </datasources> <debugpdo> <logging> <details> <method> <enabled>true</enabled> </method> <time> <enabled>true</enabled> <precision>3</precision> </time> <mem> <enabled>true</enabled> <precision>1</precision> </mem> </details> </logging> </debugpdo> </propel></config>
12年4月22⽇日星期⽇日
<?xml version="1.0"?><config> <log> <ident>propel-bookstore</ident> <type>console</type> <level>7</level> </log> <propel> <datasources default="bookstore"> <datasource id="bookstore"> <adapter>sqlite</adapter> <connection> <classname>DebugPDO</classname> <dsn>mysql:host=localhost;dbname=bookstore</dsn> <user>testuser</user> <password>password</password> <options> <option id="ATTR_PERSISTENT">false</option> </options> <attributes> <option id="ATTR_EMULATE_PREPARES">true</option> </attributes> <settings> <setting id="charset">utf8</setting> <setting id="queries"> <query>set search_path myschema, public</query><!-- automatically set postgresql's search_path --> <query>INSERT INTO BAR ('hey', 'there')</query><!-- execute some other query --> </setting> </settings> </connection> <slaves> <connection> <dsn>mysql:host=slave-server1; dbname=bookstore</dsn> </connection> <connection> <dsn>mysql:host=slave-server2; dbname=bookstore</dsn> </connection> </slaves> </datasource> </datasources> <debugpdo> <logging> <details> <method> <enabled>true</enabled> </method> <time> <enabled>true</enabled> <precision>3</precision> </time> <mem> <enabled>true</enabled> <precision>1</precision> </mem> </details> </logging> </debugpdo> </propel></config>
12年4月22⽇日星期⽇日
12年4月22⽇日星期⽇日
It should be simpler
12年4月22⽇日星期⽇日
Inspirations
• JiftyDBI / Perl
• KiokuDB / Perl
• ActiveRecord / Ruby
• Propel / PHP
12年4月22⽇日星期⽇日
ActiveRecord Pattern
12年4月22⽇日星期⽇日
client = Client.find(10)
client = Client.first
Client.where("orders_count = ?", params[:orders])
Client.where("created_at >= :start_date AND created_at <= :end_date", {:start_date => params[:start_date], :end_date => params[:end_date]})
Client.order("created_at DESC")
Client.limit(5).offset(30)
12年4月22⽇日星期⽇日
Object::Declare
Audrey Tang唐鳳
use Object::Declare ['MyApp::Column', 'MyApp::Param'];
my %objects = declare {
param foo => !is global, is immutable, valid_values are qw( more values );
column bar => field1 is 'value', field2 is 'some_other_value', sub_params are param( is happy ), param ( is sad );
};
print $objects{foo}; # a MyApp::Param objectprint $objects{bar}; # a MyApp::Column object
# Assuming that MyApp::Column::new simply blesses into a hash...print $objects{bar}{sub_params}[0]; # a MyApp::Param objectprint $objects{bar}{sub_params}[1]; # a MyApp::Param object
200612年4月22⽇日星期⽇日
Jifty::DBI
package Simple;use Jifty::DBI::Schema;use Jifty::DBI::Record schema { column foo => type is 'text'; column bar => type is 'text';};
12年4月22⽇日星期⽇日
Jifty::DBI
package TestApp::Model::Phone;use Jifty::DBI::Schema;use Jifty::DBI::Record schema { column user => references TestApp::Model::User by 'id', is mandatory; column type => ...; column value => validator is sub { ... }, default is sub { } ;};
12年4月22⽇日星期⽇日
Jifty
Model Schema
12年4月22⽇日星期⽇日
Jifty
Model Schema ⇛ Action
12年4月22⽇日星期⽇日
Jifty
Model Schema ⇛ Action ⇛
CRUD
12年4月22⽇日星期⽇日
Jifty
App::Model::Phone ☚ write once
12年4月22⽇日星期⽇日
Jifty
App::Model::PhoneApp::Model::PhoneCollection
12年4月22⽇日星期⽇日
Jifty
App::Model::PhoneApp::Model::PhoneCollection
App::Action::CreatePhone
12年4月22⽇日星期⽇日
Jifty
App::Model::PhoneApp::Model::PhoneCollection
App::Action::CreatePhoneApp::Action::UpdatePhone
12年4月22⽇日星期⽇日
Jifty
App::Model::PhoneApp::Model::PhoneCollection
App::Action::CreatePhoneApp::Action::UpdatePhoneApp::Action::DeletePhone
12年4月22⽇日星期⽇日
Jifty
App::Model::PhoneApp::Model::PhoneCollection
App::Action::CreatePhoneApp::Action::UpdatePhoneApp::Action::DeletePhone
$phone->as_create_action()->render();
12年4月22⽇日星期⽇日
12年4月22⽇日星期⽇日
What we need
• Can use PHP closures for validation, default value, completion ..etc.
• Everything should be lazy.
• Simple API
• No overdesign.
• Mixin schema
• CRUD generation.
• Front-end CRUD integration.
12年4月22⽇日星期⽇日
PHP 5.3 Characteristic
• APC is fast.
• json_encode / json_decode (file) are slower than require a simple array from php source code.
• function is faster than class method.
• class method is slower than properties.
• magic method is slower than normal class method.
• array is faster than object.
12年4月22⽇日星期⽇日
$array[] vs array_push
https://github.com/c9s/SimpleBench
12年4月22⽇日星期⽇日
Function calls
https://github.com/c9s/SimpleBench
12年4月22⽇日星期⽇日
PHP ORM v1
12年4月22⽇日星期⽇日
EteDB
• Initialize model schema in runtime.
• Schema is defined in Model (in __constructor).
• MySQL only.
• dynamic class generator (using eval)
• too slow.
12年4月22⽇日星期⽇日
PHP ORM v2
12年4月22⽇日星期⽇日
LazyRecord
• Lazy schema loader
• Lazy attribute
• Lazy class loader
• Lazy connection
• Static class generator
• SQL Generator for MySQL, PgSQL, SQLite
• SplClassLoader
• ... etc
12年4月22⽇日星期⽇日
Based on SQLBuilder
12年4月22⽇日星期⽇日
SQLBuilder
• A Simple SQL Generator.
• Prevent Injection.
• Migration generator. (index, alter table...etc)
• Support SQLite, Pgsql, Mysql syntax.
• Pure SQL or with named-parameters.
12年4月22⽇日星期⽇日
<?php$sqlbuilder = new SQLBuilder\QueryBuilder( $driver );$sql = $sqlbuilder->table('authors')->insert([ 'name' => 'Mary', 'address' => 'Paris',])->build();
12年4月22⽇日星期⽇日
-- General syntaxINSERT INTO authors ( name , address ) VALUES ( 'Name' , 'Address' );
-- PgSQLINSERT INTO "Authors" ( "Name" , "Address" ) VALUES ( 'Name' , 'Address' );
-- MySQLINSERT INTO `Authors` ( `Name` , `Address` ) VALUES ( 'Name' , 'Address' );
-- PDOINSERT INTO authors ( name , address ) VALUES ( ? , ? );INSERT INTO authors ( name , address ) VALUES ( :name , :address );
12年4月22⽇日星期⽇日
<?php$sql = $builder->table('Member')->select('*') ->where() ->equal( 'a' , 'bar' ) // a = 'bar' ->notEqual( 'a' , 'bar' ) // a != 'bar' ->is( 'a' , 'null' ) // a is null ->isNot( 'a' , 'null' ) // a is not equal ->greater( 'a' , '2011-01-01' ); ->greater( 'a' , ['date(2011-01-01)'] ); // do not escape ->or()->less( 'a' , 123 ) ->and()->like( 'content' , '%content%' ); ->group() // and ( a = 123 or b != 123 ) ->is( 'a' , 123 ) ->isNot( 'b', 123 ) ->ungroup() ->build();
12年4月22⽇日星期⽇日
Overview
12年4月22⽇日星期⽇日
Model Overview
12年4月22⽇日星期⽇日
<?php$author = new Author;$ret = $author->create([ 'name' => "Deflator Test $i", 'country' => 'Tokyo', 'confirmed' => true, 'date' => new DateTime('2011-01-01 00:00:00'),]);if( $ret->success ) { echo "Created!";}
12年4月22⽇日星期⽇日
<?php$ret = $author->update(array( 'name' => 'Bar' ));if( $ret->success ) { echo "Updated!";}else { echo $ret; // __toString support}
12年4月22⽇日星期⽇日
<?php$record = Author::load(array( 'name' => 'Foo' ));
// To find a record with primary key:$record = Author::load( 1 );
// To update a record (static):$ret = Author::update( array( 'name' => 'Author' ))->where() ->equal('id',3) ->execute();
12年4月22⽇日星期⽇日
$author->toJson();$author->toArray();$author->toXml();$author->toYaml();
12年4月22⽇日星期⽇日
Collection Overview
12年4月22⽇日星期⽇日
<?php$authors = new AuthorCollection;foreach( $authors as $author ) { echo $author->name , "\n"}
Iterator
12年4月22⽇日星期⽇日
<?php $names = new NameCollection; $names->where() ->equal('name','Foo') ->groupBy('name','address');?>
SQLBuilder Mix-In
12年4月22⽇日星期⽇日
<?php $newCollection = $names->filter(function($item) { // do something else })->filter(function($item) { return $item->confirmed; });?>
Filter
12年4月22⽇日星期⽇日
<?php $names->each(function($item) { $item->update([ .... ]); });?>
Each
12年4月22⽇日星期⽇日
<?php /* page 1, 10 per page */ $authors = new AuthorCollection; $pager = $authors->pager(1,10);
$pager = $authors->pager(); $items = $pager->items();
$pager->next(); // next page?>
Integrate with OFFSET & LIMIT
Collection Pager
12年4月22⽇日星期⽇日
Relationship<?php// has many$address = $author->addresses->create([ 'address' => 'farfaraway']);
// create related address$author->addresses[] = [ 'address' => 'Harvard' ];
$addresses = $author->addresses->items();
foreach( $author->addresses as $address ) { echo $address->address , "\n";}
12年4月22⽇日星期⽇日
Schema
12年4月22⽇日星期⽇日
Powered by CascadingAttribute.php
12年4月22⽇日星期⽇日
<?phpuse LazyRecord\Schema\SchemaDeclare;
class AddressSchema extends SchemaDeclare{ function schema() {
}}
12年4月22⽇日星期⽇日
<?phpuse LazyRecord\Schema\SchemaDeclare;
class AddressSchema extends SchemaDeclare{ function schema() { $this->column('address') ->varchar(128); }}
12年4月22⽇日星期⽇日
<?phpuse LazyRecord\Schema\SchemaDeclare;
class AddressSchema extends SchemaDeclare{ function schema() { $this->column('address') ->integer(); }}
12年4月22⽇日星期⽇日
<?phpuse LazyRecord\Schema\SchemaDeclare;
class AddressSchema extends SchemaDeclare{ function schema() { $this->column('address') ->timestamp(); }}
12年4月22⽇日星期⽇日
Default value & builder
12年4月22⽇日星期⽇日
$this->column('name') ->varchar(30) ->default('Default');
12年4月22⽇日星期⽇日
$this->column('name') ->varchar(30) ->default( array('current_timestamp') );
12年4月22⽇日星期⽇日
$this->column('name') ->varchar(30) ->defaultBuilder(function() { return date('c'); })
12年4月22⽇日星期⽇日
$this->column('name') ->varchar(30) ->default('Default') ->default( array('current_timestamp') ) ->defaultBuilder(function() { return date('c'); })
12年4月22⽇日星期⽇日
Validator
12年4月22⽇日星期⽇日
$this->column('name') ->varchar(30) ->validator('ValidatorClass')
12年4月22⽇日星期⽇日
$this->column('name') ->varchar(30) ->validator( array('ValidatorClass','method') )
12年4月22⽇日星期⽇日
$this->column('name') ->varchar(30) ->validator('function_name')
12年4月22⽇日星期⽇日
$this->column('name') ->varchar(30) ->validator(function($val) { .... })
12年4月22⽇日星期⽇日
Filter
12年4月22⽇日星期⽇日
$this->column('name') ->varchar(30) ->filter( function($val) { return preg_replace('#word#','zz',$val); });
12年4月22⽇日星期⽇日
Deflator / Inflator
12年4月22⽇日星期⽇日
use LazyRecord\Schema\SchemaDeclare;
class NameSchema extends SchemaDeclare { function schema() { $this->column('created_on') ->date() ->isa('DateTime') ->deflator( function($val) { if( is_a( $val, 'DateTime' ) ) return $val->format('Y-m-d'); elseif( is_integer($val) ) { return strftime( '%Y-%m-%d' , $val ); } return $val; }) ->inflator( function($val) { return new \DateTime( $val ); }); } }
12年4月22⽇日星期⽇日
$name->created_on; // DateTime object$name->created_on->format('Y-m-d');
$name->create([ 'created_on' => new DateTime;]);
12年4月22⽇日星期⽇日
Mixin schema
12年4月22⽇日星期⽇日
$this->mixin('MetadataMixinSchema');$this->mixin('I18nMixinSchema');$this->mixin('CommentMinxSchema');
12年4月22⽇日星期⽇日
Multiple data source
12年4月22⽇日星期⽇日
data_sources: master: dsn: 'mysql:host=localhost;dbname=lazy_test' user: root pass: 123123
database.yml
12年4月22⽇日星期⽇日
data_sources: master: dsn: 'mysql:host=localhost;dbname=lazy_test' user: root pass: 123123 slave: dsn: 'mysql:dbname=lazy_test' query_options: { quote_column: true, quote_table: true }
database.yml
12年4月22⽇日星期⽇日
// data source for writing$this->writeTo('master');
// data source for reading$this->readFrom('slave');
data_sources: master: dsn: 'mysql:host=localhost;dbname=lazy_test' user: root pass: 123123 slave: dsn: 'mysql:dbname=lazy_test' query_options: { quote_column: true, quote_table: true }
database.yml
schema
12年4月22⽇日星期⽇日
拼裝時刻
12年4月22⽇日星期⽇日
LazyBonehttp://github.com/c9s/LazyBone.git
12年4月22⽇日星期⽇日
LazyBone =
12年4月22⽇日星期⽇日
LazyRecord+ Roller Router+ RESTful Plugin+ Backbone.js
12年4月22⽇日星期⽇日
Install LazyRecord
12年4月22⽇日星期⽇日
sudo bash -c "$(curl -s -L https://raw.github.com/c9s/LazyRecord/master/install.sh)"
12年4月22⽇日星期⽇日
Define config file
12年4月22⽇日星期⽇日
---bootstrap: - bootstrap.phpschema: paths: - modeldata_sources: default: dsn: 'sqlite:/tmp/todos.db'
config/database.yml
12年4月22⽇日星期⽇日
$ lazy build-conf config/database.yml
Convert YAML to PHP.
<?php$config = require '.lazy.php';
APC caches this automatically.
12年4月22⽇日星期⽇日
Define model
12年4月22⽇日星期⽇日
<?php
class TodoSchema extends LazyRecord\Schema\SchemaDeclare{ function schema() { $this->column('id') ->primary() ->autoIncrement() ->integer();
$this->column('title') ->text();
$this->column('done') ->boolean() ->default(false);
$this->column('created_on') ->defaultBuilder( function() { return date('c'); } ) ->timestamp(); }
function bootstrap($model) { $model->create(array( 'title' => 'Foo', )); }}
12年4月22⽇日星期⽇日
Create static schema files
12年4月22⽇日星期⽇日
12年4月22⽇日星期⽇日
$ lazy build-schema model/TodoSchema.php
12年4月22⽇日星期⽇日
$ lazy build-schema model/TodoSchema.php...Classmap:! TodoSchemaProxy => model/TodoSchemaProxy.php! TodoBase => model/TodoBase.php! Todo => model/Todo.php! TodoCollectionBase => model/TodoCollectionBase.php! TodoCollection => model/TodoCollection.phpDone
12年4月22⽇日星期⽇日
Initialize database
12年4月22⽇日星期⽇日
12年4月22⽇日星期⽇日
$ lazy build-sql model/TodoSchema.php
12年4月22⽇日星期⽇日
$ lazy build-sql model/TodoSchema.phpBuilding SQL for TodoSchema--- SQL for TodoSchema CREATE TABLE todos ( id integer primary key autoincrement,title text,done boolean default 0,created_on timestamp);
12年4月22⽇日星期⽇日
Integrate with your application
12年4月22⽇日星期⽇日
<?phpuse LazyRecord\ConfigLoader;$config = new ConfigLoader;$config->load( __DIR__ . '/.lazy.php');$config->init();
12年4月22⽇日星期⽇日
Define routes
12年4月22⽇日星期⽇日
Roller RouterHigh performance router for PHP
12年4月22⽇日星期⽇日
Roller Router• APC cache
• FileSystem cache
• Use Array to store routes
• through PHP extension, can dispatch 1607% faster than pure php version
• Annotation reader support
• RESTful plugin
12年4月22⽇日星期⽇日
$router = new Roller\Router;
$router->get( '/blog/:id/:title' , function($id,$title) { return 'Blog'; });
$router->post( '/blog/:year/:month/:id/:title', array('Controller','method') );
$router->any( '/path/to/:year' , array('Callback','method') , array( 'year' => '\d+', ));
12年4月22⽇日星期⽇日
<?php$subroutes = new Roller\RouteSet;$subroutes->add( '/subitem' , $cb );
$routes = new Roller\RouteSet;$routes->mount( '/item' , $subroutes );
/item/subitem => $cb
RouteSet
12年4月22⽇日星期⽇日
Dispatch
$r = $router->dispatch( isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '/' );
if( $r ) { echo $r();} else { die('Page not found');}
12年4月22⽇日星期⽇日
RESTful Plugin
12年4月22⽇日星期⽇日
<?php$router = new Roller\Router( null, array( 'cache_id' => 'router_demo'));
$restful = new Roller\Plugin\RESTful(array( 'prefix' => '/=/restful' ));
$restful->setGenericHandler( 'MyGenericHandler' );$router->addPlugin($restful);
12年4月22⽇日星期⽇日
GET /=/restful/postsGET /=/restful/posts.jsonGET /=/restful/posts.ymlGET /=/restful/posts/23GET /=/restful/posts/23.jsonPOST /=/restful/posts/23DELETE /=/restful/posts/23
Auto-generated routes
12年4月22⽇日星期⽇日
Define Your Resource Handler
12年4月22⽇日星期⽇日
<?phpuse Roller\Plugin\RESTful\ResourceHandler;use Roller\Plugin\RESTful\GenericHandler;
class MyGenericHandler extends GenericHandler{ public function create($resource) { }
public function load($resource,$id) { }
public function update($resource,$id) { }
public function delete($resource,$id) { }
public function find($resource) { }}
12年4月22⽇日星期⽇日
<?phpnamespace LazyBone\Resource;use Roller\Plugin\RESTful\ResourceHandler;use Todo;use TodoCollection;
class TodoResource extends ResourceHandler{ public function create() { $vars = json_decode($this->readInput(),true); $todo = new Todo; $ret = $todo->create($vars); if( $ret->success ) { return $todo->toArray(); } $this->codeBadRequest(); return array( 'error' => $ret->message ); }
public function update($id) { $todo = new Todo( $id ); if( ! $todo->id ) { return $this->codeNotFound(); }
$vars = json_decode($this->readInput(),true); unset( $vars['created_on'] ); // lazy record bug
if($vars) { $todo->update( $vars ); return $todo->toArray(); } return $this->codeBadRequest(); }
....
}12年4月22⽇日星期⽇日
Backbone.js
12年4月22⽇日星期⽇日
Todo = Backbone.Model.extend({ // Default attributes for the todo item. defaults: function() { return { title: "empty todo...", done: false // order: Todos.nextOrder(), }; },
// Toggle the `done` state of this todo item. toggle: function() { this.save({done: !this.get("done")}); },
clear: function() { this.destroy(); }});
12年4月22⽇日星期⽇日
TodoList = Backbone.Collection.extend({ // Reference to this collection's model. model: Todo, url:"/=/todos", done: function() { return this.filter(function(todo){ return todo.get('done'); }); }, remaining: function() { return this.without.apply(this, this.done()); }, });
12年4月22⽇日星期⽇日
12年4月22⽇日星期⽇日
Hackingforks welcome!
http://github.com/c9s/LazyRecord.git
12年4月22⽇日星期⽇日
Q & A ?
12年4月22⽇日星期⽇日