Last active
August 29, 2015 14:05
-
-
Save ebernhardson/4b58fd87fedfcdd2b032 to your computer and use it in GitHub Desktop.
Serializer.php
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 Flow\Serializer; | |
use Flow\Data\UserNameBatch; | |
use Flow\Model\AbstractRevision; | |
use Flow\Model\UserTuple; | |
use Flow\Model\UUID; | |
use Flow\Templating; | |
use ArrayIterator; | |
use Closure; | |
use GenderCache; | |
use Language; | |
use SpecialPage; | |
use Title; | |
use User; | |
/** | |
* Basic unit of transformation | |
*/ | |
interface TransformerInterface { | |
function transform( $value ); | |
} | |
/** | |
* Used for building a serializer | |
*/ | |
interface SerializerType { | |
function getDefaultOptions(); | |
function getParentType( array $options ); | |
function buildSerializer( SerializerBuilder $builder, array $options ); | |
} | |
/** | |
* Mediocre priority queue implementation, we don't need anything better | |
* since we only need to get the fully ordered result once per queue. If | |
* profiling shows any need this can be more performant, but more complicated. | |
*/ | |
class PriorityQueue { | |
public function insert( $value, $priority ) { | |
$this->data[$priority][] = $value; | |
} | |
public function toArray() { | |
ksort( $this->data ); | |
return call_user_func_array( 'array_merge', $this->data ); | |
} | |
} | |
/** | |
* Primary outwards facing code for serialization package. | |
* | |
* Usage: | |
* | |
* $factory = new SerializerFactory( $types ); | |
* $serializer = $factory->create( 'revision', new RevisionType )->getSerializer(); | |
* ... | |
* $data = $serializer->transform( $row ); | |
*/ | |
class SerializerFactory { | |
protected $types; | |
/** | |
* @var SerializerType[] | |
*/ | |
public function __construct( array $types ) { | |
$this->types = $types; | |
} | |
/** | |
* @param string|SerializerType $type | |
* @return SerializerType | |
* @throws Exception | |
*/ | |
public function resolveType( $type ) { | |
if ( $type instanceof SerializerType ) { | |
return $type; | |
} | |
if ( isset( $this->types[$type] ) ) { | |
return $this->types[$type]; | |
} | |
throw new \Exception( "Expected SerializerType, got $type" ); | |
} | |
/** | |
* @var string $name | |
* @var SerializerType|string|null $type | |
* @var array $options | |
* @return SerialierBuilder | |
*/ | |
public function create( $name, $type = null, array $options = array() ) { | |
if ( $type === null ) { | |
$type = 'text'; | |
} | |
$types = array(); | |
while( $type !== null ) { | |
$type = $this->resolveType( $type ); | |
$types[] = $type; | |
$options += $type->getDefaultOptions(); | |
$type = $type->getParentType( $options ); | |
} | |
$builder = new SerializerBuilder( $this, $name, $options ); | |
$options = $builder->getOptions(); | |
foreach ( array_reverse( $types ) as $type ) { | |
$type->buildSerializer( $builder, $options ); | |
} | |
return $builder; | |
} | |
} | |
// enum? | |
class SerializerPriority { | |
const PRE_CHILDREN = 0; | |
const CHILDREN = 1000; | |
const STANDARD = 10000; | |
const EXTEND = 100000; | |
const LAST = 1000000; | |
} | |
/** | |
* Configuration for a serializer. Holds name, options and | |
* a prioritized list of transformations to perform. | |
*/ | |
class SerializerConfigBuilder { | |
protected $name; | |
protected $options; | |
protected $transformers = array(); | |
public function __construct( $name, array $options = array() ) { | |
$this->name = $name; | |
$this->options = $options; | |
$this->transformers = new PriorityQueue; | |
} | |
public function getName() { | |
return $this->name; | |
} | |
public function getOptions() { | |
return $this->options; | |
} | |
public function getTransformers() { | |
return $this->transformers->toArray(); | |
} | |
/** | |
* @var TransformerInterace|Closure $transformer | |
* @var integer $priority Higher numbers are run first | |
* @return self | |
*/ | |
public function addTransformer( $transformer, $priority = SerializerPriority::STANDARD ) { | |
if ( | |
!$transformer instanceof Closure && | |
!$transformer instanceof TransformerInterface | |
) { | |
throw new \Exception( 'Expected TransformerInterface or Closure' ); | |
} | |
$this->transformers->insert( $transformer, $priority ); | |
return $this; | |
} | |
} | |
/** | |
* Adds children to the SerializerConfigBuilder with priority 0 | |
*/ | |
class SerializerBuilder extends SerializerConfigBuilder { | |
protected $factory; | |
protected $children = array(); | |
protected $extend = array(); | |
public function __construct( SerializerFactory $factory, $name, array $options = array() ) { | |
parent::__construct( $name, $options ); | |
$this->factory = $factory; | |
} | |
public function get( $name ) { | |
if ( isset( $this->children[$name] ) ) { | |
return $this->children[$name]; | |
} | |
if ( isset( $this->extend[$name] ) ) { | |
return $this->extend[$name]; | |
} | |
throw new \Exception( "Could not find child $name" ); | |
} | |
public function remove( $name ) { | |
unset( $this->children[$name], $this->extend[$name] ); | |
return $this; | |
} | |
public function has( $name ) { | |
return isset( $this->children[$name] ) | |
|| isset( $this->extend[$name] ); | |
} | |
public function add( $child, $type = null, array $options = array() ) { | |
if ( $child instanceof self ) { | |
$this->children[$child->getName()] = $child; | |
return $this; | |
} | |
if ( !is_string( $child ) ) { | |
throw new \Exception( 'Expected string or SerializerBuilder' ); | |
} | |
if ( $type !== null && !is_string( $type ) && !$type instanceof SerializerType ) { | |
throw new \Exception( 'Expected null, string, or SerializerType' ); | |
} | |
$this->children[$child] = $this->createBuilder( $child, $type, $options ); | |
return $this; | |
} | |
public function extend( $child, $type = null, array $options = array() ) { | |
if ( $child instanceof self ) { | |
$this->extend[$child->getName()] = $child; | |
return; | |
} | |
if ( !is_string( $child ) ) { | |
throw new \Exception( 'Expected string or SerializerBuilder' ); | |
} | |
if ( $type !== null && !is_string( $type ) && !$type instanceof SerializerType ) { | |
throw new \Exception( 'Expected null, string, or SerializerType' ); | |
} | |
$this->extend[$child] = $this->createBuilder( $child, $type, $options ); | |
return $this; | |
} | |
public function getSerializer() { | |
$transformers = clone $this->transformers; | |
if ( $this->children ) { | |
$children = array(); | |
foreach ( $this->children as $name => $child ) { | |
$children[$name] = $child->getSerializer(); | |
} | |
$transformers->insert( | |
new GroupSerializer( $children ), | |
SerializerPriority::CHILDREN | |
); | |
} | |
if ( $this->extend ) { | |
$extend = array(); | |
foreach ( $this->extend as $name => $child ) { | |
$extend[$name] = $child->getSerializer(); | |
} | |
$transformers->insert( | |
new ExtendGroupSerializer( $extend ), | |
SerializerPriority::EXTEND | |
); | |
} | |
return new Serializer( $transformers->toArray() ); | |
} | |
public function createBuilder( $name, $type = null, array $options = array() ) { | |
return $this->factory->create( $name, $type, $options ); | |
} | |
} | |
/** | |
* Transform a single data point with an ordered sequence | |
* of transformations. | |
*/ | |
class Serializer implements TransformerInterface { | |
/** | |
* @var Closure|TransformerInterface[] | |
*/ | |
protected $transformers; | |
/** | |
* @param Closure|TransformerInterface[] $transformers | |
*/ | |
public function __construct( array $transformers ) { | |
$this->transformers = $transformers; | |
} | |
/** | |
* @param array $data | |
* @return array | |
*/ | |
public function transform( $data ) { | |
foreach ( $this->transformers as $transformer ) { | |
if ( $transformer instanceof Closure ) { | |
$data = $transformer( $data ); | |
} else { | |
$data = $transformer->transform( $data ); | |
} | |
} | |
return $data; | |
} | |
} | |
/** | |
* Transformers a single data point into many points, | |
* one for each provided transformer | |
*/ | |
class GroupSerializer extends Serializer { | |
public function transform( $data ) { | |
$result = array(); | |
foreach ( $this->transformers as $name => $transformer ) { | |
if ( $transformer instanceof Closure ) { | |
$result[$name] = $transformer( $data ); | |
} else { | |
$result[$name] = $transformer->transform( $data ); | |
} | |
} | |
return $result; | |
} | |
} | |
/** | |
* Extends a single data point with additional | |
* data from each provided transformer | |
*/ | |
class ExtendGroupSerializer extends Serializer { | |
public function transform( $data ) { | |
foreach ( $this->transformers as $name => $transformer ) { | |
if ( $transformer instanceof Closure ) { | |
$data[$name] = $transformer( $data ); | |
} else { | |
$data[$name] = $transformer->transform( $data ); | |
} | |
} | |
return $data; | |
} | |
} | |
//////////////////////////////////////////////////////////////////////////// | |
//////////////////////////////////////////////////////////////////////////// | |
//////////////////////////////////////////////////////////////////////////// | |
abstract class AbstractSerializerType implements SerializerType { | |
public function getParentType( array $options ) { | |
return 'text'; | |
} | |
public function getDefaultOptions() { | |
return array(); | |
} | |
} | |
class PropertyType implements SerializerType { | |
public function getParentType( array $options ) { | |
return null; | |
} | |
public function getDefaultOptions() { | |
return array( | |
'path' => false, | |
); | |
} | |
public function buildSerializer( SerializerBuilder $builder, array $options ) { | |
if ( isset( $options['path'] ) ) { | |
$path = $options['path']; | |
} else { | |
$path = $builder->getName(); | |
} | |
if ( $path instanceof Closure ) { | |
$builder->addTransformer( | |
$path, | |
SerializerPriority::PRE_CHILDREN | |
); | |
} elseif ( $path !== false ) { | |
$builder->addTransformer( | |
new PropertyPathTransformer( $path ), | |
SerializerPriority::PRE_CHILDREN | |
); | |
} | |
} | |
} | |
class UuidType extends AbstractSerializerType { | |
public function getDefaultOptions() { | |
return array( | |
'timestamp' => false, | |
); | |
} | |
public function buildSerializer( SerializerBuilder $builder, array $options ) { | |
if ( $options['timestamp'] ) { | |
$builder->addTransformer( function( $data ) use ( $options ) { | |
return $data instanceof UUID | |
? $data->getTimestampObj()->getTimestamp( $options['timestamp'] ) | |
: null; | |
} ); | |
} else { | |
$builder->addTransformer( function( $data ) { | |
return $data instanceof UUID ? $data->getAlphadecimal() : null; | |
} ); | |
} | |
} | |
} | |
class TitleType extends AbstractSerializerType { | |
public function getDefaultOptions() { | |
return array( | |
'title_text' => false, | |
'special' => false, | |
'namespace' => false, | |
); | |
} | |
public function buildSerializer( SerializerBuilder $builder, array $options ) { | |
if ( $options['special'] ) { | |
$builder->addTransformer( function( $data ) use ( $options ) { | |
return $data | |
? SpecialPage::getTitleFor( $options['special'], $data ) | |
: null; | |
} ); | |
} elseif ( $options['namespace'] ) { | |
$builder->addTransformer( function( $data ) use ( $options ) { | |
return $data | |
? Title::newFromText( $data, $options['namespace'] ) | |
: null; | |
} ); | |
} | |
$builder->addTransformer( function( $data ) use ( $options ) { | |
if ( !$data instanceof Title ) { | |
return null; | |
} | |
return array( | |
'url' => $data->getLinkURL(), | |
'title' => $options['title_text'] ?: $data->getText(), | |
'exists' => $options['special'] ? true : $data->exists(), | |
); | |
} ); | |
} | |
} | |
class DateFormatsType extends AbstractSerializerType { | |
protected $callback; | |
public function __construct( User $user, Language $lang ) { | |
$this->callback = function( $data ) use ( $user, $lang ) { | |
return $data === null ? null : array( | |
'timeAndDate' => $lang->userTimeAndDate( $data, $user ), | |
'date' => $lang->userDate( $data, $user ), | |
'time' => $lang->userTime( $data, $user ), | |
); | |
}; | |
} | |
public function getParentType( array $options ) { | |
return 'uuid'; | |
} | |
public function getDefaultOptions() { | |
return array( 'timestamp' => TS_MW ); | |
} | |
public function buildSerializer( SerializerBuilder $builder, array $options ) { | |
$builder->addTransformer( $this->callback ); | |
} | |
} | |
class BooleanType extends AbstractSerializerType { | |
public function buildSerializer( SerializerBuilder $builder, array $options ) { | |
$builder->addTransformer( function( $data ) { | |
return (bool)$data; | |
} ); | |
} | |
} | |
class UsernameLookupType extends AbstractSerializerType { | |
protected $usernames; | |
public function __construct( UsernameBatch $usernames ) { | |
$this->callback = function( $data ) use ( $usernames ){ | |
return $data instanceof UserTuple | |
? $usernames->get( $data->wiki, $data->id, $data->ip ) | |
: null; | |
}; | |
} | |
public function buildSerializer( SerializerBuilder $builder, array $options ) { | |
$builder->addTransformer( $this->callback ); | |
} | |
} | |
class GenderLookupType extends AbstractSerializerType { | |
protected $genderCache; | |
public function __construct( GenderCache $genderCache ) { | |
$wikiId = wfWikiId(); | |
$this->callback = function( $data ) use ( $genderCache, $wikiId ) { | |
return ( isset( $data['wiki'], $data['name'] ) && $data['wiki'] === $wikiId ) | |
? $genderCache->getGenderOf( $data['name'], __METHOD__ ) | |
: 'unknown'; | |
}; | |
} | |
public function buildSerializer( SerializerBuilder $builder, array $options ) { | |
$builder->addTransformer( $this->callback ); | |
} | |
} | |
class UserType extends AbstractSerializerType { | |
public function buildSerializer( SerializerBuilder $builder, array $options ) { | |
$builder | |
->add( 'id', 'text', array( 'path' => 'id' ) ) | |
->add( 'wiki', 'text', array( 'path' => 'wiki' ) ) | |
->add( 'name', 'userNameLookup' ) | |
// Run the links and gender serializers with the output of children above | |
->extend( 'gender', 'userGenderLookup', array( 'path' => 'name' ) ) | |
->extend( 'links', null, array( 'path' => 'name' ) ) | |
->get( 'links' ) | |
->add( 'contribs', 'title', array( 'special' => 'Contributions' ) ) | |
->add( 'talk', 'title', array( 'namespace' => NS_USER_TALK ) ) | |
->add( 'block', 'title', array( | |
'special' => 'Block', | |
'title_text' => wfMessage( 'blocklink' ) | |
) ); | |
} | |
} | |
class RevisionLinksType extends AbstractSerializerType { | |
public function buildSerializer( SerializerBuilder $builder, array $options = array() ) { | |
$self = $this; | |
$builder->addTransformer( function( $data ) use ( $self ) { | |
return $data instanceof FormatterRow | |
? $self->buildLinks( $data ) | |
: null; | |
} ); | |
} | |
public function buildLinks( FormatterRow $row ) { | |
// @todo copy from RevisionFormatter | |
} | |
} | |
class RevisionPropertiesType extends AbstractSerializerType { | |
public function buildSerializer( SerializerBuilder $builder, array $options = array() ) { | |
$self = $this; | |
$builder->addTransformer( function( $data ) use( $self ) { | |
return ( is_array( $data ) && isset( $data['workflowId'], $data['revision'] ) ) | |
? $self->buildProperties( $data['workflowId'], $data['revision'] ) | |
: null; | |
} ); | |
} | |
public function buildProperties( UUID $workflowId, AbstractRevision $revision ) { | |
// @todo copy from RevisionFormatter | |
} | |
} | |
class RevisionActionsType extends AbstractSerializerType { | |
public function buildSerializer( SerializerBuilder $builder, array $options ) { | |
$self = $this; | |
$builder->addTransformer( function( $data ) use ( $self ) { | |
return $data instanceof FormatterRow | |
? $self->buildActions( $data ) | |
: null; | |
} ); | |
} | |
public function buildActions( FormatterRow $row ) { | |
// @todo copy from RevisionFormatter | |
} | |
} | |
class RevisionContentType extends AbstractSerializerType { | |
protected $templating; | |
public function __construct( Templating $templating ) { | |
$this->templating = $templating; | |
} | |
public function getDefaultOptions() { | |
return array( | |
'content_format' => 'html', | |
); | |
} | |
public function buildSerializer( SerializerBuilder $builder, array $options ) { | |
$templating = $this->templating; | |
$builder->addTransformer( function( $data ) use ( $options, $templating ) { | |
$contentFormat = ( $data instanceof PostRevision && $data->isTopicTitle() ) | |
? 'plaintext' | |
: $options['content_format']; | |
return array( | |
'content' => $templating->getContent( $data, $contentFormat ), | |
'contentFormat' => $contentFormat, | |
); | |
} ); | |
} | |
} | |
class PropertyPathTransformer implements TransformerInterface { | |
public function __construct( $path ) { | |
$this->path = explode( '.', $path ); | |
} | |
public function transform( $data ) { | |
$origData = $data; | |
foreach ( $this->path as $piece ) { | |
if ( is_object( $data ) ) { | |
$method = 'get' . ucfirst( $piece ); | |
if ( method_exists( $data, $method ) ) { | |
$data = $data->$method(); | |
} elseif ( property_exists( $data, $piece ) ) { | |
$data = $data->$piece; | |
} else { | |
return null; | |
} | |
} elseif ( is_array( $data ) ) { | |
if ( !isset( $data[$piece] ) ) { | |
return null; | |
} | |
$data = $data[$piece]; | |
} elseif ( $data === null ) { | |
return null; | |
} else { | |
throw new \Exception( "Invalid property path element `$piece` from `" . implode( '.', $this->path ) .'`' ); | |
} | |
} | |
return $data; | |
} | |
} | |
class RevisionType implements SerializerType { | |
public function getParentType( array $options ) { | |
return null; | |
} | |
public function getDefaultOptions() { | |
return array( | |
'content_format' => 'html', | |
); | |
} | |
public function buildSerializer( SerializerBuilder $builder, array $options ) { | |
$builder | |
->add( 'workflowId', 'uuid', array( 'path' => 'workflow.id' ) ) | |
->add( 'revisionId', 'uuid', array( 'path' => 'revision.revisionId' ) ) | |
->add( 'timestamp', 'uuid', array( | |
'path' => 'revision.revisionId', | |
'timestamp' => TS_MW, | |
) ) | |
->add( 'changeType', 'text', array( 'path' => 'revision.changeType' ) ) | |
->add( 'dateFormats', 'dateFormats', array( 'path' => 'revision.revisionId' ) ) | |
->add( 'properties', 'revisionProperties' ) | |
->add( 'isModerated', 'bool', array( 'path' => 'moderatedRevision.isModerated' ) ) | |
->add( 'links', 'revisionLinks' ) | |
->add( 'actions', 'revisionActions' ) | |
->add( 'author', 'user', array( 'path' => 'revision.userTuple' ) ) | |
->add( 'previousRevisionId', 'uuid', array( 'path' => 'revision.prevRevisionId' ) ) | |
->add( 'moderator', 'user', array( 'path' => 'moderatedRevision.moderatedBy' ) ) | |
->add( 'moderationState', 'text', array( 'path' => 'moderatedRevisoin.moderationState' ) ) | |
->add( 'content', 'content', array( | |
'path' => 'revision', | |
'content_format' => $options['content_format'], | |
) ) | |
->add( 'size' ); | |
// add nested size properties | |
$builder->get( 'size' ) | |
->add( 'old', 'text', array( | |
'path' => function( $data ) { | |
return $data->previousRevision | |
? strlen( $data->previousRevision->getContentRaw() ) | |
: null; | |
} | |
) ) | |
->add( 'new', 'text', array( | |
'path' => function( $data ) { return strlen( $data->revision->getContentRaw() ); } | |
) ); | |
// arguments for the revisionProperties type | |
$builder->get( 'properties' ) | |
->add( 'workflowId', 'text', array( 'path' => 'workflow.id' ) ) | |
->add( 'revision', 'text', array( 'path' => 'revision' ) ); | |
} | |
} | |
// __halt_compiler(); | |
///////////////////////////////////////////////////////////////////////////////////////////// | |
///////////////////////////////////////////////////////////////////////////////////////////// | |
///////////////////////////////////////////////////////////////////////////////////////////// | |
function main() { | |
$c = \Flow\Container::getContainer(); | |
$c['serializer.types'] = $c->share( function() use ( $c ) { | |
return array( | |
// basic types | |
'bool' => new BooleanType, | |
'text' => new PropertyType, | |
// data model converters | |
'uuid' => new UuidType, | |
'dateFormats' => new DateFormatsType( $c['user'], \RequestContext::getMain()->getLanguage() ), | |
'user' => new UserType, | |
'title' => new TitleType, | |
// specialty types | |
'revisionProperties' => new RevisionPropertiesType, | |
'revisionLinks' => new RevisionLinksType, | |
'revisionActions' => new RevisionActionsType, | |
'content' => new RevisionContentType( $c['templating'] ), | |
// cached lookups | |
'userNameLookup' => new UsernameLookupType( $c['repository.username'] ), | |
'userGenderLookup' => new GenderLookupType( GenderCache::singleton() ), | |
); | |
} ); | |
$c['serializer.factory'] = $c->share( function() use ( $c ) { | |
return new SerializerFactory( $c['serializer.types'] ); | |
} ); | |
$serializer = $c['serializer.factory']->create( 'revision', new RevisionType )->getSerializer(); | |
$formatter = $c['formatter.revision']; | |
$row = $c['query.post.view']->getSingleViewResult( 's0qke8d5ou6vzdr6' ); | |
/* | |
$res = $serializer->transform( $row ); | |
array_walk_recursive( $res, function( &$data ) { | |
if ( $data instanceof \Message ) { | |
$data = $data->text(); | |
} | |
} ); | |
var_dump( $res ); | |
die(); | |
*/ | |
for ( $i = 0; $i < 3; ++$i ) { | |
$rounds[] = number_format( 1000 * bench( $serializer, $row ), 2 ); | |
//$rounds[] = number_format( 1000 * bench2( $formatter, $row ), 2 ); | |
} | |
// toss the first result due to JIT | |
array_shift( $rounds ); | |
$avg = array_sum( $rounds ) / count( $rounds ); | |
var_dump( $rounds ); | |
var_dump( $avg ); | |
} | |
function bench( $serializer, $row, $times = 50 ) { | |
$start = microtime( true ); | |
for ( $i = $times; $i > 0 ; --$i ) { | |
$serializer->transform( $row ); | |
} | |
return ( microtime( true ) - $start ) / $times; | |
} | |
function bench2( $formatter , $row, $times = 50 ) { | |
$ctx = \RequestContext::getMain(); | |
$start = microtime( true ); | |
for ( $i = $times; $i > 0 ; --$i ) { | |
$formatter->formatApi( $row, $ctx ); | |
} | |
return ( microtime( true ) - $start ) / $times; | |
} | |
require_once __DIR__ . '/../../../../maintenance/commandLine.inc'; | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment