Skip to content

Instantly share code, notes, and snippets.

@parweb
Last active July 17, 2016 01:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save parweb/425ff1fdf03c052a83c7f67ddba24bf1 to your computer and use it in GitHub Desktop.
Save parweb/425ff1fdf03c052a83c7f67ddba24bf1 to your computer and use it in GitHub Desktop.
Facebook/DataLoader for the PHP communauty on top of Icicle
<?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 );
});
}
<?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();
}
}
<?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;
}
}
<?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;
}
}
}
<?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