LazyRecord: The Fast ORM for PHP

Post on 10-May-2015

9,927 views 1 download

Tags:

transcript

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⽇日星期⽇日