Building Activity Streams using Symfony2, Doctrine2 and MongoDB post

Posted on 2013-07-02 by jwage


NOTE The example code in this blog post has been simplified to make things more concise and easy to read.

At OpenSky we utilize Symfony2, Doctrine2 and MongoDB for streams of activity across our site. We’ve based our implementation on the JSON Activity Streams 1.0 specification. This blog post demonstrates how you can easily setup your own activity streams.

An activity item consists of a published date, actor, object, target and verb. It describes an action performed by an entity. Here is an example JSON document of what an activity item might look like:

{
  "published": "2011-02-10T15:04:55Z",
  "actor": {
    "url": "http://example.org/martin",
    "objectType" : "person",
    "id": "tag:example.org,2011:martin",
    "image": {
      "url": "http://example.org/martin/image",
      "width": 250,
      "height": 250
    },
    "displayName": "Martin Smith"
  },
  "verb": "post",
  "object" : {
    "url": "http://example.org/blog/2011/02/entry",
    "id": "tag:example.org,2011:abc123/xyz"
  },
  "target" : {
    "url": "http://example.org/blog/",
    "objectType": "blog",
    "id": "tag:example.org,2011:abc123",
    "displayName": "Martin's Blog"
  }
}

To get started implementing this let’s define a base PHP class that we can use to represent an activity item.

<?php

namespace Project\Document;

use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;

/**
 * @ODM\MappedSuperclass(collection="activity_items")
 * @ODM\InheritanceType("SINGLE_COLLECTION")
 * @ODM\DiscriminatorField(fieldName="verb")
 * @ODM\DiscriminatorMap({
 *     "love" = "Project\Document\LoveActivityItem"
 * })
 */
abstract class AbstractActivityItem
{
    /** @ODM\Date */
    protected $published;

    protected $actor;
    protected $object;
    protected $target;

    // ...
}

For this blog post we’re only going to implement one verb called love. On OpenSky you can love products and the action is pushed in to the feed of your followers. So let’s get started implementing the LoveActivityItem class:

<?php

namespace Project\Document;

use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Project\Document\Love;
use Project\Document\Product;
use Project\Document\User;

/** @ODM\Document */
class LoveActivityItem extends AbstractActivityItem
{
    /** @ODM\ReferenceOne(targetDocument="User") */
    protected $actor;

    /** @ODM\ReferenceOne(targetDocument="Love") */
    protected $object;

    /** @ODM\ReferenceOne(targetDocument="Product") */
    protected $target;

    public function __construct(
        User $user, Love $love, Product $product
    )
    {
        $this->actor = $user;
        $this->object = $love;
        $this->target = $product;
    }

    // ...
}

Now that we have our basic model defined, let’s implement the code that will wire everything up. Assume we already have a Symfony event in our application being notified called user.love. So setup a listener that listens to that event and records the activity item:

<?php

namespace Project/Listener;

class ActivityListener
{
    /**
     * @var Project\Activity\ActivityManager
     */
    private $activityManager;

    /**
     * Listens to the `user.love` event.
     */
    public function onUserLove(UserLoveEvent $event)
    {
        $this->activityManager->recordUserLove(
            $event->getUser(),
            $event->getLove()
        );
    }
}

Now let’s implement the ActivityManager class with the recordUserLove method:

<?php

namespace Project\Activity;

use Doctrine\ODM\MongoDB\DocumentManager;
use Project\Document\LoveActivityItem;
use Project\Document\Love;
use Project\Document\User;

class ActivityManager
{
    private $dm;

    public function __construct(DocumentManager $dm)
    {
        $this->dm = $dm;
    }

    public function recordUserLove(User $user, Love $love)
    {
        $this->dm->persist(
            new LoveActivityItem($user, $love, $love->getProduct())
        );
    }
}

The new activity item will get flushed to the database at the end of the action with the love since we flush in the controller where the user.love event is notified. Now it is possible to generate a feed of activity items for the whole site, a specific user or even a set of users:

<?php

$user = $dm->getRepository('Project\Document\User')->findOneByEmail('[email protected]');

$activityItems = $dm->getRepository('Project\Document\ActivityItem')
    ->createQueryBuilder()
    ->field('actor')->references($user)
    ->getQuery()
    ->execute();

That is it. You can easily add new verbs to the model and add code to the listener and manager to record new activities. Good luck! I hope this was helpful for someone!

Categories: articles