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.
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.
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.
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.
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:verbelement contains an Atom URI defining the verb being used. - The
Activity:objectelement contains a number ofactivity:object-typeelements (described in the next paragraph), as well as other elements required to describe the action's object. - Actors are defined by the existing
atom:authorelement, as shown in Listing 1. - The
Activity:targetelement is similar to theactivity:objectelement 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
entryelement and its containedactivity:objectelement cannot be the same. - Details about the blog post—the ones in standard
entryelement in Listing 1—are now in theactivity:objectelement, along with the requiredactivity:object-typeelement. - The standard
atom:titleandatom:contentelements 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-typeelement. - Each verb URI gets its own
activity:verbelement. - 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:
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.
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.
| Description | Name | Size | Download method |
|---|---|---|---|
| Article source code | ActivityStreamsEncoder.zip | 2KB | HTTP |
Information about download methods
Learn
- Activity Streams: Visit the go-to resource for updates to the Activity Streams standard.
- DiSo Project:
Learn more about this initiative to facilitate the creation of open, nonproprietary, and interoperable building blocks for the decentralized social web.
- Adding
richness to activity streams (Chris Messina, June 2008): Read the blog post that introduced Activity Streams to the world.
- Creating an Atom feed in PHP (Brian M. Carey, developerWorks, July 2009): Read an introduction to creating Atom feeds using PHP.
- SimpleXML processing with PHP (Elliotte Rusty Harold, developerWorks, October 2006): Get an introduction to processing XML feeds (including Atom) using PHP.
- Atom (standard): Check out the Wikipedia overview of the Atom specification.
- RFC 4827 - The Atom Syndication Format: Read the Atom specification for details on this XML-based document format that describes lists of related information, known as feeds.
- RFC 3339 - Date and Time on the Internet: Timestamps: Explore the date/time specification used by Atom.
- Atom wiki: Take a look at the official Atom reference.
- PHP tutorial: Learn the basics of PHP.
- Register for these free developerWorks tutorials to learn how to
program with PHP:
- Learning PHP, Part 1 (Tyler Anderson and Nicholas Chase, developerWorks, June 2002): Explore the basics of PHP through HTML forms and database connections.
- Learning PHP, Part 2 (Tyler Anderson and Nicholas Chase, developerWorks, June 2002): Learn how to upload files as well as how to create, manipulate, and read XML data.
- Learning PHP, Part 3 (Tyler Anderson and Nicholas Chase, developerWorks, July 2002): Learn how to stream files and create object methods and exceptions.
- PHP project resources: Check out this extensive array of developerWorks resources for PHP developers.
- My developerWorks: Personalize your developerWorks experience.
- IBM XML certification: Find out how you can become an IBM-Certified Developer in XML and related technologies.
- XML technical library: See the developerWorks XML Zone for a wide range of technical articles and tips, tutorials, standards, and IBM Redbooks.
- developerWorks technical events and webcasts: Stay current with technology in these sessions.
- developerWorks on Twitter: Join today to follow developerWorks' tweets.
- developerWorks
podcasts: Listen to interesting interviews and discussions for software developers.
Get products and technologies
- PHP: Get PHP. You need version 5 or higher for this article.
- IBM product evaluation versions: Download or explore the online trials in the IBM SOA Sandbox and get your hands on application development tools and middleware products from DB2®, Lotus®, Rational®, Tivoli®, and WebSphere®.
Discuss
- XML zone discussion forums: Participate in any of several XML-related discussions.
- developerWorks blogs: Check out these blogs and get involved.

Ben 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/.




