Category Archive for 'zendframework'

Custom frontends for Zend_Cache

As we all know, server-side caching is a useful technique for optimizing the performance of web apps. Whenever some expensive operation is called for – a db query, a remote web service call, etc – caching the results of the operation means that the next time you need them, you can get them relatively cheaply from the cache.

Zend_Cache: A basic usage example

Zend Framework contains a Zend_Cache component that supports a variety of frontends and backends. Typical usage for filesystem-based caching is something like the following:


// create the cache object
$frontend = array(
    'lifetime' => 86400, // seconds
    'automatic_serialization' => true,
);
$backend = array(
    'cache_dir' => '/path/to/your/cache',
);
$cache = Zend_Cache::factory('Core', 'File', $frontend, $backend);

// now use the cache object
$cacheId = 'someCacheId';
$data = $cache->load($cacheId);
if (false == $data){
    $data = someExpensiveOperation();
    $cache->save($data, $cacheId);
}

In practice, I tend to put the cache creation portion into a factory/container class of some kind, while the usage/consumption is often in a repository or service class. But for clarity, it is shown all in line, as if it were in a controller.

The first time you run through this code, the data will not have been saved in cache (a cache miss, as they say), so you will be forced to incur the cost of the someExpensiveOperation() call. But the next time you run through it – assuming the cache has not expired beyond its lifetime – then you will register a cache hit and bypass the expensive operation.

All cool.

Yeah, but…

But there is one thing that has always bugged me about this typical flow: generating the $cacheId. For example, if someExpensiveOperation() is a db call to fetch an article, then the $cacheId will probably employ the id or slug of the article:

// $slug is the slug of the article, something like 'my-cool-article'
$cacheId = 'article_byslug_' . str_replace('-', '_', $slug);  // Zend_Cache does not like cache id's with hyphens

Generating a cache id strikes me as part of the internal details of the “caching process”. As such, it seems to me like the knowledge of how to do that should be embedded inside the caching object itself. It’s his business to know how to load data from cache, save data to cache, and remove data from cache. Connecting an id to that data should be part of his job. So, why should I have to construct cache id’s for him?

Even further, why am I seeing cache id’s at all? Couldn’t the cache object simply offer me an interface with methods like:


public function loadArticlebySlug($slug);
public function saveArticleBySlug($article);
public function removeArticleBySlug($article);

Isn’t there some easy way to make this happen?

Custom cache frontends to the rescue!

It turns out that Zend_Cache::factory() actually does allow me to create custom frontends that implement whatever interface I want. I can define my interface:

interface Project_Cache_Frontend_ArticleInterface
{
    public function loadArticlebySlug($slug);
    public function saveArticleBySlug($article);
    public function removeArticleBySlug($article);
}

Then implement in a class extending Zend_Cache_Core:

class Project_Cache_Frontend_Article extends Zend_Cache_Core implements Project_Cache_Frontend_ArticleInterface
{
    public function loadArticlebySlug($slug)
    {
        return $this->load($this->getCacheIdBySlug($article->slug));
    }

    public function saveArticleBySlug($article)
    {
        return $this->save($article, $this->getCacheIdBySlug($article->slug));
    }

    public function removeArticleBySlug($article)
    {
        return $this->remove($this->getCacheIdBySlug($article->slug));
    }

    protected function getCacheIdBySlug($slug)
    {
        return 'article_byslug_' . str_replace('-', '_', $slug);
    }
}

Note the protected method getCacheIdBySlug($slug). All the knowledge of how to create cache id’s is wrapped up in the cache object. It just exposes a functional interface that describes what you want to do, not how you want it done.

Now, tell the Zend_Cache::factory() method to use my custom frontend:

// frontend and backend options as before
$frontend = array(
    'lifetime' => 86400, // seconds
    'automatic_serialization' => true,
);
$backend = array(
    'cache_dir' => '/path/to/your/cache',
);
$cache = Zend_Cache::factory('Project_Cache_Frontend_Article', 'File', $frontend, $backend, true, false, true);

Note that we are specifying the complete name of our implementing cache class, as well as three boolean values.

The first boolean value – true in this case – tells the factory that we are using a custom frontend.

The second boolean value – false in this case – tells the factory that we are not using a custom backend.

The third boolean value – true in this case – tells the factory to use autoloading to instantiate the frontend and backend objects.

Fully constructed by the factory, cache usage is cleaner:


$data = $cache->loadArticleBySlug($slug);
if (false === $data){
    $data = someExpensiveOperation();
}

No cache id’s, no mixing of concerns.

Maybe it’s a whole lot of work just to tuck away some cache id generation. But I confess that it feels better to me.

Unit testing for placeholder-based view-helpers in Zend Framework

Just got bitten by an interesting little issue.

I have a Zend Framework view-helper that I use to add a CSS class to an HTML <body> tag. Usage in a view-script is:

$this->bodyTag()->addClass('someclass');

Then in a layout, I’d output it using:

<?= $this->bodyTag() ?>

The class itself is very simple:


/**
 * A body tag
 *
 * @author David Weinraub <david@papayasoft.com>
 */
class PapayaSoft_Zend_View_Helper_BodyTag extends Zend_View_Helper_Placeholder_Container_Standalone
{
    public function bodyTag()
    {
        return $this;
    }

    public function toString()
    {
        $tag = array();
        $tag[] = '<body';

        $storage = $this->getContainer();

        if (count($storage) > 0){
            $classes = array();
            foreach ($storage as $class){
                $classes[] = $class;
            }
            $tag[] = sprintf(' class="%s"', implode(' ', $classes));
        }
        $tag[] = '>';
        return implode('', $tag);
    }

    public function addClass($class)
    {
        $this->getContainer()->append($class);
        return $this;
    }
}

I’ve begun moving this kind of stuff out into my own library, which I will manage in its own project under source control. I can then import it into multiple projects, just as I would with the libs for Zend or Doctrine or HTML Purifier.

Consequently, it’s now time – actually long past-due – to really get into the discipline of unit-testing. A simple class like this seems a decent place to start.

Unit-tests for the BodyTag class looked something like this:


/**
 * Test BodyTag class
 *
 * @group Zend_View_Helper
 * @author David Weinraub <david@papayasoft.com>
 */
class PapayaSoft_Zend_View_Helper_BodyTagTest extends PHPUnit_Framework_TestCase
{
    /**
     * @var PapayaSoft_Zend_View_Helper_BodyTag
     */
    private $_bodyTag;

    public function setUp()
    {
        $this->_bodyTag = new PapayaSoft_Zend_View_Helper_BodyTag();
    }

    public function tearDown()
    {
        unset($this->_bodyTag);
    }

    public function testRenderOnNoAddedClasses()
    {
        $this->assertEquals('<body>', (string) $this->_bodyTag);
    }

    public function testRenderOnSingleAddedClass()
    {
        $this->_bodyTag->addClass('myclass');
        $this->assertEquals('<body class="myclass">', (string) $this->_bodyTag);
    }

    public function testRenderOnMultipleAddedClasses()
    {
        $this->_bodyTag = new PapayaSoft_Zend_View_Helper_BodyTag();
        $this->_bodyTag->addClass('someclass')->addClass('anotherclass');
        $this->assertEquals('<body class="someclass anotherclass"<', (string) $this->_bodyTag);
    }
}

Pretty straightforward. Or so I thought.

The last test failed, reporting the following:

BodyTagTest::testRenderOnMultipleAddedClasses()
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-<body class="someclass anotherclass">
+<body class="myclass someclass anotherclass">
It seemed as if result of running the penultimate method testRenderOnSingleAddedClass() was still in effect when testRenderOnMultipleAddedClasses() started. When the testRenderOnSingleAddedClass() method is removed, the testRenderOnMultipleAddedClasses() passes.

What’s going on here? The whole point of tearDown() and setUp() is to reset the test environment to a clean state, in this case by unsetting and re-instantiating the $_bodyTag member variable.

Well, the whole key here turns out to be state of the system. It’s not sufficient to simply reset the object in the member variable if it has made other changes to the state of the system. Sort of like selling the cow that crapped in your living room, but forgetting to clean up the mess he made. And that’s actually what appears to be happening.

The framework’s own unit test for Zend_View_Helper_HeadTitle, another Zend_View_Helper_Placeholder_Container_Standalone-extended class, offers some hint of this. The setUp() method is as follows:

    /**
     * Sets up the fixture, for example, open a network connection.
     * This method is called before a test is executed.
     *
     * @return void
     */
    public function setUp()
    {
        $regKey = Zend_View_Helper_Placeholder_Registry::REGISTRY_KEY;
        if (Zend_Registry::isRegistered($regKey)) {
            $registry = Zend_Registry::getInstance();
            unset($registry[$regKey]);
        }
        $this->basePath = dirname(__FILE__) . '/_files/modules';
        $this->helper = new Zend_View_Helper_HeadTitle();
    }

So there is some Zend_Registry stuff in there that needs to be cleared, as well. Adding the registry-clearing code from the ZF unit-test into my own solved the problem.

Live and learn.

CRISP-y goodness: tracking Phuket internet speeds

I am pleased to report that we have launched the new version of CRISP – Customers Reporting Internet Speeds in Phuket.

For some years, well-known computer columnist and baker Woody Leonhard (see AskWoody.com for his opinions on Windows patches or his columns in the Windows Secrets Newsletter, or his various Windows for Dummies books, not to mention his bakery business and his Phuket Sandwich Shoppes) has led a group of Phuket internet users in running speed tests on their internet connections and storing this data in an app developed and hosted by Henry Habermacher. The data was freely available as a CSV download to anyone who wanted to perform their own analysis on it.

Woody’s prime motivations were to provide Phuket internet users with real usage data on which to base their ISP choices and to clearly demonstrate the significant discrepancy between speeds advertised by ISP’s in Phuket and those that are actually delivered.

As of this writing, the db has over 15,000 speed reports dating back to 2008.

Henry was unable to host it going forward, so we needed to move it to a new home. But rather than simply migrate, we decided to do a rewrite in PHP and I got the call. Using a design by Seth Bareiss, my re-implementation is built on Zend Framework and the Doctrine 1 ORM. Relative to the previous version, it features some enhanced searching and user management, including registration and profile management.

If you are a Phuket-based internet user, come on over, register, and start contributing to the tracking effort.

http://www.khunwoody.com/phuket-internet-speed/

Zend Framework default routing for hyphenated and camelCase URL’s

Using the default routing in Zend Framework (v1.x), I always forget how certain URL patterns map to action and view-script names. So just a quick post, mostly for myself, to record them here.

URL: mycontroller/test-me
Controller: mycontroller
Action: testMe
Script: test-me.phtml

URL: mycontroller/testMe
Controller: mycontroller
Action: testme
Script: test-me.phtml

That’s it!

What to do about ZodiacFacts and #zf on Twitter?

The Twitter hashtag #zf has been used by the Zend Framework for several years now. The Twitter user @ZodiacFacts began using it some time ago, polluting the #zf stream with all that medieval nonsense. Zend Framework Project Lead Matthew Weier O’Phinney tweeted a reply to @ZodiacFacts politely requesting that he/she refrain from using the hashtag and @ZodiacFacts seems to have politely complied.

So, what’s the problem?

As of this writing, @ZodiacFacts has over 170,000 followers – a lamentable indictment on the state of human rationality. It is no surprise that some of them continue to retweet into the #zf stream.

There are not many of them, only a few each day. So, it’s only a minor annoyance. Still, life is filled with enough of minor annoyances that it’s desirable to remove/minimize them when we can.

The solution, perhaps

A Twitter Bot could be built that polls Twitter for tweets containing both @ZodiacFacts and #zf and then sends a politely worded reply to the Twitter user requesting that he refrain from using the #zf hashtag.

I have actually built a class on which such a bot can be based. And I have registered the Twitter account @zfisforzend to function as the sender.

But since the #zf hashtag should rightly be regarded as a community resource, I don’t really feel that it is my place to deploy the solution on my own. The “community” or even Zend HQ should make the decision about whether to deploy, the phrasing of the message, etc. After all, both of these things affect the public face of the project. Hey, it might even inflame the legions of @ZodiacFact fans to spam the #zf stream, just out of pique.

So, I am posting my current code so that if one of the project leaders wishes to use it – or to develop their own, which would no doubt be better than mine – then they can do so with little effort. I’d be happy to pass off the ownership of the @zfisforzend Twitter account to whatever community member wants to use it for this purpose.


/**
 * Access Twitter API, do twitter search, then reply
 *
 * @author David Weinraub ( david@papayasoft.com )
 */
class PS_TwitterSearchAndReplyBot
{
    /**
     * @var Zend_Service_Twitter
     */
    protected $_twitter = null;

    /**
     * @var string
     */
    protected $_reply = null;

    /**
     * Path to the file containing the id of the tweet to which we have replied
     *
     * @var string
     */
    protected $_sincePath = null;

    /**
     * @var string
     */
    protected $_userAgent = 'PapayaSoft-SearchAndReplyBot/1.0';

    /**
     * Constructor
     *
     * @param string $username Twitter username for the acct sending replies
     * @param string $password Twitter password for the acct sending replies
     * @param string $query the query we use to find tweets.
     * @param string $reply the text of the reply we wish to send
     * @param string $sincePath path to the file where we save the last replied tweet
     */
    public function __construct($username, $password, $query, $reply, $sincePath)
    {
        if ('' == $username){
            throw new Exception('User is required');
        }

        if ('' == $password){
            throw new Exception('Password is required');
        }

        $this->_twitter = new Zend_Service_Twitter($username, $password);
        $this->_twitter->getHttpClient()->setHeaders('useragent', $this->_userAgent);

        if ('' == $query){
            throw new Exception('Query is required');
        }
        $this->_query = $query;

        if ('' == $reply){
            throw new Exception('Reply is required');
        }
        $this->_reply = $reply;

        $this->_sincePath = $sincePath;
    }

    /**
     * Get the matching tweets
     *
     * @return array the matching tweets
     */
    public function getTweets()
    {
        $service = new Zend_Service_Twitter_Search();
        $service->getHttpClient()->setHeaders('useragent', $this->_userAgent);
        if (null == $sinceId){
            $sinceId = (int) $this->readSince();
        }
        $params = array(
            'since_id' => $sinceId,
        );
        return $service->search($this->_query, $params);
    }

    /**
     * Send the reply to a matching tweet
     *
     * @param array $tweet
     * @return void
     */
    public function sendReply($tweet)
    {
        $target = $tweet['from_user'];
        $reply = '@' . $target . ' ' . $this->_reply;

        $result = $this->_twitter->statusUpdate($reply, $tweet['id']);

        if ($result->isSuccess()){
            $this->writeSince($tweet['id']);
            return true;
        } else {
            throw new Exception('Failure posting status reply to target');
        }
    }

    /**
     * Save the id of the last tweet to which we have replied
     *
     * @param int $sinceId
     * @return 
     */
    public function writeSince($sinceId)
    {
        @$fp = fopen($this->_sincePath, 'w');

        if (!$fp){
            throw new Exception('Unable to open since-file for writing');
        }
        fwrite($fp, $sinceId);
        fclose($fp);
        return $this;
    }

    /**
     * Get the twitter id of the last tweet to which we have replied
     *
     * @return int
     */
    public function readSince()
    {
        return file_get_contents($this->_sincePath);
    }

    /**
     * Entry point. Do it all.
     */
    protected function run()
    {
        $tweets = $this->getTweets();
        foreach ($tweets as $tweet){
            $this->sendReply($tweet['from_user'], $tweet['id']);
        }
    }
}

Usage would be something along the following:


$bot = new PS_TwitterSearchAndReplyBot(
    'zfisforzend',
    'thepassword',
    '#zf AND @ZodiacFacts',
    'The ZF hashtag has been used by http://bit.ly/dlRih0 for 2+ years. Please do not use it for ZodiacFacts. Thanks! ;-) ',
    dirname(__FILE__) .'/data/since.dat'
);
$bot->run();

Then set up a cron job to run this script with some polling frequency.

Note that the message we send should not contain either of the terms “#zf” or “Zend” directly. After all, those are search terms we all use in our Twitter clients. The last thing we want is for all these bot-generated replies to further pollute the targeted hashtag stream.

As you can see, it is general enough to serve as the base for other Twitter Reply bots. I confess that the code is only partially tested, but I think it conveys the idea.

So, whaddya think?

Zend Framework on shared hosting

Zend Framework recommends – but does not rigidly enforce – a file structure for your application. Most of the prominent “getting started” sample apps – like the Zend Framework Quickstart, Rob Allen’s Getting Started tutorial, HariKT’s Base Files skeleton – use it:

ZF recommended structure

Using this structure usually requires creating a virtual host pointing to this public folder and then creating the file public/.htaccess that maps all non-file, non-directory requests to the index.php file. You then get to enjoy all the rich, creamy Zend goodness.

But sometimes you are stuck with shared hosting and you can’t always place your other app folders as siblings to your public web root. For example, one prominent web hosting company provides me a web root of the form /usr/www/users/myusername with a home directory of /usr/home/myusername.

So whaddya gonna do?

A Solution – Push Down and Protect

This question has been asked and answered many times, but most of the solutions use either custom plugins or tricky .htaccess rules. There’s nothing wrong with those – and in general, they are proposed by people way smarter than me. So there is an argument for going with the best. As they used to say: “No one ever got fired for buying IBM.”

But there is a simpler way that has worked well for me: depart slightly from the recommended file structure and modify some include paths. I have seen this approach referenced before, in particular by tharkun on Stackoverflow, but I had not seen it written out explicitly. So, I offer it here in the hope that other ZF n00bs like me can benefit from it.

Essentially, I just push all my app folders down into a subfolder of the web root called something like _zf, web protect that _zf folder with a Deny from All in an .htaccess file for the folder, and change a few paths in the index.php entry point.

I presume that the shared hosting offers me a web root folder called public_html.

So, my folder structure looks like this:

ZF application structure for shared hosting

I make sure that the file public_html/_zf/.htaccess prevents any direct web access:

Deny from All

This keeps anyone from viewing any of the non-public app files.

Then we just modify paths in public_html/index.php to point to the _zf folder, as follows:


set_include_path(implode(PATH_SEPARATOR,
    array(
        dirname(__FILE__)  . DIRECTORY_SEPARATOR . '_zf' . DIRECTORY_SEPARATOR . 'library',
        get_include_path(),
    )
));

// Define path to application directory
defined('APPLICATION_PATH')
    || define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/_zf/application'));

// Everything else is the same as usual. For example, using Zend_Application...

// Define application environment
defined('APPLICATION_ENV')
    || define('APPLICATION_ENV', (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'production'));

require_once 'Zend/Application.php';  

$application = new Zend_Application(
    APPLICATION_ENV,
    APPLICATION_PATH . '/configs/application.ini'
);
$application->bootstrap()->run();

And that’s it!

A purely modular app structure can also be handled in this way. For example, you can simply put your modules folder inside your application folder. Since all the logic associated to module routing, module bootstrapping, and module-specific paths already resides within the framework, the setup above still works.

The Downside

There is an insidious down-side to all this. Best practice dictates keeping all those app folders – and especially the config files, rich with passwords and the like – out of the web root. In theory, we have that all handled with the .htaccess Deny on the _zf folder. But it’s a single point of failure. If it fails for some reason – say, the .htaccess file doesn’t get deployed – then we are fully exposed to a wide variety of potential mischief.

There is some additional obscurity you can obtain by changing the name of the _zf folder to something (practically) unguessable. I could even imagine a cron job that renames the folder and makes corresponding changes to the index.php file. But that’s probably excessive. And, in any case, as we should all well know, relying solely on security through obscurity is a poor approach.

But maybe this approach is useful to get an app off the ground with a customer who is stuck with his shared hosting package. At some point in the future, when the app is sufficiently profitable, maybe you can convince the customer to move to virtual private server hosting, at which point it’s a small modification to return to the standard ZF-recommend file structure.

Whaddya think?

Debugging PHP – Netbeans Code Templates for Zend_Debug and FirePHP

I recently changed my day-to-day PHP IDE to Netbeans and it’s been a pleasure. [Don't ask me what I was using before, too embarrassed to say.]

idea light bulb

In particular, one of Netbeans’ nice features is Code Templates, little snippets of text that you can insert into your document simply by typing a short prefix and then hitting the Tab key. They are actually smarter than just static snippets in that they can contain variables that are dynamically filled/changed as you edit. Hugely useful. I use them constantly for a variety of tasks, but my most common use of code templates is for debugging.

One useful debug tool is FirePHP in conjunction with Firebug, especially for applications using AJAX.

Several weeks ago, Jeremy Kendall (@JeremyKendall) helpfully pointed me to Zend_Debug::dump() which behaves like an enhanced var_dump(). I was actually gratified to see that it resembles one of my own hacks that I have been using for years, though Zend_Debug::dump() is, not surprisingly, more polished.

Following on that, Markus (@derstahlstift) posted a Netbeans code template that uses Zend_Debug::dump().

I admit that they are both cool, but I prefer to identify precisely where I am doing the dump, using PHP’s magic constants __FILE__ and __LINE__. This allows me to easily chase down the dump statements and remove them once the debugging is finished.

So, here are my own versions, both in the form of Netbeans Code Templates.

For FirePHP::send():

FB::send("${VAR default="variable"} = " . $$${VAR} . ":: ${func default="func"} :: " . __FILE__ . "(" . __LINE__ . ")");
${cursor}

For Zend_Debug::dump():

Zend_Debug::dump($$${VARIABLE variableFromNextAssignmentName default="variable"}, "${VARIABLE variableFromNextAssignmentName}", true);
echo "

" . __FILE__ . "(" . __LINE__ . ") :: ${function default="function"} :: ${message default="message"}

";
${selection}${cursor}

Whaddya think? Do you have any tricks/shortcuts that you use for debugging? Let me know.

Cheers!

Zend Framework View Helper to Lowercase Titles

When working on a web app, I often find myself with my local development version open in one browser window with the live version open in another. To allow me quickly distinguish one from the other, I like to tweak the rendering in the development version by converting the title to lower case.

Since I have recently begun using Zend Framework for much of my development, I wrote a little view helper to handle it.


class App_View_Helper_HeadTitle extends Zend_View_Helper_HeadTitle
{
    public function headTitle($title = null, $setType = Zend_View_Helper_Placeholder_Container_Abstract::APPEND)
    {
        if (null !== $title && APPLICATION_ENV == 'development'){
            $title = strtolower($title);
        }
        return parent::headTitle($title, $setType);
    }
}

To use it, just add the helper path to your bootstrap. For example,you could do it in your config/application.ini with:

resources.view[] =
resources.view.helpers.App_View_Helper_ = "App/View/Helper"

The calls to $this->headTitle() in your view scripts or layout scripts remain unchanged.

As a side note, a great repository of Zend Framework snippets is at the aptly named site ZFSnippets.com.

Do you use any similar tricks or snippets in your development? Let me know via the comments.

Cheers!

Zend Framework – Here I Come

Time to elevate my game.

After years of developing and tuning my own PHP-based web development framework which I have used on countless projects, I have decided to move into one of the more established PHP-based web frameworks.

My own framework is a loose collection of classes and methodologies that have worked pretty well so far. I can usually figure out how to do anything that has come up. Although it is a huge step up from so much of the the script-kiddie crap that is still fairly common – among even some successful commercial houses here in Phuket – I must confess that it never felt quite tight enough for me. It never lent itself to the kind of controlled development process I would ultimately like to be doing: source control, automated unit-testing, automated builds, and automated deployments.

There is no shortage of discussion out there about web frameworks: which ones are essentially CMS’s or mere libraries rather than full frameworks; which ones are tightly/loosely coupled; which ones are sufficiently true to established design patterns, to object-oriented principles, to MVC principles, etc; which ones are performance dogs, etc. I’ve read through much of it.

Upshot: I’m sticking with PHP, going with Zend Framework.

I’ve got nothing against the other PHP frameworks. CodeIgniter seems very approachable and has a decent community. CakePHP seems to have a lot of advocates and supportive community. Kohana, too, seems nice. Of course, there are many others.

And if I were really going to go wild, I would leap in Ruby on Rails about which I have never heard a single bad thing. But a leap into a new framework is enough without having to bite off the syntax of another language to boot.

Perhaps some other time I’ll get into all the reasons and do a comparative analysis, though it is just as likely that it will never happen since there are already tons of such resources out there and doing my own comparative analysis at a level that would be satisfactory to me would require becoming expert at all the others.

First step is to use a few of the Zend Framework classes here and there in a few projects, availing myself of the “loosely-coupled, use-at-will” nature of the library. Mostly to familiarize myself with some of the design patterns and to learn what supportive functionality is available outside the context of the full framework stack.

Eventually, I will commit to using the full MVC stack on a green-field project, either a brand new site or a redesign of an existing site. I already have one in mind. Both excited and terrified at the same time.

Go, go, go, …