Friday 5 February 2010

Utilizing Twitter lists with Zend_Service_Twitter

Twitter lists with the Zend FrameworkSeveral months ago Twitter added the list feature to it's public API. While debating some use cases for an event registration application I stumbled upon an interesting feature, which adds participants automatically to a Twitter list upon registration. This way registered and interested users can discover like-minded individuals and get in touch prior to any pre-social event activities. This post will show how this feature can be implemented by utilizing the Zend_Service_Twitter component, and how it then can be used in a Zend Framework based application.

Implementing the common list features

Looking at the three relevant parts of the Twitter list API some common features emerged and had to be supported to get the feature out of the door. These are namely the creation, deletion of new lists and the addition, removal of list members (i.e. event participants). Since the current Twitter component doesn't support these list operations out of the box it was time to put that develeoper hat on and get loose; which was actually a joy due to the elegance of the extended Zend_Service_Twitter component laying all the groundwork.

A non-feature-complete implementation is shown in the next code listing and can alternatively be pulled from GitHub. Currently it only supports the above stated common operations plus the ability to get the lists of a Twitter account and it's associated members; but feel free to fork it or even turn it into an official proposal.
<?php

require_once 'Zend/Service/Twitter.php';
require_once 'Zend/Service/Twitter/Exception.php';

class Recordshelf_Service_Twitter_List extends Zend_Service_Twitter
{
const LIST_MEMBER_LIMIT = 500;
const MAX_LIST_NAME_LENGTH = 25;
const MAX_LIST_DESCRIPTION_LENGTH = 100;

/**
* Initializes the service and adds the list to the method types
* of the parent service class.
*
* @param string $username The Twitter account name.
* @param string $password The Twitter account password.
* @see Zend_Service_Twitter::_methodTypes
*/
public function __construct($username = null, $password = null)
{
parent::__construct($username, $password);
$this->_methodTypes[] = 'list';
}
/**
* Creates a list associated to the current user.
*
* @param string $listname The listname to create.
* @param array $options The options to set whilst creating the list.
* Allows to set the list creation mode (public|private)
* and the list description.
* @return Zend_Rest_Client_Result
* @throws Zend_Service_Twitter_Exception
*/
public function create($listname, array $options = array())
{
$this->_init();

if ($this->_existsListAlready($listname)) {
$exceptionMessage = 'List with name %s exists already';
$exceptionMessage = sprintf($exceptionMessage, $listname);
throw new Zend_Service_Twitter_Exception($exceptionMessage);
}

$_options = array('name' => $this->_validListname($listname));
foreach ($options as $key => $value) {
switch (strtolower($key)) {
case 'mode':
$_options['mode'] = $this->_validMode($value);
break;
case 'description':
$_options['description'] = $this->_validDescription($value);
break;
default:
break;
}
}
$path = '/1/%s/lists.xml';
$path = sprintf($path, $this->getUsername());

$response = $this->_post($path, $_options);
return new Zend_Rest_Client_Result($response->getBody());
}
/**
* Deletes an owned list of the current user.
*
* @param string $listname The listname to delete.
* @return Zend_Rest_Client_Result
* @throws Zend_Service_Twitter_Exception
*/
public function delete($listname)
{
$this->_init();

if (!$this->_isListAssociatedWithUser($listname)) {
$exceptionMessage = 'List %s is not associate with user %s ';
$exceptionMessage = sprintf($exceptionMessage,
$listname,
$this->getUsername()
);
throw new Zend_Service_Twitter_Exception($exceptionMessage);
}
$_options['_method'] = 'DELETE';
$path = '/1/%s/lists/%s.xml';
$path = sprintf($path,
$this->getUsername(),
$this->_validListname($listname)
);
$response = $this->_post($path, $_options);
return new Zend_Rest_Client_Result($response->getBody());
}
/**
* Adds a member to a list of the current user.
*
* @param integer $userId The numeric user id of the member to add.
* @param string $listname The listname to add the member to.
* @return Zend_Rest_Client_Result
* @throws Zend_Service_Twitter_Exception
*/
public function addMember($userId, $listname)
{
$this->_init();

if (!$this->_isListAssociatedWithUser($listname)) {
$exceptionMessage = 'List %s is not associate with user %s ';
$exceptionMessage = sprintf($exceptionMessage,
$listname,
$this->getUsername()
);
throw new Zend_Service_Twitter_Exception($exceptionMessage);
}

$_options['id'] = $this->_validInteger($userId);
$path = '/1/%s/%s/members.xml';
$path = sprintf($path,
$this->getUsername(),
$this->_validListname($listname)
);

if ($this->_isListMemberLimitReached($listname)) {
$exceptionMessage = 'List can contain no more than %d members';
$exceptionMessage = sprintf($exceptionMessage,
self::LIST_MEMBER_LIMIT
);
throw new Zend_Service_Twitter_Exception($exceptionMessage);
}

$response = $this->_post($path, $_options);
return new Zend_Rest_Client_Result($response->getBody());
}
/**
* Removes a member from a list of the current user.
*
* @param integer $userId The numeric user id of the member to remove.
* @param string $listname The listname to remove the member from.
* @return Zend_Rest_Client_Result
* @throws Zend_Service_Twitter_Exception
*/
public function removeMember($userId, $listname)
{
$this->_init();

if (!$this->_isListAssociatedWithUser($listname)) {
$exceptionMessage = 'List %s is not associate with user %s ';
$exceptionMessage = sprintf($exceptionMessage,
$listname,
$this->getUsername()
);
throw new Zend_Service_Twitter_Exception($exceptionMessage);
}

$_options['_method'] = 'DELETE';
$_options['id'] = $this->_validInteger($userId);
$path = '/1/%s/%s/members.xml';
$path = sprintf($path,
$this->getUsername(),
$this->_validListname($listname)
);
$response = $this->_post($path, $_options);
return new Zend_Rest_Client_Result($response->getBody());
}
/**
* Fetches the list members of the current user.
*
* @param string $listname The listname to fetch members from.
* @return Zend_Rest_Client_Result
* @throws Zend_Service_Twitter_Exception
*/
public function getMembers($listname) {
$this->_init();
$path = '/1/%s/%s/members.xml';
$path = sprintf($path,
$this->getUsername(),
$this->_validListname($listname)
);
$response = $this->_get($path);
return new Zend_Rest_Client_Result($response->getBody());
}
/**
* Fetches the list of the current user or any given user.
*
* @param string $username The username of the list owner.
* @return Zend_Rest_Client_Result
*/
public function getLists($username = null)
{
$this->_init();
$path = '/1/%s/lists.xml';
if (is_null($username)) {
$path = sprintf($path, $this->getUsername());
} else {
$path = sprintf($path, $username);
}
$response = $this->_get($path);
return new Zend_Rest_Client_Result($response->getBody());
}
/**
* Checks if the list exists already to avoid number
* indexed recreations.
*
* @param string $listname The list name.
* @return boolean
* @throws Zend_Service_Twitter_Exception
*/
private function _existsListAlready($listname)
{
$_listname = $this->_validListname($listname);
$lists = $this->getLists();
$_lists = $lists->lists;
foreach ($_lists->list as $list) {
if ($list->name == $_listname) {
return true;
}
}
return false;
}
/**
* Checks if the list is associated with the current user.
*
* @param string $listname The list name.
* @return boolean
*/
private function _isListAssociatedWithUser($listname)
{
return $this->_existsListAlready($listname);
}
/**
* Checks if the list member limit is reached.
*
* @param string $listname The list name.
* @return boolean
*/
private function _isListMemberLimitReached($listname)
{
$members = $this->getMembers($listname);
return self::LIST_MEMBER_LIMIT < count($members->users->user);
}
/**
* Returns the list creation mode or returns the private mode when invalid.
* Valid values are private or public.
*
* @param string $creationMode The list creation mode.
* @return string
*/
private function _validMode($creationMode)
{
if (in_array($creationMode, array('private', 'public'))) {
return $creationMode;
}
return 'private';
}
/**
* Returns the list name or throws an Exception when invalid.
*
* @param string $listname The list name.
* @return string
* @throws Zend_Service_Twitter_Exception
*/
private function _validListname($listname)
{
$len = iconv_strlen(trim($listname), 'UTF-8');
if (0 == $len) {
$exceptionMessage = 'List name must contain at least one character';
throw new Zend_Service_Twitter_Exception($exceptionMessage);
} elseif (self::MAX_LIST_NAME_LENGTH < $len) {
$exceptionMessage = 'List name must contain no more than %d characters';
$exceptionMessage = sprintf($exceptionMessage,
self::MAX_LIST_NAME_LENGTH
);
throw new Zend_Service_Twitter_Exception($exceptionMessage);
}
return trim($listname);
}
/**
* Returns the list description or throws an Exception when invalid.
*
* @param string $description The list description.
* @return string
* @throws Zend_Service_Twitter_Exception
*/
private function _validDescription($description)
{
$len = iconv_strlen(trim($description), 'UTF-8');
if (0 == $len) {
return '';
} elseif (self::MAX_LIST_DESCRIPTION_LENGTH < $len) {
$exceptionMessage = 'List description must contain no more than %d characters';
$exceptionMessage = sprintf($exceptionMessage,
self::MAX_LIST_DESCRIPTION_LENGTH
);
throw new Zend_Service_Twitter_Exception($exceptionMessage);
}
return trim(strip_tags($description));
}
}

Adding the 'auto list' feature

For using the above implemented add member feature it's assumed that a participant has provided a valid and existing Twitter username, his approval of being added to the event list (i.e. zfweekend) and that he further has been registered effectively. To have the name of the Twitter list to act on and the account credentials available corresponding configuration entries are set as shown next.

application/configs/application.ini

[production]
twitter.username = __USERNAME__
twitter.password = __PASSWORD__
twitter.auto.listname = zfweekend
With the Twitter credentials and the list name available it's now possible to pull this feature into the register method of the register Action Controller, where it's applied as shown in the outro listing. As you will see, besides some bad practices due to demonstration purposes, the register Form makes use of a custom TwitterScreenName validator and filter which are also available via GitHub. Happy Twitter listing!
<?php

class RegisterController extends Zend_Controller_Action
{
/**
* @badpractice Push this into a specific Form class.
* @return Zend_Form
*/
private function _getForm()
{
$form = new Zend_Form();
$form->setAction('/register/register')
->setMethod('post');
$twitterScreenName = $form->createElement('text', 'twitter_screen_name',
array('label' => 'Twittername: ')
);
$twitterScreenName->addValidator(new Recordshelf_Validate_TwitterScreenName())
->setRequired(true)
->setAllowEmpty(false)
->addFilter(new Recordshelf_Filter_TwitterScreenName());

$autoListApproval = $form->createElement('checkbox', 'auto_list_approval',
array('label' => 'I approved to be added to the event Twitter list: ')
);

$form->addElement($twitterScreenName)
->addElement($autoListApproval)
->addElement('submit', 'register', array('label' => ' Register '));

return $form;
}
public function indexAction()
{
$this->view->form = $this->_getForm();
}
public function thanksAction()
{
}
/**
* @badpractice Handle possible Exception of
* Recordshelf_Service_Twitter_List::addMember.
* @return Recordshelf_Service_Twitter_List
*/
public function registerAction()
{
$this->_helper->viewRenderer->setNoRender();
$form = $this->_getForm();
$request = $this->getRequest();

if ($this->getRequest()->isPost()) {
if ($form->isValid($request->getPost())) {

$model = new Recordshelf_Model_Participant($form->getValues());
$model->save();

if ($form->getElement('auto_list_approval')->isChecked()) {
$twitterScreenName = $form->getValue('twitter_screen_name');
$twitter = $this->_getTwitterListService();
$response = $twitter->user->show($twitterScreenName);
$userId = (string) $response->id;
$response = $twitter->list->addMember($userId,
$this->_getTwitterListName());

$model->hasBeenAddedToTwitterList(true);
$model->update();

return $this->_helper->redirector('thanks');
}
} else {
return $this->_helper->redirector('index');
}
}
}
/**
* @badpractice Push this into a dedicated Helper or something similar.
* @return Recordshelf_Service_Twitter_List
*/
private function _getTwitterListService()
{
$config = Zend_Registry::get('config');
return new Recordshelf_Service_Twitter_List(
$config->twitter->get('username'),
$config->twitter->get('password')
);
}
/**
* @badpractice Push this into a dedicated Helper or something similar.
* @return string
*/
private function _getTwitterListName()
{
$config = Zend_Registry::get('config');
return $config->twitter->auto->get('listname');
}
}


5 comments:

Matthew Weier O'Phinney said...

Any way I could convince you to contribute these features back to Zend_Service_Twitter? :)

Raphael Stolt said...

Yes, might need some mentoring in writing the tests for the list Twitter API part. ;D

motercalo said...

This blog is very nice and informative.Thank you for the great story.

Unknown said...

This is great. Though when using Twitter Oauth, I am unable to remove members from a list. The twitter response reads "incorrect signature". Any ideas?

winamax said...

This blog is very nice and informative.