howto

Building a LAMP development environment on Snow Leopard

How to install Apache, PHP, MySQL, MongoDB and a whole bunch of other useful packages on Mac OS X 10.6

You already have a copy of Apache and PHP and that's good enough for our purposes, but you're going to need a few things that don't come bundled with your Mac. For that you need to...

Set up a LAMP stack on OS X after the jump.

The established standard with Git is for commit messages to have a first line that is 50 characters or less and then add two newlines, and then explain the commit throughly.

This may seem crazy short, but you’ll find that forcing yourself to summarise the commit encourages you to be atomic and concise. If you can’t summarise it in 50-80 characters, you probably are trying to commit two commits as one.

A great rule of thumb from the Homebrew Formula Cookbook.

If, like me, you hose Apache on your Ubuntu LTS server when you finally get around to upgrading libssl, and PHP stops working...

You'll be needing this:

apt-get --reinstall install apache2 php5-mysql \
libapache2-mod-php5 mysql-server

(You're welcome)

Preparing a string for use in a PHP regular expression.

This blog post is just for me, so I can look it up later... I suppose you can borrow it if you find it useful :)

Using dynamic strings inside a regex can be annoying, especially if you can't guarantee that the strings are actually sane. Including a URL in a regular expression is downright obnoxious, since URLs and regular expressions share most of the same special characters, but they mean entirely different things. I threw this function together to make strings regexable. Enjoy.

Function

/**
 * Prepare a string for use in a regular expression.
 * 
 * @author Justin Hileman {@link http://justinhileman.com}
 * @access public
 * @param string $str
 * @return string
 */
function preg_real_escape_string($str) {
    $replace = array('\\' => '\\\\', '^' => '\^', '.' => '\.',
        '$' => '\$', '|' => '\|', '(' => '\(', ')' => '\)',
        '[' => '\[', ']' => '\]', '*' => '\*', '+' => '\+',
        '?' => '\?', '{' => '\{', '}' => '\}', ',' => '\,');
    return strtr($str, $replace);
}

Yeah, the name is a bit weird. This function is an analog to mysql_real_escape_string(), so it seemed fitting.

Usage

// basic usage (this is actually exactly the case I wrote this function for).
$regex = '/^' . preg_real_escape_string($name) . '/i';
$nameless = preg_replace($regex, '', $something_starting_with_name);
 
// trying to find a URL via regex can be especially obnoxious.
$regex = '/' . preg_real_escape_string($my_url) . '/';
$some_text = preg_replace($regex, '<a href="$1">$1</a>', $some_text);

Coda tip: show "invisible characters" when you highlight text

Coda is awesome. Here's a quick way to make it awesomer: invisible visible invisible characters!

Coda lets you show all the newlines, tabs and spaces, which comes in handy. But the default settings are pretty distracting. If you change the invisible character display color to match the background, they only show up when you select text.

First do this:

Show invisible characters

Then change this:

Change invisible character color to match the background

And rock out like this:

Invisible visible invisible characters!

Pro Tip: Use an explicit LIMIT in Doctrine

Doctrine is usually pretty rad. And sometimes it's really really dumb.

For example, in an imaginary CMS the following DQL query will grab a random blog post:

$random_post = Doctrine_Query::create()
    ->select('*')
    ->from('BlogPosts')
    ->where('published')
    ->orderBy('RANDOM()')
    ->fetchOne();

And it will work great when you first start writing blog posts. But as the total number of posts increases, the performance will get worse and worse. That's because, apparently, when you ask Doctrine to "fetch one" that doesn't quite convert to LIMIT 1 on the back end. It will actually select--and possibly instantiate an object for--everything that matches your query. After it's done with all this work, Doctrine will hand you the first result.

If you want to save yourself headaches later, use something like this:

$random_post = Doctrine_Query::create()
    ->select('*')
    ->from('BlogPosts')
    ->where('published')
    ->orderBy('RANDOM()')
    ->limit(1)
    ->fetchOne();

Any time you call fetchOne(), be sure to explicitly use limit(1).

Save yourself from the Twitpocalypse with this handy bookmarklet

The Twitpocalypse is upon us, and a whole lot of things have broken...

In some cases it's recoverable though. If you click a link from your favorite Twitter-related service and come across a URL like this, you can fix it! Just use this handy dandy little bookmarklet and you'll be back on the right Tweet in no time.

UNpocalypse!

Use this like you normally use a bookmarklet. Drag it to your bookmark bar, or right click and "save as bookmark".

The standard bookmarklet disclaimers apply: This works in Firefox and Safari, but might not work in all browsers. It's a quick and dirty bookmarklet, so if you use it somewhere it doesn't belong it will do unexpected things with your browser. Don't try using it on non-Twitpocalypsed tweets, as it will try to grab some random tweet from the far distant future.

How to: back up your iPhone with rsync

iTunes hates me:

A couple of weeks ago, iTunes decided to stop talking to my iPhone. For some reason, iTunes on my computer decided that my phone wasn't authorized to use my iTunes Store account.

My phone thought it was legit... Everything continued to work on that end. My computer thought it was cool. But somewhere in the connection between the phone and computer, iTunes decided that it would be best to wipe my phone. Which it continued to offer to do every time I tried to sync.

No me gusta.

So I'm taking drastic measures:

Since iTunes is being lame, it won't let me back anything up. Luckily, my friend rsync never fails me.

In short:

  1. Use rsync to back everything up.
  2. Let iTunes do its wipey/restorey thing.
  3. Recover the stuff I care about via reverse rsync.
  4. ...
  5. Profit!

Because I always forget how to do things:

Here's how to use rsync to back up your iPhone... This process requires a "liberated" phone, with OpenSSH installed (and turned on). But that whole process is beyond the scope of this post.

To rsync all the important stuff off your phone, make sure your phone is on, SSH is running, and everything is connected to the same network. Then open Terminal, change to an appropriate directory, and type this:

rsync -avz --stats --progress --exclude "private/var/mobile/Media/iTunes_Control/Music" root@YOUR_PHONE_NAME.local:/ ./backup

Of course, you've gotta replace YOUR_PHONE_NAME with the actual name of your phone...

It will ask for your phone's root password. If you haven't changed it, this is probably "alpine". Also, shame on you.

It bears repeating

A handful of questions about Twitter

when is too much too much?

That's the beauty of Twitter. You choose your own level of involvement.

if I don't respond to follow them does that mean that they know I'm not interested in them? why are they following me?

People new to Twitter, people who are unfamiliar with the follow/friend paradigm, and people who think it's a popularity contest will be bugged if you don't follow back. But that's because those three groups don't understand the power of a one-sided friendship.

On Facebook, you have to be friends (bi-directional) with someone to interact with them. Both of you have to agree on the status of your relationship.

But Twitter isn't about who is listening to you, or who is a bi-directional friend. Twitter is about who you interact with. For example, I am not following about a third of the people I interact with (reply to, talk about, etc). A good chunk of them aren't following me either. Because on Twitter, everything is open, and you don't have to have a defined relationship with someone to interact.

If someone responds to you, it'll show up under "@ replies" and you can carry on a conversation. But just because you're talking with them doesn't mean you have to listen to everything they say.

if they are following me are they listening or are they just waiting for me to follow them?

A little of both. Some people actually care, and some just want you to follow in return. Some are robots, some are spammers, and some are real people, who are really interested in what you have to say.

But regardless of the type of user, you should feel no obligation to follow back. I use a couple of tools to make this process easier.

Twimailer sends me really great "follow" notifications, so I can usually decide right in the email whether I want to follow back or ignore.

TweetSum calculates your recent followers' DBI ... It's a bit like a Google PageRank for Twitter users. It's based on their likelihood to follow you, to interact with you, and not send spammy tweets. It has a simple interface for sorting through the masses of followers and deciding who is worth following back.

at what point will i have to separate my friends from commerce, brands, I like, don't like, don't know.

I still haven't. I unfollow brands and companies that annoy me, but I don't worry too much about mixing them in with the stream. If you really need the separation, check out Nambu (Mac only) or TweetDeck (really awful interaction). They both allow you to group the people you follow, so you can interact with them as discrete streams. I tried that approach for a while, but it didn't suit me, so I'm back to one big river of messages.

how many is the right number of people to listen to, follow.

That depends.

I follow anyone who interests me at the time. If you make me laugh, or you start a conversation with me, or I interact with you in some other space--Facebook, IRL, mailing lists, etc--I might start following you. But to me, following is a fluid concept... If I tire of you, I might unfollow. If you tweet too much, I might unfollow you. If you set up automatic tweeting of all your Last.fm activity, there's a good chance I'll unfollow you.

But following isn't the only way I interact with people on Twitter. I track quite a few things that I'm interested in, and converse with people who talk about them. I listen to--and usually engage--everyone who talks to or about me, regardless of our respective follow status.

at what point does it get too hard to do?

When you think about it too much :)

This was originally a response to a post on a Meetup group I attend. It's a bit rough, but I feel like there's some value in it, so here ya go :)

IE 6 "Update"

Done21 is taking an interesting stab at the Internet Explorer 6 problem: They're providing and hosting code to simulate an IE yellow notice bar which prompts users to upgrade.

Unfortunately, they're spreading the wrong message. While it's good to upgrade to anything other than IE 6, it's better to upgrade away from IE completely. So here's an improved notice bar which prompts the user to get Firefox instead.

The `update` bar seen in Internet Explorer 6

Feel free to grab the code, remix for your own use. Provide your own message. Spread the word.

Try it out here.

<!--[if IE 6]>
<script type="text/javascript"> 
    /*Load jQuery if not already loaded*/ if(typeof jQuery == 'undefined'){ document.write("<script type=\"text/javascript\"   src=\"http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js\"></"+"script>"); var __noconflict = true; } 
    var IE6UPDATE_OPTIONS = {
        icons_path: "http://static.ie6update.com/hosted/ie6update/images/",
        url: "http://getfirefox.com",
        message: "Internet Explorer is a bad, bad browser. Click here to upgrade your Internet experience..."
    }
</script>
<script type="text/javascript" src="http://static.ie6update.com/hosted/ie6update/ie6update.js"></script>
<![endif]-->

IE6 Update is licensed by Done21 under Creative Commons BY NC SA 3.0. IE6 Update code is derived from Activebar2.

Ever wonder what's up with Vim on your Ubuntu boxen?

The default Vim that ships with Ubuntu is lame. To get Real Vim, you need to do this:

sudo apt-get install vim-full

It spins for a while then tells you it's going to install about a hundred new packages. Say 'Y'.

Because when it's finally done, you have Real Vim, not Lame Vim.

Post-mortem of a Search Engine Suicide

I prob'ly don't need to tell you this, but don't ever ask search engines to delist your site. Ever. They actually do it.

A bit of background...

Last month I relaunched justin hileman dot info with a fresh new theme, and a couple of cool features. While I was developing the replacement site, I had a staging version which I didn't want Google to index. I used a handy little robots.txt file to keep the search engine spiders at bay:

User-agent: *
Disallow: /

This file did the trick.

The staging site wasn't indexed, exactly as advertised. Unfortunately when the time came to deploy, my über-restrictive robots.txt file overwrote the existing file and slipped into the live site.

Google, Yahoo, and friends dutifully ignored every page on my site. The majority of the damage was instantaneous. Most high traffic and high PR pages were unindexed within a few hours. My crawl rate dropped, the major search engines removed more pages every time they crawled my site.

My SERP traffic, understandably, tanked.

Yesterday my search engine referrals hit zero.

Search engine referrer traffic, post-apocalypse

The sharp drop in SERP traffic on February 12th coincides with the first Google crawl with the new robots.txt. The second drop, around the 23rd was the result of Yahoo's reindex. In just a few days my site was completely unlisted from the major search engines.

As of the time of this post, a search for "justin hileman dot info", which should result in about 1500 pages on this domain, returns nothing.

Google Search for justin hileman dot info

How could this have been avoided?

Google Webmaster tools provides a great overview of your site. It dutifully lists any problems encountered while spidering your domain, and what might have caused them. In my case, there's a huge red flag:

Google Webmaster tools site overview

A few restricted URLs is normal, but 817 is certainly a bit excessive. Had I paid attention to the tools Google provides, I would have noticed an abrupt change in crawl rate, and the spike in restricted URLs. But at the time, I only saw the decline in traffic, and didn't think to consult the webmasters tools.

The moral of the story:

First, search engines actually respect your robots.txt file. Second, it's a really bad idea to tell them to go away, because they will. And take your traffic with them.

Google, Yahoo, I've learned my lesson... Please relist me.

Fuzzier (human-friendly) timestamps with Drupal 6.x

I set up super-sweet fuzzy "submitted by" timestamps on my blog a couple of days ago. They're quite a bit easier to grok at a glance, and they show the full date when you hover over them.

Go on, try it with the one at the top of this post. You know you want to.

Drupal 6.x makes tweaks like this easy. Here's how you can do it too:

Open up your theme's template.php file. If you're using a stock theme, copy the theme from Drupal core into your site's themes folder before editing anything.

Add these three functions:

/**
 * Calculate a fuzzy delta between two timestamps. If only one timestamp is
 * passed, uses 'now' as the other. Optionally takes a third 'granularity'
 * parameter, which decides how many chunks to return. Default: 1.
 */
function _fuzzy_delta($to, $from = null, $granularity = 1) {
    $times = array(
        'year'   => 31536000,
        'month'  => 2592000,
        'week'   => 604800,
        'day'    => 86400,
        'hour'   => 3600,
        'minute' => 60,
        'second' => 0
    );
 
    if ($from === null) $from = time();
    $secs = abs($from - $to);
 
    $count = 0;
    $time = '';
 
    foreach ($times as $name => $value) {
        if ($secs >= $value) {
            $slice = floor($secs / $value);
            $time .= $slice . ' ' . $name;
            if (($slice != 1)) $time .= 's';
 
            $secs = $secs % $value;
            $count++;
            if ($count > $granularity - 1 || $secs == 0) break;
            else $time .= ', ';
        }
    }
 
    return $time;
}
 
function phptemplate_node_submitted($node) {
    return t('posted by !username <span title="@long_date">@ago ago</span>',
        array(
            '!username' => theme('username', $node),
            '@long_date' => format_date($node->created, 'long'),
            '@ago' => _fuzzy_delta($node->created)
        )
    );
}
 
function phptemplate_comment_submitted($comment) {
    $node = node_load($comment->nid);
    return t('comment by !username <span title="@date">@later later</span>',
        array(
            '!username' => theme('username', $comment),
            '@date' => format_date($comment->timestamp, 'long'),
            '@later' => _fuzzy_delta($comment->timestamp, $node->created),
        )
    );
}

If you want comments to use "ago" formatting instead of "later", use this version of phptemplate_comment_submitted instead:

function phptemplate_comment_submitted($comment) {
    return t('comment by !username <span title="@date">@ago ago</span>',
        array(
            '!username' => theme('username', $comment),
            '@date' => format_date($comment->timestamp, 'long'),
            '@ago' => _fuzzy_delta($comment->timestamp),
        )
    );
}

Now check out your hot new datestamps!

Note: For my theme, I had to change the comment.tpl.php template file. You might have to change that and/or your node template files. Here's the chunk I had to add to mine:

<div class="submitted"><?php print theme('comment_submitted', $comment); ?></div>

Enjoy.

'Unsupported operand types' in Drupal 6.x

I recently (finally) updated my blog to Drupal 6, but I was having all sorts of trouble running cron. Instead of doing all the chrony magical stuff, it kept saying things like this:

Fatal error: Unsupported operand types in [...]/drupal/includes/common.inc on line 1376
Fatal error: Unsupported operand types in [...]/drupal/includes/common.inc on line 1546

If you run into either of these errors, you've found the one of the joys of a rapidly changing api! As of Drupal 6.x, Drupal has changed the arguments for the l() and url() functions. These functions used to have a bunch of arguments for things like 'absolute', 'query', 'fragment', and so forth. The problem was when you had a fragment but no query string, or you just wanted to declare a url as absolute. You had to add a whole bunch of null arguments to get to the one you actually wanted to set. So Drupal made a wise move and combined all the options into a single argument, fittingly named  $options .

How to fix your unsupported operand type error after the jump.

How to use your iPhone while lying down in bed

I love my phone. But it sucks to use it while lying down, because no matter how I turn it, it tries to stay right-side-up. But I've figured out a coping mechanism. So here's how to use your iPhone while lying down in bed:

  1. Lay down on your side.
  2. Turn the iPhone on it's side, so it's actually usable.
  3. Open Safari, or any other app that can't stay right-side-up.
  4. Wait for the interface to turn all sideways and worthless.
  5. Turn it so that the iPhone is completely upside-down. Most apps, Safari included, won't know what to do… so they'll freak out and stay in landscape mode.
  6. Now the landscape mode is usable!