Posted on August 6, 2009
This is really something I've been wanting to point out because, for one, I very much like and agree with the approach, and second, it's something that any developer using the Zend Framework should digest and take into consideration when writing their own code.
There are numerous components that will accept configuration options, and usually that method is called setOptions($array) and it accepts an associative array (key/value pairs) as parameter.
And often, setOptions() iterates over the array and calls setOption($key, $value) as demonstrated by the following example:
public function setOptions($options)
{
foreach ($options as $option => $value) {
$this->setOption($option, $value);
}
return $this;
}
And typically, setOption() takes the first parameter (the key) and checks whether there's a method that matches "set" followed by the key name. For example, setOption('active', true) will end up calling setActive(true). The code often looks similar to the following:
public function setOption($option, $value)
{
$method = 'set' . $option;
if (method_exists($this, $method)) {
$this->$method($value);
} else {
throw new Exception('Unknown option: ' . $option);
}
return $this;
}
This comes in handy when you're trying to configure a component that accepts a plethora of options, which you can then conveniently store in an array.
Furthermore, the constructor of a class could accept $options as first parameter (as is often the case), and also check whether it is an instance of Zend_Config, in which case it first converts it using $options = $options->toArray();
public function __construct($options = null)
{
if ($options instanceof Zend_Config) {
$options = $options->toArray();
}
if (is_array($options)) {
$this->setOptions($options);
}
}
Another convention you may have noticed is that the setter methods tend to "return $this;". This is known as fluent interface and allows for more concise, readable code by being able to chain method calls.
Print This Post
Posted on July 6, 2009
Since short URLs are all the rage these days, I wanted to outfit one of my websites with its own short URL capability.
A simple way to accomplish this is to base62 encode a numeric identifier (the database table's primary key). In order to identify a URL as a short URL, it will be prefixed with an uppercase 'S'. So, a short URL would be something like this: http://mixoom.com/Snfup8.
With regular expressions it's simple to turn off and on case sensitivity using (?-i) and (?i) respectively. So ultimately the new route needed in order to accomplish mapping the short URL is the following:
$route = new Portal_Controller_Router_Route_Regex(
'(?-i)S([\w\d]+)',
array('controller' => 'photos',
'action' => 'shorturl'),
array('shortid' => 1)
);
Print This Post
Posted on May 26, 2009
Zend_Acl is an excellent component that provides Access Control List (ACL) functionality. In most cases the goal is to manage user access to resources. access to to manage all things related to user access. In a nutshell, a role
to any kind of resource. But unfortunate it doesn't quite live up to its full potential just yet, due to a few implementation details as outlined in tickets and ZF-4460. The latter of the two also has comments that include a few examples for a workaround.
I'm using the following class which gives any custom Assert object access to the actual Resource passed to it.
/**
* The current Zend_Acl design does not allow for
* using a custom Role and Resource objects and expect that they'll make it through
* to custom assertions.
* See http://framework.zend.com/issues/browse/ZF-1722
* and http://framework.zend.com/issues/browse/ZF-4460
*/
class My_Acl extends Zend_Acl
{
/**
* Returns the identified Resource
*
* The $resource parameter can either be a Resource or a Resource identifier.
*
* @param Zend_Acl_Resource_Interface|string $resource
* @throws Zend_Acl_Exception
* @return Zend_Acl_Resource_Interface
*/
public function get($resource)
{
if (!$this->has($resource)) {
require_once 'Zend/Acl/Exception.php';
throw new Zend_Acl_Exception("Resource '$resource' not found");
}
if ($resource instanceof Zend_Acl_Resource_Interface) {
return $resource;
}
return $this->_resources[$resource]['instance'];
}
}
Unfortunately, this doesn't fix the issue of the Role making it through to an assertion, but in most of my cases that's the acting user anyway, so I don't even try to grab the passed in $role and instead use the identity straight from Zend_Auth.
/**
* Ensure the photo is owned by the user with $role
*
*/
class PhotoOwnerAssertion implements Zend_Acl_Assert_Interface
{
public function assert(Zend_Acl $acl,
Zend_Acl_Role_Interface $role = null,
Zend_Acl_Resource_Interface $resource = null,
$privilege = null)
{
if (!$resource instanceof Photos_Row) {
return false;
}
/* @var $resource Photos_Row */
/*
* Workaround; the current Zend_Acl design does not allow for
* using a custom Role interface and expect that it'll make it through.
* See http://framework.zend.com/issues/browse/ZF-1722 and
* http://framework.zend.com/issues/browse/ZF-4460
*/
$role = Zend_Auth::getInstance()->getIdentity();
return $resource->getOwnerId() == $role;
}
}
When setting up the ACL, I provide an instance of the custom assertion which will then provide the proper access control. It's fairly well encapsulated (other than the bug workarounds).
/**
* Only allow owners to view, edit, and delete their photos
*/
$acl->allow('member', 'Photo', array('view', 'edit', 'delete'), new PhotoOwnerAssertion());
In this case, the Photos_Row class must also provide a getOwnerId() method.
class Photos_Row extends Zend_Db_Table_Row_Abstract
implements Zend_Acl_Resource_Interface
{
/**
* Resource type (for use with ACL)
*
* @see Zend_Acl_Resource_Interface
*
* @return string
*/
public function getResourceId()
{
return 'Photo';
}
/**
* Return the photo owner's UUID
*
* @see UgcItem
*
* @return string
*/
public function getOwnerId()
{
return $this->avataruuid;
}
}
A little more abstraction and the custom assertion can be used for models other than photos.
Print This Post
Posted on May 5, 2009
Twitter is all the rage these days. Every site out there has some kind of "Tweet This" link or "Follow us on Twitter" button. Some sites have even deeper integration and tweet events on your behalf. In most cases, those sites are asking you for your Twitter username and password. What? Even scarier, many people enter their credentials without thinking twice. It's crazy. When has it become acceptable to enter your credentials for your online accounts (that often make you choose six or more character passwords) into some random third party site? Well, the answer, I suppose, is since social networking sites have began asking for email account access to rummage through your contact list. Still, it's a rather unacceptable solution for a self-respecting web site to operate this way, especially since Twitter supports the OAuth protocol which is designed to tackle this exact problem.
If you're familiar with how Flickr allows third-party applications and websites access to your account, then you know how it works. A web site requests access to your account, you are prompted to allow and deny access, and that's it. There are no passwords involved. And if you decide that you don't like what that website is doing with your account, you can revoke access at any time.
I will assume that you're already familiar with the Zend Framework. If that is not the case, and you're a PHP developer, you should really consider starting to use it. It is a very well designed and powerful collection of classes that complement each other and, after the initial ramp up time and learning curve, will pay off in both terms of development speed as well as maintainability. Check out the Quick Start.
In fact, Zend Framework (1.8) ships with a Zend_Service_Twitter class, which provides all the Twitter functionality. The problem is that this class only supports Basic Authentication using your Twitter account username and password. But fear not, we can bend this class to do our bidding.
See, underneath the hood, Zend_Service_Twitter is actually a Zend_Rest_Client, which is powered by Zend_Http_Client. Let's just remember that for now.
Let's take a look at this OAuth thing. Zend Framework has some preliminary support for it in the incubator. The client portion of it is functional, although kind of buggy, still.
The proposal for Zend_Oauth can be found here http://framework.zend.com/wiki/pages/viewpage.action?pageId=37957, complete with a ma.gnolia.com example use case.
Let me summarize how this works real quick:
1. Your configured Zend_Oauth_Consumer fetches a request token, which is used to prompt the user of the service to allow access.
2. Once access is allowed, your application receives an access token.
3. Your can ask the access token object to hand you an http client. It's a Zend_Oauth_Client, which extends Zend_Http_Client, and automagically handles the signing so you can treat it like a regular Zend_Http_Client and perform all the GETS and POSTS you want. Nifty!
Now let's go back to the Zend_Service_Twitter. Remember how it uses a Zend_Http_Client? All we have to do now is remove the basic (username/password) authentication mechanism and replace it with the OAuth-based version. To achieve that, we'll simply extend Zend_Service_Twitter as My_Service_Twitter. and make the following changes:
class My_Service_Twitter extends Zend_Service_Twitter
{
/**
* @var array
*/
protected $_oauthOptions;
/**
* @var Zend_Oauth_Token_Access
*/
protected $_accessToken;
/**
* Initialize Oauth
*/
protected function _init()
{
if (!$this->_authInitialized) {
$client = $this->_accessToken->getHttpClient($this->_oauthOptions);
$client->setHeaders('Accept-Charset', 'ISO-8859-1,utf-8');
self::setHttpClient($client);
$this->_authInitialized = true;
}
$client = self::getHttpClient();
$client->resetParameters();
}
/**
* @param array $oauthOptions
* @return My_Service_Twitter provides fluent interface
*/
public function setOauthOptions(array $oauthOptions)
{
$this->_oauthOptions = $oauthOptions;
return $this;
}
/**
* @return array
*/
public function getOauthOptions()
{
return $this->_oauthOptions;
}
/**
* @param Zend_Oauth_Token_Access $token
* @return My_Service_Twitter provides fluent interface
*/
public function setToken(Zend_Oauth_Token_Access $token)
{
$this->_accessToken = $token;
return $this;
}
/**
* @return Zend_Oauth_Token_Access
*/
public function getToken()
{
return $this->_accessToken;
}
}
And it's ready to be used. Instantiate the class, set the Oauth token via setToken() and then use the class the same way as before.
Print This Post
Posted on January 3, 2009
While sessions come in handy for many things, I like to avoid them for the sake of scalability. PHP itself uses a shared-nothing architecture, with only sessions needing special treatment.
Once you've got more than one web server, your options are to either use a custom session handler, a load balancer with IP affinity, the commercial Zend Platform with clustered sessions, or simply no sessions at all.
For the purpose of authenticating users, I use the Zend_Auth component, sometimes with custom adapters as an application may require.
The issue at hand, however, is that Zend_Auth's default storage backend uses Zend_Session. Luckily it is super easy to write a different Zend_Auth_Storage_Backend that uses cookies instead. The one pitfall in this case is ensuring that malicious users cannot manipulate the cookie value in order to impersonate users or otherwise gain any kind of advantage. So we don't just store the identity that Zend_Auth throws at the backend, we also generate a salted hash that prevents tampered cookies from throwing us for a loop.
Aside from now being able to scale a web application in a clustered environment, there are additional side-effects, which may be seen as benefits. Sessions can expire. Even though the cookie containing the session id (in PHP the default is PHPSESSID) may still exist on the client side, the PHP's session garbage collector may have already cleaned up the actual session data file after a certain timeout period (the default is 1800 seconds, which is half an hour).
That's something that won't happen when you're using the cookie backend, since the cookie (by default) expires after the browser closes. That means that unless you've specificially added a mechanism to time out users after a certain amount of inactivity, they'll remain logged in as long as they keep their browser windows open.
You may also want to let users remain logged in for longer than just the current browser session, you can achieve that by simply setting the cookie's expiration to a particular date in the future — Ideally in combination with a "[ ] remember me" checkbox on your web site's login form.
Here's the start of a class that implements this mechanism. It's only a draft, but should be functional as-is.
/**
* A cookie-based Zend_Auth storage backend that does not require sessions
*
*/
class My_Auth_Storage_Cookie implements Zend_Auth_Storage_Interface
{
/**
* @var string
*/
protected $_basedir = '/';
/**
* @var string
*/
protected $_cookieName;
/**
* @var string
*/
protected $_domain;
/**
* @var string
*/
protected $_secret;
/**
* @var mixed
*/
protected $_cached;
/**
* Constructor
*
* @param string $cookieName The name of the cookie where the identity is stored
* @param string $secret The salt used in the hashing of the data
*/
public function __construct($cookieName = null, $secret = null)
{
if (null !== $cookieName) {
$this->setCookieName($cookieName);
}
if (null !== $secret) {
$this->setSecret($secret);
}
}
/**
* The basedir of the cookie
*
* @return string
*/
public function getBasedir()
{
return $this->_basedir;
}
/**
* Set the basedir of the cookie
*
* @param string $basedir
* @return Portal_Auth_Storage_Cookie
*/
public function setBasedir($basedir)
{
$this->_basedir = $basedir;
return $this;
}
/**
* Get the name of the cookie
*
* @return string
*/
public function getCookieName()
{
return $this->_cookieName;
}
/**
* Set the name of the cookie
*
* @param string $cookieName
* @return Portal_Auth_Storage_Cookie
*/
public function setCookieName($cookieName)
{
$this->_cookieName = $cookieName;
return $this;
}
/**
* Get the (sub)domain the cookie is set for
*
* @return string
*/
public function getDomain()
{
return $this->_domain;
}
/**
* Set the (sub)domain the cookie is set for
*
* @param string $domain
* @return Portal_Auth_Storage_Cookie
*/
public function setDomain($domain)
{
$this->_domain = $domain;
return $this;
}
/**
* Get the Secret (or salt) used to store the data
*
* @return string
*/
public function getSecret()
{
return $this->_secret;
}
/**
* Set Secret (or salt) used to store the data
*
* @param string $secret
* @return Portal_Auth_Storage_Cookie
*/
public function setSecret($secret)
{
$this->_secret = $secret;
return $this;
}
/**
* Returns true if and only if storage is empty
*
* @throws Zend_Auth_Storage_Exception If it is impossible to determine whether storage is empty
* @return boolean
*/
public function isEmpty()
{
return $this->read() == null;
}
/**
* Returns the contents of storage
*
* Behavior is undefined when storage is empty.
*
* @throws Zend_Auth_Storage_Exception If reading contents from storage is impossible
* @return mixed
*/
public function read()
{
if (!$this->_cached) {
if (array_key_exists($this->_cookieName, $_COOKIE)) {
$value = $_COOKIE[$this->_cookieName];
list ($contents, $now, $checksum) = explode('|', $value);
if (md5(base64_decode($contents) . $now . $this->_secret) == $checksum) {
$this->_cached = $contents;
}
}
}
return $this->_cached;
}
/**
* Writes $contents to storage
*
* @param mixed $contents
* @throws Zend_Auth_Storage_Exception If writing $contents to storage is impossible
* @return void
*/
public function write($contents)
{
$this->_cached = $contents;
$now = time();
$checksum = md5($contents . $now . $this->_secret);
$value = base64_encode($contents) . '|' . $now . '|' . $checksum;
if (!setcookie($this->getCookieName(),
$value,
0, // end of the session
$this->getBasedir(),
$this->getDomain(),
null, // https
null)) {
throw new Zend_Auth_Storage_Exception('Failed to set cookie');
}
}
/**
* Clears contents from storage
*
* @throws Zend_Auth_Storage_Exception If clearing contents from storage is impossible
* @return void
*/
public function clear()
{
if (!setcookie($this->_cookieName,
false, // clears the cookie
0,
$this->getBasedir())) {
throw new Zend_Auth_Storage_Exception('Failed to clear cookie');
}
}
}
Please Note: The class stores the "identity" set by the Zend_Auth adapter as clear text. It also cannot handle anything other than scalar values. To be safe, the read() and write() methods really *should* handle arrays or other data types. This can be achieved by simply serializing when writing and unserializing when reading.
Now that you have the class, using it is straight forward. In your bootstrap, configure Zend_Auth like so:
Zend_Auth::getInstance()->setStorage(new My_Auth_Storage_Cookie('i', 'secretsalt'));
That's it already. Now you may use Zend_Auth as described in the Zend Framework Manual. And again, anything other than scalar values such as arrays is not supported without modifications, so persisting a DbTable Result Object won't work out of the box. Implementing the serialize() and unserialize() is left as an exercise for the reader.
Print This Post