Skip to content
This is an exampe of a simple LAMP web app that uses MDB2, memcache, and Bootstrap.
JavaScript PHP
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.
protected
static
views
README.md
index.php

README.md

Design Rationale

I wanted to keep the solution for this project, whose scope is clearly defined and small, simple and avoid over-engineering it.

Apache Configuration

The protected directory is closed off as indicated in the Apache config since it shouldn't be publicly exposed. I set one month expiration headers for static assets to leverage web caching.

Data Model

I used MyISAM as the storage engine because I didn't need InnoDB's transactions, row locking or foreign keys. MyISAM works well for an app that'll have mostly reads compared to writes. I figured that most of the time, there will just be one write per week.

My first inclination was to normalize my database into 3NF --- a tracks table and table that related tracks to weeks with a rank parameter. This would cut down on redundant track data. But then everytime I grabbed the top tracks for a new week, I'd need to determine whether each track already existed in the tracks table. I ultimately decided that the advantages of normalization weren't worth the added complexity.

PHP Short Tags

I know some people warn against using PHP short tags, but I think they are worth it because they make templates more readable and are less tedious to type.

Data Retrieval Script

The script logs errors and has the option of checking the cron queue table.

I handle errors in connecting to the databse, cURL, and last.fm API calls. If the latter two occur, a cron job can be set to check the queue table in the database every five minutes and run the script again.

Nicedog PHP Framework

I used a tiny PHP framework called NiceDog --- it's one file! --- which is light-weight and has a nice routing system. Nicedog reminds me of Flask, a Python micro-framework, and I knew I wouldn't need an ORM or a lot of the other typical MVC functionality that comes with larger PHP frameworks.

My framework has two layers: a view layer and a combined model/controller layer. The only resources are tracks so I didn't see the need to separate models and controllers. There are only a few types of database interactions so I just used raw SQL instead of having the large overhead and added complexity of an ORM.

Database Connection Pooling and Caching

MDB2 is used to pool MySQL connections. Memcache caches data for dates and tracks in memory. Everytime new top tracks are retrieved, memcache is flushed.

Frontend Design

Bootstrap allowed me to quickly prototype a decent looking frontend. I was able to quickly put up a responsive and cross-browser compatible layout with a navigation bar and striped table.

I added a sweet 404 error page. How can you say no to a bald, bearded guy who delivers a long, awkward monologue?

Installation and Setup

Mac OS X Lion

Prerequisites

Ensure these directories exist:

$ sudo mkdir /usr/local/include
$ sudo mkdir /usr/local/bin
$ sudo mkdir /usr/local/lib
$ sudo mkdir -p /usr/local/man/man1

Setup Apache2

Apache2 is pre-installed onto Lion. Check with which httpd.

$ sudo chmod u+w /etc/apache2/httpd.conf
$ sudo vim httpd.conf

Uncomment #LoadModule php5_module libexec/apache2/libphp5.so.

To enable virtual hosts, uncomment #Include /private/etc/apache2/extra/httpd-vhosts.conf.

Enable .htaccess files by finding Directory /Library/WebServer/Documents and changing AllowOverride None to AllowOverride All. My full Mac OS X Apache2 config.

Install and Setup MySQL

Install Homebrew:

$ /usr/bin/ruby -e "$(/usr/bin/curl -fsSL https://raw.github.com/mxcl/homebrew/master/Library/Contributions/install_homebrew.rb)"

Use it to install MySQL:

$ brew install mysql

Have MySQL run as your system user:

$ unset TMPDIR
$ mysql_install_db --verbose --user=`whoami` --basedir="$(brew --prefix mysql)" --datadir=/usr/local/var/mysql --tmpdir=/tmp

Start MySQL:

$ mysql.server start

If MySQL started successfully, follow the instructions given by running this command:

$ /usr/local/Cellar/mysql/5.5.10/bin/mysql_secure_installation

If you run into problems logging in as root or setting up other user accounts, try this.

Setup PHP

PHP is pre-installed onto Lion. Check with which php.

$ cd /etc
$ sudo cp php.ini.default php.ini
$ sudo chmod ug+w php.ini
$ sudo chgrp admin php.ini

Edit php.ini to have:

error_reporting  =  E_ALL | E_STRICT
display_errors = On
html_errors = On
extension_dir = "/usr/lib/php/extensions/no-debug-non-zts-20090626"
short_open_tag = On

Change all instances of /var/mysql/mysql.sock to /tmp/mysql.sock.

Check PHP by restarting Apache (sudo apachectl restart) and putting this file in a document root that's served by Apache and hitting it in a browser:

<?php phpinfo(); ?>

Install memcached

$ brew install memcached

Start memcached with $ /usr/local/bin/memcached.

Install PHP Extensions MDB2 and memcache

Pear isn’t setup on Lion, but the install phar file is here, so we just need to run it:

$ cd /usr/lib/php
$ sudo php install-pear-nozlib.phar

Edit /etc/php.ini and find the line ;include_path = ".:/php/includes" and change it to include_path = ".:/usr/lib/php/pear". Now update the package sources:

$ sudo pear channel-update pear.php.net
$ sudo pecl channel-update pecl.php.net
$ sudo pear upgrade-all

Install MDB2, MDB2's mysqli driver, and memcache:

$ pear install MDB2-2.5.0b3
$ pear install MDB2_Driver_mysqli-1.5.0b3
$ pecl install memcache

Update php.ini for memcache extension by adding extension = memcache.so under the Dynamic Extensions section.

Put this file in a document root that’s served by Apache and check it works:

<?php
$memcache = new Memcache;
$memcache->connect('localhost', 11211) or die ("Could not connect");

$version = $memcache->getVersion();
echo "Server's version: ".$version."<br/>\n";

$tmp_object = new stdClass;
$tmp_object->str_attr = 'test';
$tmp_object->int_attr = 123;

$memcache->set('key', $tmp_object, false, 10) or die ("Failed to save data at the server");
echo "Store data in the cache (data will expire in 10 seconds)<br/>\n";

$get_result = $memcache->get('key');
echo "Data from the cache:<br/>\n";

var_dump($get_result);

Setup Web App

Apache's default document root is /Library/WebServer/Documents. I find it's convenient to have a symlink to that directory from ~/Sites.

$ cd && ln -s /Library/WebServer/Documents Sites

Edit /etc/hosts to have:

127.0.0.1         lastfm.loc

Create the lastfm directory in the Sites folder, cd into that, and git clone my repo:

$ mkdir ~/Sites/lastfm && cd ~/Sites/lastfm && git clone git://github.com/davidxia/lastfm.git

Have this entry in /etc/apache2/extra/httpd-vhosts.conf (replace lastfm.loc with your choice of URL):

<VirtualHost *:80>
    DocumentRoot "/Users/[your-username]/Sites/lastfm"
    ServerName lastfm.loc
    ErrorLog "/private/var/log/apache2/lastfm.loc-error_log"
    CustomLog "/private/var/log/apache2/lastfm.loc-access_log" common

    <Directory /Users/[your-username]/Sites/lastfm>
        Options All -Indexes

        <IfModule rewrite_module>
            RewriteEngine On
            RewriteCond %{REQUEST_FILENAME} !-d
            RewriteCond %{REQUEST_FILENAME} !-f
            RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
        </IfModule>
    </Directory>

    <Directory /Users/[your-username]/Sites/lastfm/protected>
        Order allow,deny
        deny from all
    </Directory>

    <IfModule expires_module>
        ExpiresActive On
        ExpiresDefault "access plus 300 seconds"
        ExpiresByType text/css "access plus 1 month"
        ExpiresByType text/javascript "access plus 1 month"
        ExpiresByType application/javascript "access plus 1 month"
        ExpiresByType application/x-javascript "access plus 1 month"
        ExpiresByType image/gif "access plus 1 month"
        ExpiresByType image/jpg "access plus 1 month"
        ExpiresByType image/png "access plus 1 month"
    </IfModule>
</VirtualHost>

Restart Apache and try hitting http://lastfm.loc in a browser.

Import Sample Data

Log into MySQL and run these commands:

> CREATE DATABASE lastfm;
> GRANT ALL PRIVILEGES ON lastfm.* to "lastfm"@"localhost" IDENTIFIED BY "lastfm";
> FLUSH PRIVILEGES;
> EXIT;

Now run mysql import with the MySQL dump file in the repo.

$ mysql -u lastfm -p -h localhost lastfm < protected/lastfm.dump

To dump the lastfm table:

$ mysqldump lastfm -u lastfm -p > protected/lastfm.dump

Setup Cron Job

Insert this into crontab:

@weekly php ~/Sites/lastfm/protected/get_top_tracks.php > ~/Sites/lastfm/protected/cron.log 2>&1

OR

0 0 * * 0 php ~/Sites/lastfm/protected/get_top_tracks.php > ~/Sites/lastfm/protected/cron.log 2>&1

To setup the cron job that handles queued up API calls:

*/5 * * * * php ~/Sites/lastfm/protected/get_top_tracks.php checkQueue > ~/Sites/lastfm/protected/cron.log 2>&1

Ubuntu 10.04

This assumes you have an operational LAMP stack.

Configure Apache2

Clone my repo, and point Apache at the root directory of the cloned repo. If you are using virtual hosts, see my vhost entry below.

git clone git://github.com/davidxia/lastfm.git

My Setup

I've deployed a live version at lastfm.davidxia.com I created a symlink from /var/www/lastfm to /home/david/lastfm and put this in Apache virtual hosts:

<VirtualHost *:80>
    ServerName lastfm.davidxia.com
    DocumentRoot /var/www/lastfm

    <Directory /var/www/lastfm/>
        Options All -Indexes
        Order allow,deny
        allow from all

        <IfModule mod_rewrite.c>
            RewriteEngine On
            RewriteCond %{REQUEST_FILENAME} !-d
            RewriteCond %{REQUEST_FILENAME} !-f
            RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
        </IfModule>
    </Directory>

    <Directory /var/www/lastfm/protected/>
        Order allow,deny
        deny from all
    </Directory>

    <IfModule mod_expires.c>
        ExpiresActive On
        ExpiresDefault "access plus 300 seconds"
        ExpiresByType text/css "access plus 1 month"
        ExpiresByType text/javascript "access plus 1 month"
        ExpiresByType application/javascript "access plus 1 month"
        ExpiresByType application/x-javascript "access plus 1 month"
        ExpiresByType image/gif "access plus 1 month"
        ExpiresByType image/jpg "access plus 1 month"
        ExpiresByType image/png "access plus 1 month"
    </IfModule>
</VirtualHost>

Then I reloaded Apache:

$ sudo service apache2 reload

Install memcached

Install and start memcached:

$ sudo apt-get install memcached
$ sudo service memcached

Install PHP Extensions MDB2 and memcache

Check that you have PEAR and PECL installed. If not, run:

$ sudo apt-get install php-pear php5-dev libcurl3-openssl-dev

Update package sources:

$ sudo pear channel-update pear.php.net
$ sudo pecl channel-update pecl.php.net
$ sudo pear upgrade-all

Install MDB2, MDB2's mysqli driver, and memcache

$ pear install MDB2-2.5.0b3
$ pear install MDB2_Driver_mysqli-1.5.0b3
$ pecl install memcache

Update php.ini for memcache extension by adding extension = memcache.so under the Dynamic Extensions section.

Put this file in a document root that’s served by Apache and check it works:

<?php
$memcache = new Memcache;
$memcache->connect('localhost', 11211) or die ("Could not connect");

$version = $memcache->getVersion();
echo "Server's version: ".$version."<br/>\n";

$tmp_object = new stdClass;
$tmp_object->str_attr = 'test';
$tmp_object->int_attr = 123;

$memcache->set('key', $tmp_object, false, 10) or die ("Failed to save data at the server");
echo "Store data in the cache (data will expire in 10 seconds)<br/>\n";

$get_result = $memcache->get('key');
echo "Data from the cache:<br/>\n";

var_dump($get_result);

Import Data into MySQL

See "Import Sample Data" above. The commands should be the same.

Set Cron Task

@weekly php /path/to/lastfm/protected/get_top_tracks.php > /path/to/lastfm/protected/cron.log 2>&1

To setup the cron job that handles queued up API calls:

*/5 * * * * php ~/Sites/lastfm/protected/get_top_tracks.php checkQueue > ~/Sites/lastfm/protected/cron.log 2>&1
Something went wrong with that request. Please try again.