Summary

Serializer is a core component of tnc-event-dispatcher, It helps to serilize Events to be transportable, Also unserilize Serialized-Events to be Event objects again.

Serializer is inspired by Symfony Serializer, It also includes two parts, Normalizer and Formatter, The workflow like this:

Workflow Of Serializer

Normalizer takes care of transform Events to be a Array and the reverse, And Formatter is working on transform the Array to be a formatted string and the reverse, like Json, XML, ...

Example

<?php
namespace TNC\EventDispatcher;

# Specify Normalizers
$supportedNormalizers = [
    new Serialization\Normalizers\TNCActivityStreams\TNCActivityStreamsWrappedEventNormalizer(),
    new Serialization\Normalizers\TNCActivityStreams\TNCActivityStreamsNormalizer()
];

# Specify Serialization Format
$formatter  = new Serialization\Formatters\JsonFormatter();

# Initialize Serializer
$serializer = new Serializer($supportedNormalizers, $formatter);

Normalizer

ActivityStreams Normalizers

Activity Streams is a data format which defines a action, an activity consists of an actor, a verb, an an object, and a target. It tells the story of a person performing an action on or with an object. For more details please check their Document
TNC EventDispatcher internally providers ActivityStreams Normalizers implementation.

Conventions

  • EventName
  • Use only lowercase letters, numbers, dots (.) and underscores (_);
  • Prefix names with a namespace followed by a dot (e.g. order., user.*);
  • End names with a verb that indicates what action it is (e.g. user.login, payment.subscribe).

Classes

  • TNC\EventDispatcher\Serialization\Normalizers\TNCActivityStreams\TNCActivityStreamsWrappedEventNormalizer Norlimize a WrappedEvent to be a Activity Streams Array or the reverse
Supported Normalization Supported Denormalization
$object instanceof WrappedEvent $className == WrappedEvent::class
  • Serialization\Normalizers\TNCActivityStreams\TNCActivityStreamsNormalizer
    Norlimize a Event to be a Activity Streams Array or the reverse
Supported Normalization Supported Denormalization
$object instanceof TNCActivityStreamsEvent is_subclass_of($className, TNCActivityStreamsEvent::class)

Usage

In tnc-event-dispatcher, ActivityStreams is supported by TNCActivityStreamsNormalizer.

TNCActivityStreamsNormalizer only supports object which implements TNCActivityStreamsEvent interface(see above), If you check the source code of the Normalizer, it looks like this:

<?php
class TNCActivityStreamsNormalizer {

# ...    

    public function supportsNormalization($object)
    {
        return ($object instanceof TNCActivityStreamsEvent);
    }

    public function supportsDenormalization($data, $className)
    {
        if (!class_exists($className)) {
            return false;
        }

        return is_subclass_of($className, TNCActivityStreamsEvent::class);
    }

# ...        

}

So a proper Event which wants to be normalized as ActivityStreams format, it have to implement TNCActivityStreamsEvent interface.

<?php
class TestEvent implements TNCActivityStreamsEvent
{
    public function getTransportMode()

    public function normalize(ActivityBuilderInterface $builder)

    public function denormalize(Activity $activity)
}

When TNCActivityStreamsNormalizer do normalize, it will call the "normalize" method of the object, with a ActivityBuilderInterface implementation(default is DefaultActivityBuilder, can be changed as a constructor parameter of TNCActivityStreamsNormalizer). When do denormalize, it will call the "denormalize" method with a Activity instance(without call constructor), you can restore the Event here.

<?php
$normalizer = new TNCActivityStreamsNormalizer();
$event = new TestEvent();

$normalizedData = $normalizer->normalize($event);
$restoredEvent = $normalizer->denormalize($normalizedData, TestEvent::class);

DefaultActivityBuilder

TNCActivityStreamsNormalizer come with a default ActivityBuilderInterface implementation which is DefaultActivityBuilder, it can be used to create a Activity.
For more usage, visit the test cases.

<?php
$builder = new DefaultActivityBuilder();

# fill the Activity by array
$builder->setFromArray([
   'actor' => [
     'objectType' => 'type2',
     'id' => 'id4',
     'content' => 'content',
     'attachments' => [ # the rule of attachments is as same as ActivityObject
       ['subtype1', 'subid1'],
       'subid2',
       [
         'objectType' => 'subtype3',
         'id' => 'subid3'
       ]
     ]
   ]
 ]);

# use methods
$builder->setId('123');
$builder->setVerb('message.send');
$builder->setPublished('now');
$builder->setActor((new ActivityObject())->setId('123'));
...

# get the built Activity
$builder->getActivity();

User Cases

Case1: benn logged in

  • Event

I choose "user.login" as the EventName, Which is following the convention "namespace.verb"

Example Code:

<?php
class UserLoginEvent implements TNCActivityStreamsEvent
{
    const NAME = "user.login";

    private $user = null;

    public function __constructor(User $user) { $this->user = $user; }

    public function getTransportMode() { return self::TRANSPORT_MODE_ASYNC; }

    /**
     * @param \TNC\EventDispatcher\Serialization\Normalizers\TNCActivityStreams\DefaultActivityBuilder $builder 
     */
    public function normalize($builder)
    {
        $builder->setActor(
          (new ActivityObject())->setObjectType('user')->setId($this->user->getId())
        );
        return $builder->getActivity();
    }

    /**
     * @param \TNC\EventDispatcher\Serialization\Normalizers\TNCActivityStreams\Impl\Activity $activity 
     */
    public function denormalize($activity) 
    {
        $this->user = UsersRepository::find($activity->getActor()->getId());
    }
}

$dispatcher->dispatch(UserLoginEvent::NAME, new UserLoginEvent($user));
  • Formatted Data (ActivityStreams)
title user.login
verb login
actor { objectType: "user", id: 123456 }
id Generated Unique String
published 2017-10-13T11:31:34+08:00
provider { objectType: "community", id: "Poppen" }
generator { id: "tnc-event-dispatcher", content: { mode: "async", class: "UserLoginEvent" } }

Case2: benn visited fan's profile

Example Code:

<?php
class ProfileVisitEvent implements TNCActivityStreamsEvent
{
    const NAME = "user.visit";

    private $visitor = null;
    private $target = null;

    public function __constructor(User $visitor, User $target) { 
        $this->visitor = $visitor;
        $this->target = $target;
    }

    public function getTransportMode() { return self::TRANSPORT_MODE_ASYNC; }

    /**
     * @param \TNC\EventDispatcher\Serialization\Normalizers\TNCActivityStreams\DefaultActivityBuilder $builder 
     */
    public function normalize($builder)
    {
        $builder->setFromArray(
          [
            'actor' => ['user', $this->visitor->getId()],
            'object' => ['profile', $this->target->getName()]
          ]
        );
        return $builder->getActivity();
    }

    /**
     * @param \TNC\EventDispatcher\Serialization\Normalizers\TNCActivityStreams\Impl\Activity $activity 
     */
    public function denormalize($activity) 
    {
        $this->visitor = UsersRepository::find($activity->getActor()->getId());
        $this->target = UsersRepository::findByName($activity->getObject()->getId());
    }
}

$dispatcher->dispatch(ProfileVisitEvent::NAME, new ProfileVisitEvent($visitor, $target));
  • Formatted Data (ActivityStreams)
title user.visit
verb visit
actor { objectType: "user", id: 12345 }
object { objectType: "profile", id: "fan" }
id Generated Unique String
published 2017-10-13T11:31:34+08:00
provider { objectType: "community", id: "Poppen" }
generator { id: "tnc-event-dispatcher", content: { mode: "async", class: "UserLoginEvent" } }

Case3: benn sent a message to leo

Example Code:

<?php
class MessageSendEvent implements TNCActivityStreamsEvent
{
    const NAME = "message.send";

    private $sender = null;
    private $receiver = null;
    private $message = null;

    public function __constructor(User $sender, Message $message, User $receiver) { 
        $this->sender = $sender;
        $this->message = $message;
        $this->receiver = $receiver;
    }

    public function getTransportMode() { return self::TRANSPORT_MODE_ASYNC; }

    /**
     * @param \TNC\EventDispatcher\Serialization\Normalizers\TNCActivityStreams\DefaultActivityBuilder $builder 
     */
    public function normalize($builder)
    {
        $builder->setFromArray(
          [
            'actor' => ['user', $this->visitor->getId()],
            'object' => ['message', $this->message->getId()],
            'target' => ['user', $this->receiver->getId()]
          ]
        );
        return $builder->getActivity();
    }

    /**
     * @param \TNC\EventDispatcher\Serialization\Normalizers\TNCActivityStreams\Impl\Activity $activity 
     */
    public function denormalize($activity) 
    {
        $this->sender = UsersRepository::find($activity->getActor()->getId());
        $this->message = UsersRepository::find($activity->getObject()->getId());
        $this->receiver = UsersRepository::find($activity->getTarget()->getId());
    }
}

$dispatcher->dispatch(MessageSendEvent::NAME, new MessageSendEvent($visitor, $target));
  • Formatted Data (ActivityStreams)
title message.send
verb send
actor { objectType: "user", id: 12345 }
object { objectType: "message", id: 112231 }
target { objectType: "user", id: 88929 }
id Generated Unique String
published 2017-10-13T11:31:34+08:00
provider { objectType: "community", id: "Poppen" }
generator { id: "tnc-event-dispatcher", content: { mode: "async", class: "UserLoginEvent" } }

Formatter

JSON Formatter

TNC\EventDispatcher\Serialization\Formatters\JsonFormatter