Last active
July 17, 2016 01:22
-
-
Save parweb/425ff1fdf03c052a83c7f67ddba24bf1 to your computer and use it in GitHub Desktop.
Facebook/DataLoader for the PHP communauty on top of Icicle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\hissezhaut\DataLoader; | |
use App\hissezhaut\DataLoader\Types\Map; | |
use App\hissezhaut\DataLoader\Types\Options; | |
use App\hissezhaut\DataLoader\Types\Queue; | |
use Exception; | |
use Icicle\Awaitable as UPromise; | |
use Icicle\Awaitable\Promise; | |
use Icicle\Coroutine\Coroutine; | |
use Icicle\Loop; | |
class DataLoader extends Map { | |
public $_batchLoadFn; | |
public $_options; | |
public $_promiseCache; | |
public $_queue; | |
public function __construct ( callable $batchLoadFn, Options $options = null ) { | |
// dd( $batchLoadFn ); | |
if ( !is_callable( $batchLoadFn ) ) { | |
throw new Exception( | |
'DataLoader must be constructed with a function which accepts ' . | |
"Array<key> and returns Promise<Array<value>>, but got: {$batchLoadFn}." | |
); | |
} | |
$this->_batchLoadFn = $batchLoadFn; | |
$this->_options = $options; | |
$this->_promiseCache = $options && $options->cacheMap ? $options->cacheMap : new Map; | |
$this->_queue = new Queue; | |
} | |
public function find ( $key ) { | |
$him = function ( $dt ) use ( $key ) { | |
yield $dt->load( $key ); | |
}; | |
return new Coroutine( $him( $this ) ); | |
} | |
public function load ( $key ) : Promise { | |
if ( $key === null ) { | |
throw new Exception( | |
'The loader.load() function must be called with a value,' . | |
"but got: {$key}." | |
); | |
} | |
$options = $this->_options; | |
$shouldBatch = !$options || $options->batch !== false; | |
$shouldCache = !$options || $options->cache !== false; | |
$cacheKeyFn = $options && $options->cacheKeyFn; | |
$cacheKey = $cacheKeyFn ? $cacheKeyFn( $key ) : $key; | |
// var_dump( [$options, $shouldBatch, $shouldCache, $cacheKeyFn, $cacheKey] ); | |
if ( $shouldCache ) { | |
$cachedPromise = $this->_promiseCache->get( $cacheKey ); | |
if ( $cachedPromise ) { | |
// dd($cachedPromise, true); | |
return $cachedPromise; | |
} | |
} | |
$promise = new Promise( function ( callable $resolve, callable $reject ) use ( $key, $shouldBatch ) { | |
sleep(2); | |
$this->_queue->push( [ $key, $resolve, $reject ] ); | |
if ( count( $this->_queue ) === 1 ) { | |
if ( $shouldBatch ) { | |
Loop\queue( function () { | |
var_dump($this); | |
return dispatchQueue( $this ); | |
}); | |
// enqueuePostPromiseJob( function () { | |
// var_dump($this); | |
// return dispatchQueue( $this ); | |
// }); | |
} | |
else { | |
dispatchQueue( $this ); | |
} | |
} | |
}); | |
if ( $shouldCache ) { | |
$this->_promiseCache->set( $cacheKey, $promise ); | |
} | |
return $promise; | |
} | |
public function loadMany ( array $keys ) : Promise { | |
if ( !is_array( $keys ) ) { | |
throw new Exception( | |
'The loader.loadMany() function must be called with Array<key> ' . | |
"but got: {$keys}." | |
); | |
} | |
$all = collect( $keys )->map( function ( $key ) { | |
return $this->load( $key ); | |
})->toArray(); | |
return UPromise\All( $all ); | |
// return UPromise\all( collect( $keys )->map( function ( $key ) { | |
// return $this->load( $key ); | |
// })->toArray() ); | |
} | |
public function clear ( $key ) : Self { | |
$cacheKeyFn = $this->_options && $this->_options->cacheKeyFn; | |
$cacheKey = $cacheKeyFn ? $cacheKeyFn( $key ) : $key; | |
$this->_promiseCache->delete( $cacheKey ); | |
return $this; | |
} | |
public function clearAll () : Self { | |
$this->_promiseCache->clear(); | |
return $this; | |
} | |
public function prime ( $key, $value ) : Self { | |
$cacheKeyFn = $this->_options && $this->_options->cacheKeyFn; | |
$cacheKey = $cacheKeyFn ? $cacheKeyFn( $key ) : $key; | |
if ( $this->_promiseCache->get( $cacheKey ) === undefined ) { | |
$promise = $value instanceof Exception ? UPromise\reject( $value ) : UPromise\resolve( $value ); | |
$this->_promiseCache->set( $cacheKey, $promise ); | |
} | |
return $this; | |
} | |
} | |
$last_lap = microtime( true ); | |
function bench ( $method, $line, $file ) { | |
global $_start, $last_lap; | |
return true; | |
$now = microtime( true ); | |
$total = round( $now - $_start, 3 ); | |
$lap = round( $now - $last_lap, 6 ); | |
$last_lap = $now; | |
if ( $lap > 0.01 ) { | |
echo '( '.$total.'s | '.$lap.'s )-[ '.$method.' ]-( '.$line.' )---{ '.$file." }\n"; | |
} | |
} | |
$resolvedPromise = null; | |
function enqueuePostPromiseJob ( $function ) { | |
global $resolvedPromise; | |
if ( !$resolvedPromise ) { | |
$resolvedPromise = UPromise\resolve(); | |
} | |
Loop\queue( $function ); | |
return $resolvedPromise->then( function () use ( $function ) { | |
// Loop\queue( $function ); | |
}); | |
} | |
function dispatchQueue ( DataLoader $loader ) { | |
$queue = $loader->_queue; | |
$loader->_queue = new Queue; | |
$keys = $queue->map( function ( $args ) { | |
return $args[0]; | |
}); | |
// dd($keys); | |
// var_dump(__FILE__.' ---------------------------------------------------------------------------------------------------------------------------------------> '.__LINE__, $keys); | |
$batchLoadFn = $loader->_batchLoadFn; | |
$batchPromise = $batchLoadFn( $keys ); | |
if ( !$batchPromise || !method_exists( $batchPromise, 'then' ) ) { | |
return failedDispatch( $loader, $queue, new \Exception( | |
'DataLoader must be constructed with a function which accepts ' . | |
'Array<key> and returns Promise<Array<value>>, but the function did ' . | |
"not return a Promise: {$batchPromise}." | |
)); | |
} | |
$batchPromise->then( function ( $values ) use ( $keys, $queue ) { | |
if ( !is_array( $values ) ) { | |
throw new \Exception( | |
'DataLoader must be constructed with a function which accepts ' . | |
'Array<key> and returns Promise<Array<value>>, but the function did ' . | |
"not return a Promise of an Array: {$values}." | |
); | |
} | |
if ( count( $values ) !== count( $keys ) ) { | |
throw new Exception( | |
'DataLoader must be constructed with a function which accepts ' . | |
'Array<key> and returns Promise<Array<value>>, but the function did ' . | |
'not return a Promise of an Array of the same length as the Array ' . | |
'of keys.' . | |
"\n\nKeys:\n".count($keys)."" . | |
"\n\nValues:\n".count($values)."\n\n".var_export( compact('keys', 'values'), true ) | |
); | |
} | |
$queue->forEach( function ( $args, $index ) use ( $values ) { | |
list( $key, $resolve, $reject ) = $args; | |
$value = $values[$index]; | |
if ( $value instanceof \Exception ) { | |
// exit($value->getMessage()); | |
$reject( $value ); | |
} | |
else { | |
$resolve( $value ); | |
} | |
}); | |
})->capture( function ( $error ) use ( $loader, $queue ) { | |
return failedDispatch( $loader, $queue, $error ); | |
}); | |
} | |
function failedDispatch ( DataLoader $loader, Queue $queue, \Exception $error ) { | |
$queue->forEach( function ( $args ) use ( $loader, $error ) { | |
list( $key, $resolve, $reject ) = $args; | |
$loader->clear( $key ); | |
$reject( $error ); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\hissezhaut\DataLoader\Tests; | |
use App\hissezhaut\DataLoader\DataLoader; | |
use Icicle\Awaitable as UPromise; | |
use Icicle\Awaitable\Promise; | |
use Icicle\Coroutine\Coroutine; | |
function idLoader( $options = null ) { | |
$loadCalls = []; | |
$identityLoader = new DataLoader( function ( $keys ) use ( &$loadCalls ) { | |
$loadCalls[] = $keys; | |
return UPromise\resolve( $keys ); | |
}, $options ); | |
return [ $identityLoader, $loadCalls ]; | |
} | |
class DataLoaderTest { | |
static public function run () { | |
function good ( $one, $two ) { | |
echo ( $one === $two ? "-------------------------------------------------------------------------- GOOD" : "--------------------------------------------------------------------------------------------------------- NO NO NO" )."\n"; | |
}; | |
$identityLoader = new DataLoader( function ( $keys) { | |
return UPromise\resolve( $keys ); | |
}); | |
$one = function ($identityLoader) { | |
echo "it('builds a really really simple data loader')\n"; | |
$promise1 = $identityLoader->load( 1 ); | |
var_dump( '$promise1 instanceof Promise', $promise1 instanceof Promise ); | |
good( true, $promise1 instanceof Promise ); | |
$generator = function () use ( $promise1 ) { | |
yield UPromise\resolve($promise1); | |
}; $coroutine = new Coroutine($generator()); $coroutine->done(function ($value1) { | |
var_dump( '$value1 == 1 ?', $value1 ); | |
good( true, $value1 === 1 ); | |
}); | |
}; | |
// $one($identityLoader); | |
$two = function () { | |
echo "it('supports loading multiple keys in one call')\n"; | |
$identityLoader = new DataLoader( function ( $keys) { | |
return UPromise\resolve( $keys ); | |
}); | |
$promiseAll = $identityLoader->loadMany( [ 1, 2 ] ); | |
var_dump( '$promiseAll instanceof Promise', $promiseAll instanceof Promise ); | |
good( true, $promiseAll instanceof Promise ); | |
$values = $identityLoader->loadMany( [ 1, 2 ] ); | |
var_dump( '$values == [ 1, 2 ] ?', $values ); | |
good( true, $values === [ 1, 2 ] ); | |
$promiseEmpty = $identityLoader->loadMany([]); | |
var_dump( '$promiseEmpty instanceof Promise', $promiseEmpty instanceof Promise ); | |
good( true, $promiseEmpty instanceof Promise ); | |
$empty = UPromise\promisify(function()use($promiseEmpty){ | |
$promiseEmpty; | |
}); | |
var_dump( '$values == [] ?', $values ); | |
good( true, $values === [] ); | |
}; | |
// $two(); | |
$three = function () { | |
echo "it('batches multiple requests')\n"; | |
list( $identityLoader, $loadCalls ) = idLoader(); | |
$promise1 = $identityLoader->load( 1 ); | |
$promise2 = $identityLoader->load( 2 ); | |
dd(UPromise\all([ $promise1, $promise2 ])); | |
list( $value1, $value2 ) = UPromise\all([ $promise1, $promise2 ]); | |
var_dump( '$value1 == 1 ?', $value1 ); | |
good( true, $value1 === 1 ); | |
var_dump( '$value2 == 2 ?', $value2 ); | |
good( true, $value2 === 2 ); | |
var_dump( '$loadCalls == [ [ 1, 2 ] ] ?', $loadCalls ); | |
good( true, $loadCalls === [ [ 1, 2 ] ] ); | |
// expect(loadCalls).to.deep.equal([ [ 1, 2 ] ]); | |
}; | |
$three(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\hissezhaut\DataLoader\Types; | |
use stdClass; | |
use SplObjectStorage; | |
use Iterator; | |
class Map extends SplObjectStorage implements Iterator/* implements Countable , Iterator , Serializable, ArrayAccess */ { | |
protected $mapping = []; | |
public function __construct ( $key = null, $value = null ) { | |
if ( $key && $value ) { | |
$this->offsetSet( $key, $value ); | |
} | |
} | |
static public function pair ( $key, $value ) { | |
return new Static( $key, $value ); | |
} | |
public function enforceObject ( $offset ) { | |
if ( is_string( $offset ) || is_numeric( $offset ) ) { | |
$key = new stdClass; | |
$key->key = $offset; | |
$offset = $key; | |
} | |
return $offset; | |
} | |
public function offsetSet ( $_offset , $value ) { | |
$offset = $this->enforceObject( $_offset ); | |
if ( !is_object( $_offset ) ) { | |
$this->mapping[$_offset] = $offset; | |
} | |
return parent::offsetSet( $offset, $value ); | |
} | |
public function set ( $offset , $value ) { | |
return $this->offsetSet( $offset, $value ); | |
} | |
// public function attach ( $_offset , $value ) { | |
// $offset = $this->enforceObject( $_offset ); | |
// $this->mapping[$_offset] = $offset; | |
// return parent::attach( $offset, $value ); | |
// } | |
public function offsetGet ( $offset ) { | |
$offset = $this->normalizeOffset( $offset ); | |
if ( !$this->offsetExists( $offset ) ) { | |
return false; | |
} | |
return parent::offsetGet( $offset ); | |
} | |
public function get ( $offset ) { | |
return $this->offsetGet( $offset ); | |
} | |
public function offsetUnset ( $offset ) { | |
$offset = $this->normalizeOffset( $offset ); | |
if ( !$this->offsetExists( $offset ) ) { | |
return false; | |
} | |
return parent::offsetUnset( $offset ); | |
} | |
public function delete ( $offset ) { | |
return $this->offsetUnset( $offset ); | |
} | |
public function detach ( $offset ) { | |
$offset = $this->normalizeOffset( $offset ); | |
return parent::detach( $offset ); | |
} | |
public function contains ( $offset ) { | |
$offset = $this->normalizeOffset( $offset ); | |
return parent::contains( $offset ); | |
} | |
public function getHash ( $offset ) { | |
$offset = $this->normalizeOffset( $offset ); | |
return parent::getHash( $offset ); | |
} | |
public function offsetExists ( $offset ) { | |
$offset = $this->normalizeOffset( $offset ); | |
if ( !is_object( $offset ) ) { | |
return false; | |
} | |
return parent::offsetExists( $offset ); | |
} | |
public function normalizeOffset ( $offset ) { | |
return ( is_string( $offset ) || is_numeric( $offset ) ) && isset( $this->mapping[(string)$offset] ) ? $this->mapping[(string)$offset] : $offset; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\hissezhaut\DataLoader\Types; | |
use App\hissezhaut\DataLoader\Types\accessor; | |
// use DataLoader\Types\Map; | |
final class Options { | |
use accessor; | |
protected $fields = [ | |
'batch' => true, | |
'cache' => true, | |
'cacheKeyFn' => null, | |
'cacheMap' => false, | |
]; | |
protected $batch; | |
protected $cache; | |
protected $cacheKeyFn; | |
protected $cacheMap; | |
public function __construct ( $batch = true, $cache = true, $cacheKeyFn = false, $cacheMap = false ) { | |
if ( is_array( $batch ) ) { | |
$options = array_merge( $this->fields, $batch ); | |
$this->batch = $options['batch']; | |
$this->cache = $options['cache']; | |
$this->cacheMap = $options['cacheMap']; | |
$this->cacheKeyFn = $options['cacheKeyFn']; | |
} | |
else { | |
$this->batch = $batch; | |
$this->cache = $cache; | |
$this->cacheMap = $cacheMap; | |
$this->cacheKeyFn = $cacheKeyFn; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\hissezhaut\DataLoader\Types; | |
class Queue { | |
private $items = []; | |
public function push ( $item ) { | |
$this->items[] = $item; | |
} | |
public function map ( $callback ) { | |
return array_map( $callback, $this->items ); | |
} | |
public function forEach ( $callback ) { | |
return array_walk( $this->items, $callback ); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment