Express social objects in Atom format

An introduction to the Activity Streams standard

The popularity of social networking sites has given rise to an emerging standard for web feeds that express what people are doing online. With Activity Streams, an extension to the Atom format, your websites can syndicate social activity. Explore how the Activity Streams format expresses social objects, learn how to build an activity-feed encoder in PHP, and discover some uses Activity Streams might serve in the enterprise.

Ben Werdmuller (ben@benwerd.com), Consultant and writer, Freelance

Photo of Ben WerdmullerBen Werdmuller is a web strategist and developer who specializes in open source platforms. He co-founded and was the technical lead for Elgg, an open source social networking framework. Ben blogs regularly at http://benwerd.com/.



15 June 2010

Also available in Chinese Japanese Portuguese

On the web, feeds are machine-readable summaries of content, usually arranged in reverse chronological order. Most feeds have traditionally been used to syndicate blog content in the popular RSS or Atom XML-based formats. Once published by a site, the content can be read through user-friendly aggregators, or transformed and interpreted by networked software products.

PHP preparation

To follow this article's PHP examples, you need PHP 5 or higher and an overall understanding of the language. See Resources for a link to the PHP download page.

Syndicated feeds have been consumed in this way since 1999. However, in recent years, web users have also been consuming content in a more social way, through sites like Facebook, MySpace, and Twitter. These sites operate by allowing people to mark other users as "friends" and aggregating friends' activities through unified dashboards. Rather than subscribing to a single content stream, users of these sites subscribe to individual people, expecting to see everything they create, upload, and share.

An activity stream (also sometimes called a lifestream) is the collection of all the activities a person undertakes on a particular site. As web users rely more and more on activity streams for information consumption, it makes sense to be able to syndicate and subscribe to activity stream data. But since RSS and Atom don't support social metadata, a new format is needed to syndicate social activity.

Frequently used acronyms

  • RSS: Really Simple Syndication
  • URI: Uniform Resource Identifiers
  • URL: Uniform Resource Locator
  • XML: Extensible Markup Language

Enter Activity Streams, an evolving standard that extends Atom for expressing social objects. Although it is a young standard, Activity Streams is fast becoming the de facto method for syndicating activity between web applications. For example, MySpace, Facebook, and TypePad all now produce Activity Streams XML feeds. But this technology isn't just for the consumer web environment. As corporate intranets and internal software become more social, solid business reasons support implementing Activity Streams as a feature. This article describes Activity Streams in detail, considers its potential uses in enterprise environments, and provides some examples for interpreting Activity Streams feeds using PHP.

Origin of Activity Streams

Activity Streams emerged from the DiSo Project (see Resources), an open source effort to build a decentralized social web using plug-ins developed for the WordPress blogging platform as a starting point. In the DiSo model, each user's profile is a separate WordPress blog that can be hosted on any Internet-connected infrastructure. Social actions then occur across the Internet among these WordPress sites.

A syndication format is an important part of this approach. In contrast with a site like Facebook, which houses hundreds of millions of members under one digital roof, DiSo assumes that there's only one user per WordPress site. Therefore, the only way to get an at-a-glance look at what your friends are doing is to subscribe to their feeds, parse the feeds into internal data structures, and view them in an aggregated interface.

XML is the perfect technology for implementing this approach, because it is cross-platform, easy to publish and parse, and doesn't require any specialist technology. The DiSo Project went one step further and conceived of the Activity Streams standard as an extension of the Atom feed format.

Atom was devised as an alternative to RSS that can be:

  • 100 percent vendor-neutral
  • Implemented by anybody
  • Freely extensible by anybody
  • Cleanly and thoroughly specified

Vendor neutrality, extensibility, and clean specifications are all essential to a future-proof standard. For the Activity Streams standard to be a viable linchpin of the decentralized social web, it must also adhere to these principles. And, thanks to Atom's extensibility, Activity Streams can leverage existing application logic and existing parsers. Theoretically, if you already have code to handle Atom (or use one of the many existing libraries), you need to develop only a little extra code.

In March 2009, MySpace became the first major social media provider to publish feeds in the Activity Streams format. Since then, many more have followed, including Facebook, Hulu, TypePad, and Opera. But the scope for Activity Streams isn't limited to sites like Facebook. Intranets, for example, can be greatly enhanced by knowledge of social activity within a company, or between companies. The Activity Streams format also creates possibilities for aggregating multiple streams to track a company's interactions and quantify progress towards goals across commercial social media sites. Whether seen from a management, analytical, algorithmic, or user perspective, the ability to track social usage presents many opportunities for new kinds of software applications.


Anatomy of an activity stream

Activity Streams extends Atom with a few new schema elements based on the http://activitystrea.ms/spec/1.0/ XML namespace.

An Atom feed consists of a single main feed element, which includes some initial metadata (for example, the title of the feed, any authors, its URL, and the URL of the overall content it refers to). Within this main element sit any number of entry elements, which define items of content within the feed. Listing 1 shows a sample Atom feed with a single entry element, derived from my own web site:

Listing 1. A sample Atom feed from the author's website
<?xml version="1.0" encoding="UTF-8"?>
<feed
  xmlns="http://www.w3.org/2005/Atom"
  xmlns:thr="http://purl.org/syndication/thread/1.0"
  xml:lang="en"
  xml:base="http://benwerd.com/wp-atom.php"
   >
    <title type="text">Ben Werdmuller von Elgg</title>
    <subtitle type="text"></subtitle>

    <updated>2010-05-01T13:13:04Z</updated>

    <link rel="alternate" type="text/html" href="http://benwerd.com" />
    <id>http://benwerd.com/feed/atom/</id>
    <link rel="self" type="application/atom+xml" href="http://benwerd.com
/feed/atom/" />

    <entry>
        <author>
            <name>Ben Werdmuller von Elgg</name>
            <uri>http://benwerd.com/</uri>
        </author>
        <title type="html"><![CDATA[Sample article title]]></title>
        <link rel="alternate" type="text/html" href="http://benwerd.com/2010/05
/sample-article-title/" />
        <id>http://benwerd.com/2010/05/sample-article-title/</id>
        <published>2010-05-01T13:06:22Z</published>
        <content type="html" >
            ...
        </content>
    </entry>

</feed>

Activity Streams extends the Atom model by inserting data about social objects within each entry element. The existing possibilities for child elements of the entry element aren't affected. For example, you can include a title and human-readable description of the action within title and content child elements, or include social metadata within a standard Atom-formatted content element.

An Activity Streams entry element always contains three main elements that indicate:

  • The actor: the person performing the action (example: John Doe)
  • The verb: the type of action being performed (example: added)
  • The object: the item being acted upon (example: Ben's photo)

In some circumstances, an entry element can also contain the target, a container object that the action was performed or placed in (for example: John's photo gallery in the action John Doe added Ben's photo to John's photo gallery).

The following elements are used within the entry element to define verbs, objects, actors, and targets:

  • The Activity:verb element contains an Atom URI defining the verb being used.
  • The Activity:object element contains a number of activity:object-type elements (described in the next paragraph), as well as other elements required to describe the action's object.
  • Actors are defined by the existing atom:author element, as shown in Listing 1.
  • The Activity:target element is similar to the activity:object element but specifically defines the target where it exists.

Within activity:object, activity:target, and atom:author elements are one or more Activity:object-type elements that contain an Atom URI defining the parent object.

Each type of verb and object has an Atom URI, which defines the properties it can hold. For example, Listing 2 shows some Atom URIs from the base Activity Streams specification that define a person, the verb to share, and a blog post:

Listing 2. Base Activity Streams URIs for people, sharing, and blog posts
http://activitystrea.ms/schema/1.0/person
http://activitystrea.ms/schema/1.0/share
http://activitystrea.ms/schema/1.0/article

These URIs correspond to human-readable entries in the Activity Streams specification.

In Listing 3, I converted the entry element from the example Atom feed in Listing 1 into an Activity Streams representation of Ben Werdmuller posting a blog entry:

Listing 3. A social version of Listing 1 in Activity Streams format
<?xml version="1.0" encoding="UTF-8"?>
<feed
  xmlns="http://www.w3.org/2005/Atom"
  xmlns:thr="http://purl.org/syndication/thread/1.0"
  xmlns:activity="http://activitystrea.ms/spec/1.0/"
  xml:lang="en"
  xml:base="http://benwerd.com/wp-atom.php"
   >
    <title type="text">Ben Werdmuller von Elgg</title>
    <subtitle type="text"></subtitle>

    <updated>2010-05-01T13:13:04Z</updated>

    <link rel="alternate" type="text/html" href="http://benwerd.com" />
    <id>http://benwerd.com/feed/atom/</id>
    <link rel="self" type="application/atom+xml" href="http://benwerd.com
/feed/atom/" />

    <entry>
        <id>http://benwerd.com/2010/05/sample-article-title/#actionID</id>
            <title type="text">Ben posted a blog entry.</title>
            <published>2010-05-01T13:06:22Z</published>
        <author>
            <name>Ben Werdmuller von Elgg</name>
            <uri>http://benwerd.com/</uri>
            <activity:object-type>
                http://activitystrea.ms/schema/1.0/person
            </activity:object-type>
        </author>
        <activity:verb>
            http://activitystrea.ms/schema/1.0/post
        </activity:verb>
        <activity:object>
            <title type="html"><![CDATA[Sample article title]]>
</title>
            <link rel="alternate" type="text/html"
 href="http://benwerd.com/2010/05/sample-article-title/" />
            <id>http://benwerd.com/2010/05/sample-article-title/</id>
            <published>2010-05-01T13:06:22Z</published>
            <content type="html" >
                ...
            </content>
            <activity:object-type>
                http://activitystrea.ms/schema/1.0/article
            </activity:object-type>
        </activity>
    </entry>
</feed>

Note here that:

  • The xmlns:activity=http://activitystrea.ms/spec/1.0/ namespace declaration has been added to the top of the feed in order to define the Activity Streams vocabulary.
  • The id of the overall Atom entry element and its contained activity:object element cannot be the same.
  • Details about the blog post—the ones in standard entry element in Listing 1—are now in the activity:object element, along with the required activity:object-type element.
  • The standard atom:title and atom:content elements have been retained for use with parsers that do not support Activity Streams feeds.

Although this action has only a single object, it is valid to embed multiple objects in an Activity Streams entry—for example, to modify Listing 3 to represent the action Ben posted two blog entries. The blog as a whole can be added as an activity:target element to represent the semantically slightly different action Ben added two entries to his blog.

I deliberately used basic content types in the feed example in Listing 3. However, the Activity Streams specification allows for specialization of verbs and objects. For example, a collaborative project management application might indicate when users complete tasks. The software might define a specialized verb for complete, and a specialized object for task, each with its own custom Atom URI. However, many Activity Streams-compatible tools might not have any knowledge of these terms, which can cause problems during aggregation. To reduce the probability that this will occur, entries in Activity Streams feeds can include more than one verb that describe the same activity. In these cases, it is a good idea to include the entire set of ancestors for a particular verb or object. Complete might simply be a specialization of the base verb update, and Task might be a specialization of the base object note. The resulting Activity Streams entry might look like Listing 4:

Listing 4. A hypothetical completed task in an Activity Streams feed
<entry>
    <id>http://samplecompany.com/tasks/activity/23432/3242345/</id>
    <title type="text">Roger Taylor completed a task.</title>
    <published>2010-05-01T13:06:22Z</published>
    <author>
        <name>Roger Taylor</name>
        <uri>http://samplecompany.com/people/Roger+Taylor/</uri>
        <activity:object-type>
            http://activitystrea.ms/schema/1.0/person
        </activity:object-type>
    </author>
    <activity:verb>
        http://samplecompany.com/activity/schema/1.0/complete
    </activity:verb>
    <activity:verb>
        http://activitystrea.ms/schema/1.0/update
    </activity:verb>
    <activity:object>
        <title type="html"><![CDATA[Sample task]]></title>
        <link rel="alternate" type="text/html" href="http://samplecompany.com
/tasks/23432/" />
        <id>http://samplecompany.com/tasks/23432/</id>
        <published>2010-05-01T13:06:22Z</published>
        <content type="html" >
            ...
        </content>
        <activity:object-type>
            http://samplecompany.com/activity/schema/1.0/task
        </activity:object-type>
        <activity:object-type>
            http://activitystrea.ms/schema/1.0/note
        </activity:object-type>
    </activity>
</entry>

Note here that:

  • Each object type URI gets its own activity:object-type element.
  • Each verb URI gets its own activity:verb element.
  • Specialized object and verb types must have their own specialized URIs.

At the time I'm writing this article, the Activity Streams wiki documentation lists 21 verbs and 25 object types, with 11 base verbs and 19 base object types defined in the formal specification document. An Activity Streams parser should recognize each of these base verbs and objects, and handle them appropriately.


Building an Activity Streams encoder

The simplicity of the Activity Streams standard makes it easy to build an encoder. Here you'll create a basic PHP encoder object that represents an Activity Streams feed when you convert it to a string (for example, by using the PHP echo statement with it). See Download for the full source code.

First, you create the stub class, ActivityStreamEncoder, and define its constructor method to take an id, title, and description for the entry. You also define a private array, $entries, to hold your Activity Streams entries. Listing 5 shows the class and constructor:

Listing 5. Base ActivityStreamEncoder class with constructor
class ActivityStreamsEncoder {

    private $id = "";
    private $title = "";
    private $link = "";
    private $entries = array();
    
    function __construct($id, $title = '', $description = '') {
        $this->id = $id;
        $this->title = $title;
        $this->description = $description;
    }    
}

Next, you add some base classes for entries, objects, and authors. Because verbs simply consist of URIs, you reference them as strings throughout. Entries and objects will set their published date to the current date (using the PHP time() function) by default. Listing 6 shows the base class for entries:

Listing 6. Base ActivityStreamEntry class
class ActivityStreamsEntry {
    
    public $id = "";
    public $title = "";
    public $author;
    public $objects = array();
    public $verbs = array();
    public $published;
    
    function __construct() {
        $this->published = time();
    }
    
    function addObject(ActivityStreamsObject $object) {
        $this->objects[] = $object;
    }
    function addVerb(string $verb) {
        $this->verbs[] = $verb;
    }
    function setAuthor(ActivityStreamsAuthor $author) {
        $this->author = $author;
    }
    
}

The ActivityStreamsEntry class also includes simple helper methods for adding an author, objects, and verbs to the entry, ensuring the proper variable type of these values.

Because the child elements of an activity:object element are variable, you dynamically get and set the properties of ActivityStreamsObject —your object class—using custom getProperty() and setProperty() methods, respectively. This is defined in Listing 7, which shows the ActivityStreamsObject and the author class, which is a child class of ActivityStreamsObject.

Listing 7. The Activity Streams object and author encoder classes
class ActivityStreamsObject {
    public $properties = array();
    
    function getProperty($property_name) {
        if (isset($this->properties[$property_name])) 
return $this->properties[$property_name];
    }
    function setProperty($property_name, $property_value) {
        $this->properties[$property_name] = $property_value;
    }
    function __construct() {
        $this->published = time();
    }

    function addObjectType($object_type) {
        if (!isset($this->properties['object-type'])) $this->properties
                 ['object-type'] = array();
        $this->properties['object-type'][] = (string) $object_type;
    }
    
    function __toString() {
        $string = '';
        $string .= "\n<activity:object>";
        foreach($this->properties as $property => $value) {
            if (!is_array($value)) $value = array($value);
            switch($property) {
                case 'title':
                            $attr = 'type="html"';
                            break;
                case 'link':
                            $attr = 'rel="alternate" type="text/html" 
href="'.$value[0].'"';
                            $value = array(''); 
                            break;
                default:    $attr = '';
                            break;
            }
            if (sizeof($value))
                foreach($value as $val) {
                    if (empty($val))
                        $string .=  "\n\t<{$property} {$attr} />";
                    else if ($property == 'content' || $property == 'title')
                        $string .= "\n\t<{$property}
 {$attr}><![CDATA[{$val}]]></{$property}>";
                    else
                        $string .=  "\n\t<{$property} {$attr}>
{$val}</{$property}>";
                }
        }
        $string .=  "\n</activity:object>";
        
        return $string;
    }
    
}

class ActivityStreamsAuthor extends ActivityStreamsObject {
    function __construct() {
        $this->addObjectType("http://activitystrea.ms/schema/1.0/person");
    }
    function __toString() {
        $string = parent::__toString();
        $string = str_replace('activity:object>','author>',$string);
        return $string;
    }
}

Here, your classes represent a structured version of the activity:object and atom:author elements, respectively. When you try to convert them to a string (for example, by echoing them to the browser), structured XML is automatically returned. By ensuring that the equivalent PHP class for each Activity Streams element is responsible for its own XML representation, you can create a clean set of Activity Streams handler objects.

In Listing 8, you add a similar class to represent the overall entry element:

Listing 8. PHP class representing an Activity Streams entry element
class ActivityStreamsEntry {

    public $id = "";
    public $title = "";
    public $author;
    public $objects = array();
    public $verbs = array();
    public $published;
    
    function __construct() {
        $this->published = time();
    }
    
    function addObject(ActivityStreamsObject $object) {
        $this->objects[] = $object;
    }
    function addVerb($verb) {
        $this->verbs[] = $verb;
    }
    function setAuthor(ActivityStreamsAuthor $author) {
        $this->author = $author;
    }

    function __toString() {
        
        $string = '';
        $string .=  "\n<entry>";
        $published = date('c',$this->published);
        $string .=  <<< END
        <id>{$this->id}</id>
        <title type="text"><![CDATA[{$this->title}]]></title>

        <published>{$published}</published>
END;
        if ($this->author instanceof ActivityStreamsAuthor) $string .=  
$this->author;
        if (sizeof($this->verbs)) foreach($this->verbs as $verb)
            $string .=  "\n<activity:verb>{$verb}</activity:verb>";
        if (sizeof($this->objects)) foreach($this->objects as $object)
            if ($object instanceof ActivityStreamsObject) $string .=  $object;
        $string .=  "\n</entry>";
        return $string;    
    }
}

In Listing 8, the code also includes addVerb(), addObject(), and setAuthor() methods to handle the Activity Streams activity:verb, activity:object, and atom:author elements within the entry element.

Finally, you can modify your overall ActivityStreamsEncoder class to contain a corresponding addEntry() method (to add your ActivityStreamsEntry objects from Listing 8 to the stream) and a __toString() magic method to echo the entire Activity Streams feed. The __toString() method includes the XML header and namespace declarations required for correct use of the Activity Streams vocabulary. Listing 9 shows the addEntry() and __toString() methods:

Listing 9. addEntry() and __toString() methods for the ActivityStreamsEncoder class in Listing 5
function addEntry(ActivityStreamsEntry $entry) {
        $this->entries[] = $entry;
    }
    
    function __toString() {
        // Display header
        $string = '';
        $updated_time = date('c',time());
        $string .=  <<<END
<?xml version="1.0" encoding="UTF-8"?>
<feed
xmlns="http://www.w3.org/2005/Atom"
xmlns:thr="http://purl.org/syndication/thread/1.0"
xmlns:activity="http://activitystrea.ms/spec/1.0/"
xml:lang="en"
>

<title type="text">{$this->title}</title>
<updated>{$updated_time}</updated>

<link rel="alternate" type="text/html" href="{$this->link}" />
<id>{$this->id}</id>
<link rel="self" type="application/atom+xml" href="{$this->id}" />

END;
        if (sizeof($this->entries))
            foreach($this->entries as $entry)
                if ($entry instanceof ActivityStreamsEntry) $string .= (string) $entry;
                
        $string .=  <<<END
</feed>
END;
        return $string;
    }

And that's it! Now, to put together a structured Activity Streams feed, you connect a series of these objects and echo them to the browser, as in Listing 10:

Listing 10. Rendering the Activity Streams feed from Listing 4 using the PHP code from Listings 5 through 9
// Create a new Activity Stream
$stream = new ActivityStreamsEncoder('http://samplecompany.com/tasks/activity/', 
     'Task activities at Sample Company');

// Define the action's object
$object = new ActivityStreamsObject();
$object->setProperty('id','http://samplecompany.com/tasks/23432/');
$object->setProperty('title','Sample task.');
$object->setProperty('content','...');
$object->addObjectType('http://samplecompany.com/activity/schema/1.0/task');
$object->addObjectType('http://activitystrea.ms/schema/1.0/note');
$object->setProperty('link','http://samplecompany.com/tasks/23432/');

// Define the action's author
$author = new ActivityStreamsAuthor();
$author->setProperty('name','Roger Taylor');
$author->setProperty('uri','http://samplecompany.com/people/Roger+Taylor/');

// Define the overall entry, adding the object, verbs and author
$entry = new ActivityStreamsEntry();
$entry->addVerb("http://samplecompany.com/activity/schema/1.0/complete");
$entry->addVerb("http://activitystrea.ms/schema/1.0/update");
$entry->title = "Roger Taylor completed a task.";
$entry->id = 'http://samplecompany.com/tasks/activity/23432/3242345/';
$entry->addObject($object);
$entry->setAuthor($author);

// Add the entry to the stream
$stream->addEntry($entry);

// Echo the stream
header('Content type: text/xml');
echo $stream;

The code in Listing 10 maintains a structured version of the activity stream, which you can query using standard object-oriented PHP method and property calls. Once echo is called to display the feed, each object's internal __toString() magic method is called, rendering it as XML. A Content type: text/xml header is sent first to advertise to the browser that you'll be displaying an XML file.


Parsing an Activity Streams feed

PHP makes it extremely simple to parse any kind of XML feed. Simply load the structure with the built-in SimpleXML functions:

Parsing with PHP

Elliotte Rusty Harold's article "SimpleXML Processing with PHP" provides an excellent overview of parsing well-formatted XML—including Atom—in PHP.

  • Simplexml_load_file($filename) loads structured XML from a file.
  • Simplexml_load_string($filename) loads structured XML from a string.

A number of obvious modifications for your ActivityStreams classes suggest themselves. One is to unify each of your child classes into a single multipurpose ActivityStreamsElement class, which inherits the built-in PHP class SimpleXMLElement. You can then simply call SimpleXML to process a feed from XML back into these handler objects:

$activityStream = simplexml_load_string($xmlFeed, "ActivityStreamsElement");

But handling a stream as a structured series of SimpleXMLElements, which is the default value returned by the SimpleXML parsing functions, is as easy as looping through the elements in an array, as shown in Listing 11. You must take care, however, to reference both the Atom and ActivityStreams namespaces.

Listing 11. Looping through elements in a SimpleXML Atom feed
$activityStream = simplexml_load_string($stream);
$activityStream =  $activityStream->children('http://www.w3.org/2005/Atom');
if (is_array(!$activityStream->entry) && sizeof($activityStream->entry)) {
    foreach($activityStream->entry as $entry) {
        // Handle entry
    }
}

You can develop recursive functions to handle entries, objects within entries, authors, and so on. Activity Streams feeds, like their parent Atom feeds, are simple XML files that can be parsed quickly and used for many purposes.


Conclusion

The Activity Streams format is emerging as the social web application's answer to RSS: simple and easy to develop for, but powerful in a wide range of contexts. In enterprise settings, numerous possibilities exist for extending Activity Streams to provide greater security and more elaborate functionality. In addition to creating specialized object and verb types to handle business activities, you can sign streams using a technology such as OAuth to create custom, permissions-based views of company activity. And the proposed Atom Media extension allows media items such as photographs, video and audio files, and business presentations to be embedded within an Activity Streams feed.


Download

DescriptionNameSize
Article source codeActivityStreamsEncoder.zip2KB

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into XML on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=XML, Open source
ArticleID=495863
ArticleTitle=Express social objects in Atom format
publish-date=06152010