Skip to content

Instantly share code, notes, and snippets.

@ebernhardson
Last active August 29, 2015 14:05
Show Gist options
  • Save ebernhardson/4b58fd87fedfcdd2b032 to your computer and use it in GitHub Desktop.
Save ebernhardson/4b58fd87fedfcdd2b032 to your computer and use it in GitHub Desktop.
Serializer.php
<?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