Drupal 7 on nginx + php + postgres on Ubuntu 11.10

After dealing with nginx for some time I loathe the thought of going back to Apache. Therefore when I need to set up a new environment, even if its just for development, I try and make it work with nginx first. We’ll assume you’re starting out with a fresh install of Ubuntu Server 11.10.

To start out, we install all the necessary packages using apt-get.

sudo apt-get install nginx postgresql php5-common php5-cli php5-suhosin php5-pgsql php5-cgi php5-gd php5-curl php5-fpm

You’ll see a lot of other nginx + php tutorials outline writing your own fastcgi init script or to grab the spawn-fcgi process from lighthttpd. Neither of these are necessary since php 5.3 as we can now install the FastCGI Process Manager (php5-fpm).

Now let’s start nginx and php5-fpm.

sudo /etc/init.d/php5-fpm start && sudo /etc/init.d/nginx start

You can test this by browsing to the IP of the server, or localhost if this is being done on your local machine. You should see a “Welcome to nginx!” message.

Now we need to set up our first virtual host and install Drupal. Let’s start by creating a new site file at @/etc/nginx/sites-available/@.

sudo vi /etc/nginx/sites-available/drup7

Now we need to enter the following contents into this new file [source]:

    server {
            server_name drup7.dev;
            root /var/www/drup7; ## <-- Your only path reference.

            location = /favicon.ico {
                    log_not_found off;
                    access_log off;
            }

            location = /robots.txt {
                    allow all;
                    log_not_found off;
                    access_log off;
            }

            # This matters if you use drush
            location = /backup {
                    deny all;
            }

            # Very rarely should these ever be accessed outside of your lan
            location ~* .(txt|log)$ {
                    allow 192.168.0.0/16;
                    deny all;
            }

            location ~ ..*/.*.php$ {
                    return 403;
            }

            location / {
                    # This is cool because no php is touched for static content
                    try_files $uri @rewrite;
            }

            location @rewrite {
                    # Some modules enforce no slash (/) at the end of the URL
                    # Else this rewrite block wouldn't be needed (GlobalRedirect)
                    rewrite ^/(.*)$ /index.php?q=$1;
            }

            location ~ .php$ {
                    fastcgi_split_path_info ^(.+.php)(/.+)$;
                    #NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
                    include fastcgi_params;
                    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                    fastcgi_intercept_errors on;
                    fastcgi_pass 127.0.0.1:9000;
            }

            # Fighting with ImageCache? This little gem is amazing.
            location ~ ^/sites/.*/files/imagecache/ {
                    try_files $uri @rewrite;
            }
            # Catch image styles for D7 too.
            location ~ ^/sites/.*/files/styles/ {
                    try_files $uri @rewrite;
            }

            location ~* .(js|css|png|jpg|jpeg|gif|ico)$ {
                    expires max;
                    log_not_found off;
            }
    }

Save and close. I want my site to be accessed via http://drup7.dev/ so this domain was used as the server_name. Let’s enable our new site by creating a symbolic link:

    sudo ln -s /etc/nginx/sites-available/drup7 /etc/nginx/sites-enabled/

Ok, our site is set up. Now lets go make sure /var/www/drup7 exists and populate it with the Drupal source.

    sudo mkdir /var/www
    cd /var/www
    sudo wget http://ftp.drupal.org/files/projects/drupal-7.8.tar.gz
    sudo tar xvf drupal-7.8.tar.gz
    sudo mv drupal-7.8 drup7
    sudo rm drupal-7.8.tar.gz

Now let’s make a settings.php file and set ownership of the whole directory to the www-data user.

    sudo cp drup7/sites/default/default.settings.php drup7/sites/default/settings.php
    sudo chown -R www-data:www-data drup7

Ok, now we need to create a postgres user and dataabse.

    sudo -u postgres -i
    createuser drup7 --pwprompt --encrypted
    createdb drup7 --owner=drup7
    exit

You’ll be prompted for a password and several access-releated questions when you run createuser, you’ll probably want to answer “n” for all of these.

Since we’re using a real domain in the virtual host file, you’ll either need to configure your DNS settings to point to the server you’re working on, or more likely just change your hosts file. I’m working on a virtual machine, so my hosts entry will look like so:

    192.168.70.125    drup7.dev

We’ve made a bunch of changes, let’s restart php5-fpm and nginx:

    sudo /etc/init.d/php5-fpm stop && sudo /etc/init.d/nginx stop
    sudo /etc/init.d/php5-fpm start && sudo /etc/init.d/nginx start

Fire up your favorite browser and navigate to http://drup7.dev/! You should see the Drupal installation screen. Now go through the installation wizard, it should automatically detect that you have Postres installed and not MySQL.

Pat yourself on the back, you’re all done!

One-click Node.js deployment with Cloud9 and Heroku

Developing and deploying web applications has never been so easy! I love the idea of cloud development. Projects like Cloud9 and Kodingen are paving the way for a development world in the cloud — personally I would love to slim down my primary machine to a small, lightweight Chromebook and offload all my processing needs to a remote server.

With that in mind, let’s take a look at how to write and deploy an app entirely in the cloud. Start by signing up for an account and creating a new project in Cloud9. You can either do this from the ground up, or start with an existing project from a Git repo. I’m going to start with a basic app using a default implementation of the Express framework.

We’ll start by creating a file Heroku will be expecting: web.js.

/**
 * Module dependencies.
 */

var express = require('express');

var app = module.exports = express.createServer();

// Configuration

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 
});

app.configure('production', function(){
  app.use(express.errorHandler()); 
});

// Routes

app.get('/', function(req, res){
  res.render('index', {
    title: 'Node Test'
  });
});

var port = process.env.PORT || 3000;
app.listen(port);
console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);

Notice we’re defining two paths that don’t yet exist: /views and /public. We’ll need to populate /views with layout.jade as a global layout, and index.jade since we’re specifically calling a render method for it in our / route.

/views/layout.jade

!!!
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
  body!= body

/views/index.jade

h1= title
p Welcome to #{title}

And our stylesheet:
/public/stylesheets/style.css

body {
  padding: 50px;
  font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}

a {
  color: #00B7FF;
}

It looks like we’re good to, right? Wrong. We’ll need to specify dependencies using a package.json file, just like we would if we were publishing our package on npm. Here we’re depending on express, which by default uses jade as its templating engine.

package.json

{
  "name": "node-test",
  "version": "0.0.1",
  "dependencies": {
    "express": "2.2.0",
    "jade": "0.15.4"
  }
}

Not quite done yet, there’s one more file we need to declare which processes Heroku should start up when we deploy:

Procfile

web: node web.js

Okay, we’re almost there! Head to the Deploy tab of the left menu bar and add your Heroku account as a deploy target. You can create a new app right from here to deploy to. Once your app is set up, click the big green Deploy button and watch it go! You should get confirmation in the Cloud9 console if it succeeded. Navigate to the Heroku app URL and see your app in the wild!

Magento Module Version and Other Config Values

There are times when you want to determine your module’s version number programmatically — I like to append module versions to the User-Agent header of any calls back to my web services so I can access current statistics about version usage. Now, you could just include a constant somewhere in your module and update it as necessary. But why do that when the Mage core already has access to the value you defined in your config.xml file?

Here’s a simple example, assuming your module is called Myapp and is in the Package code namespace:

$version = Mage::getConfig()->getModuleConfig("Package_Myapp")->version;

This is fairly straight forward. The Mage::getConfig() method gets a reference to the config parsing class, and then we can load up the config specific to our module using the getModuleConfig method. We actually have access to a few more variables beyond version, depending on the structure of your config file, but version seems to be the most useful.

Extending Magento Customer and Product Attributes

Magento’s EAV database model allows for easy extension of its current attributes associated with different entities. Credit for this needs to go to Alan Storm as he was the one to show me this when I needed it. Note – this method is valid for version 1.5.x.x, I’m not certain this will work on anything prior.

Extending an entity’s attributes is done by executing the addAttribute method on the Mage_Eav_Model_Entity_Setup class. I’ve seen references to add attributes using this class by instantiating the class using something like:

$setup = new Mage_Eav_Model_Entity_Setup('core_setup');

But it really only makes sense to run this code once – preferably during the installation of the module. So if your module doesn’t have one already, add a setup script by adding the following to your module’s config.xml. For demonstration purposes, we’ll assume our package name is Package and our app is called Myapp.

<?xml version="1.0"?>
<config>
    <global>
        <resources>
            <myapp_setup>
                <setup>
                    <module>Package_Myapp</module>
                    <class>Package_Myapp_Model_Resource_Eav_Mysql4_Setup</class>
                </setup>
                <connection>
                    <use>core_setup</use>
                </connection>
            </myapp_setup>
            <myapp_write>
                <connection>
                    <use>core_write</use>
                </connection>
            </myapp_write>
            <myapp_read>
                <connection>
                    <use>core_read</use>
                </connection>
            </myapp_read>
        </resources>
    </global>
</config>

Now create a setup file in your modules directory in sql/myapp_setup/mysql4-install-0.0.1.php, remembering to change the version number to correspond to the version number in your config.xml. Here, we can get an instance of the Mage_Eav_Model_Entity_Setup simply by referencing $this. Here’s an example of adding an attribute to the Custom model.

addAttribute('customer', 'custom_field', array(
    'type' => 'varchar',
    'input' => 'text',
    'label' => 'Custom Field',
    'visible' => false,
    'required' => false,
    'position' => 69,
));

As you can see this method takes three parameters: an entity type, a unique identifier for this attribute, and an array of the attribute’s options. There are many options available here, you can pretty much set a value for any column on the eav_attribute table, although there’s a _prepareValues method that sets defaults and reads shorter versions of the column names. This method is located in: app/code/core/Mage/Eav/Model/Entity/Setup.php

/**
 * Prepare attribute values to save
 *
 * @param array $attr
 * @return array
 */
protected function _prepareValues($attr)
{
    $data = array();
    $data = array(
        'backend_model'             => $this->_getValue($attr, 'backend', ''),
        'backend_type'              => $this->_getValue($attr, 'type', 'varchar'),
        'backend_table'             => $this->_getValue($attr, 'table', ''),
        'frontend_model'            => $this->_getValue($attr, 'frontend', ''),
        'frontend_input'            => $this->_getValue($attr, 'input', 'text'),
        'frontend_label'            => $this->_getValue($attr, 'label', ''),
        'frontend_class'            => $this->_getValue($attr, 'frontend_class', ''),
        'source_model'              => $this->_getValue($attr, 'source', ''),
        'is_required'               => $this->_getValue($attr, 'required', 1),
        'is_user_defined'           => $this->_getValue($attr, 'user_defined', 0),
        'default_value'             => $this->_getValue($attr, 'default', ''),
        'is_unique'                 => $this->_getValue($attr, 'unique', 0),
        'note'                      => $this->_getValue($attr, 'note', ''),
    );
    return $data;
}

So on the left, the key of the $data array is the database column, and the _getValue method is reading the name you use in your attribute options when calling the addAttribute method. The third parameter of the _getValue is the default value should you omit the value from your options array.

Beyond these values, you can also specify a ‘group’ to assign your new attribute to a group set, and ‘option’ to pass in an array of values to be used in a select list. I haven’t tested these out extensively, let me know if you discover how these are used.

Also, remember that if your setup script has already run for your module and you need to run it again simply remove the row where code = 'myapp_setup' in the core_resource table. Your setup script should then run the next time a page is rendered.

Adding to vBulletin Sessions

vBulletin sessions are accessible throughout the application using the $vbulletin->session object. Session variables are stored in $vbulletin->session->vars as an array, so accessing something like the useragent associated with the session is as simple as:

$useragent = $vbulletin->session->vars['useragent'];
echo $useragent;

The above example in my case would output “Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0) Gecko/20100101 Firefox/4.0”

vBulletin stores session data in a rather unique way. Each session variable is associated with a column on the session table in the database. This means if you want to extend the session to store some new information, you’ll need to alter the table to add your new columns. My Product already has an install script, so I’ll add it there, but you could also add it manually by running the below SQL statement against your database.

$vbulletin->db->query("
    ALTER TABLE `" . TABLE_PREFIX . "session`
    ADD COLUMN `new_session_variable` varchar(255) DEFAULT NULL;
");

So now we have our new column. How do we write to it you ask? Well the $vbulletin->session object has a method for that unambiguously called set. But we have a problem. We can use the set method to build an array of key => value pairs to write to the database, however the build_query_array method that prepares the values for the insert statement only matches keys based on hard-coded list of column names defined in the class. This means that before we call our set method we need to extend this array to include our new column.

$vbulletin->session->db_fields = array_merge(
    $vbulletin->session->db_fields,
    array(
        'new_session_variable' => TYPE_STRING
    )
);

Notice we’re also assigning a TYPE to this column. This will be used in the cleaning process that validates and prepares the value for the database, so it’s important to get this type right. The method that uses these accepts either TYPE_INT or TYPE_STRING.

Now we’re ready to set our variable. Adding to the script above, we get:

$vbulletin->session->db_fields = array_merge(
    $vbulletin->session->db_fields,
    array(
        'new_session_variable' => TYPE_STRING
    )
);

$vbulletin->session->set('new_session_variable', 'foobar');

Now our session variable is queued up to be saved, but keep in mind that it isn’t in our session just yet. At the end of each page load vBulletin executes a shutdown method that, among other things, executes $vbulletin->session->save() which will actually write this value to the database. On the next page load you’ll be able to access your new variable using $vbulletin->session->vars['new_session_variable'].

Automatically Set vBulletin Admin Settings On Save

This is a pretty specific issue I’m addressing here but I thought I would continue the theme from my last post in regard to setting up admin settings in vBulletin. I had a need to store some config values accessible by my plugin code that directly relate to the values of the user-entered settings. There’s lots of places I could store this data, but for the sake of consistency and adhering to the existing structure of vBulletin I decided to put this data in new Setting fields. Here’s how I did it.

Creating the Settings

Referring back to my last post, let’s set up a new field with a custom Option Code. Since this field will be automatically populated, we don’t want to allow the user to be able to input anything here. They will, however, be able to see the value stored in the field. Here’s what my Option Code looks like.

<input type=\"text\" class=\"bginput\" name=\"setting[" . $setting['varname'] . "]\" id=\"it_setting_" . $setting['varname'] . "\" value=\"" . $setting['value'] . "\" size=\"40\" dir=\"ltr\" tabindex=\"1\" disabled=\"true\" />

If you look at the source generated on the Settings page you’ll notice this looks nearly identical to any other text input, although I’ve added disabled="true" to disallow user input. That’s it! Save this field and you should see it in your Settings, but you should not be able to edit it. I also put “(Auto-Populated)” in the title of this field and added a note in the description that this field does not need to be completed by the user to avoid any confusion.

Building the Plugin

Now we need to write a plugin to listen for submissions of the Settings form, check if certain conditions are met, and save the desired values into our new field. Create a new plugin within your Product and set it to run on the admin_options_processing hook. This hook runs in the middle of the save_settings function that writes setting values to the database.

/**
* Save the setting POST var
*/
$setting_array = $vbulletin->GPC['setting'];

/**
* Check if the user input field is being submitted. Also
* test for the existence of a unique variable we'll append
* to the global $vbulletin object to prevent an infinite loop.
*/
if ($setting_array['productname_userinputfield'] && !$vbulletin->productname_admin_processing) {
    // Set our unique variable to true to prevent this IF block
    // from executing multiple times.
    $vbulletin->productname_admin_processing = true;

    // Set our new value. 
    $autovalues = array("productname_autofield" => "foobar");

    // Save our value
    save_settings($autovalues);
}

Couple things to note here. First, note that we’re only executing our code if $vbulletin->productname_admin_processing evaluates as a negated variable. This is because our hook is running in the middle of the save_settings function, but we need to call this same function in order to save our setting values. We can save a variable as a flag on the global $vbulletin object to guarantee that it will be accessible anywhere in our application. Now, when save_settings executes again our code won’t infinitely nest executions of the save_settings function within itself. Also, save_settings takes one parameter, an array of key-value pairs where the key is the fieldname. Do not name this variable $settings in your plugin as it will overwrite the existing $settings variable the parent save_settings function is processing, and none of the user-input fields will end up being saved.

Adding vBulletin Admin Settings to Your Product

Adding settings for you product is usually a necessary part of creating a distributable package. Here’s a guide on how to add settings to your product before you export it.

Enabling Debug Mode

To enable debug mode, simply add the following line to the top of your includes/config.php. This setting is not recommended for production environments.

$config['Misc']['debug'] = true;

Now navigate to your Settings -> Options page in admincp. You’ll notice some new “Developer” options, including “Add New Settings Group”. This will let you create a new setting group and assign it to a product. When you open up this new Setting Group you’ll notice an Add Setting link in the setting group title bar to add individual settings to that group. Here’s the options you can set:

  • Varname: This is the name you’ll use to call your setting out programmatically using $vbulletin->options[] in a plugin or {vb:raw vboptions} in a template. I usually stick to a productname_fieldname naming convention.
  • Setting Group: If you clicked the “Add Setting” link from within your new setting group this should already be on the correct value. If not you can change it here.
  • Title: A phrase to be used as the title in the admin menu.
  • Description: A phrase to be used as the description in the admin menu.
  • Option Code: This is an option for how you want this particular setting to display in the admin menu. A blank value is usually sufficient, though a value of “textarea” renders as a textarea rather than a single-line text input, a “yesno” value will render “Yes” and “No” radio buttons, or you can enter custom display code. See below for an example.
  • Default: The default value.
  • Data Validation Type: This allows you to select from a set list of data types the form expects to receive when this field is submitted.
  • Validation PHP Code: Allows you to enter custom validation rules for this fields. More on this below.
  • Display Order: The numerical value of display order within this settinggroup.
  • Blacklist: This determines whether or not the value should be backed up when admin settings are backed up. Blacklisted fields are skipped in the backup process.
  • vBulletin Default: This determines whether or not the field will be reset during upgrade scripts. You’ll likely want to set this to “No” for any additional fields if you want the values to persist through any upgrades.
Retrieving settings in your code

To retrieve a setting, simply call it as I briefly mentioned above. For example, this plugin script would print the admin setting with the Varname “productname_fieldname”.

echo $vbulletin->options['productname_fieldname'];

Or you can use them as variables in your template files. Here’s a field value wrapped in a header tab.

<h1>{vb:raw vboptions.productname_fieldname}</h1>
Custom Option Code Display

You may run into a situation where you want a setting input to display in a method not supported by the standard options. A <select> tag is a great example of this. The Option Code option natively supports some workarounds for this. First, you can specify select:piped followed by a list of select values. Here’s an example of how the disable_ajax field in vBulletin’s General Settings accomplishes this:

select:piped
0|enable_all_ajax_features
1|disable_problematic_ajax_features
2|disable_all_ajax_features

These values are all accompanied by entries in the @[email protected] table with a fieldname of cpoption and a varname corresponding to the value entered for the select option (e.g. enable_all_ajax_features).

Another method is to specify the actual HTML to be rendered to the page for the inputs. An example of this is the sitemap_priority setting in the XML Sitemap setting group.

" . vb_number_format('0.0', 1) . "
" . vb_number_format('0.1', 1) . "
" . vb_number_format('0.2', 1) . "
" . vb_number_format('0.3', 1) . "
" . vb_number_format('0.4', 1) . "
" . vb_number_format('0.5', 1) . "
" . vb_number_format('0.6', 1) . "
" . vb_number_format('0.7', 1) . "
" . vb_number_format('0.8', 1) . "
" . vb_number_format('0.9', 1) . "
" . vb_number_format('1.0', 1) . "

The thing to keep in mind here is that the value you insert will be used in a php script between two double-quotes. Therefore, it’s necessary to escape any double quotes you want to use with a backslash (\). It also means, however, that you can break the string and insert values accessible from php variables and functions. Just remember to start the string back up with another double-quote. You can access data corresponding to the current setting using the $setting array, as shown above.

Custom Validation PHP Code

This one is pretty straightforward. You simply need to write a php script to return true or false based on the $data variable. This example is from the thumbcolor setting in the Message Attachment Options and validates that the data is in hex color code format. The preg_match function returns the number of times the regex pattern occurs in a string, so here its testing whether or not that value is greater than 0.

return (preg_match('~^#([0-9A-F]{6})$~i', $data) > 0);

Create a custom Session Namespace with a Model in Magento

It’s possible to create a new Session model in all your custom modules in Magento. This will give you a unique session namespace to isolate your session variables from the rest of the framework.

First, make sure you have your <models /> node configured with a class prefix to use.

<config>
    <global>
        <models>
            <myapp>
                <class>Package_Myapp_Model</class>
            </myapp>
        </models>
    </global>
</config>

Once you have this set up, build your model. Here’s a simple Session.php model file.

<?php
class Package_Myapp_Model_Session extends Mage_Core_Model_Session_Abstract {

 public function __construct() {
   $namespace = 'myapp';
   $namespace .= '_' . (Mage::app()->getStore()->getWebsite()->getCode());

   $this->init($namespace);
    Mage::dispatchEvent('myapp_session_init', array('myapp_session' => $this));
 }

}

The Mage_Core_Model_Session_Abstract model already contains everything we need. Access session variables using the magic get/set methods.

// Set the "foo" variable
Mage::getSingleton("myapp/session")->setFoo("bar");
// Same thing
Mage::getSingleton("myapp/session")->setData("foo", "bar");

// Another example. Let's say we want to name our variable "foo_bar"
Mage::getSingleton("myapp/session")->setFooBar("data");
// or
Mage::getSingleton("myapp/session")->setData("foo_bar", "bar");

// Get data the same way
Mage::getSingleton("myapp/session")->getFoo()
// or
Mage::getSingleton("myapp/session")->getData("foo");

Check if jQuery is already included in the page

When you’re building a distributable package for a framework that is likely to have several other third-party packages installed, it’s difficult to know when modifications or additions to the framework have already been made further up the chain of operations. I recently ran into this in a package I built that relies on jQuery – another package installed on the system had already included and extended the jQuery object. When my addition came into play, the jQuery object was reset and the other package broke as a result.

To fix this, I wrote a script to test if the jQuery object had been instantiated yet and, if not, to load it. Here’s what it looks like:

if (typeof jQuery === 'undefined') {
    document.write(unescape("%3Cscript src='http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js' type='text/javascript'%3E%3C/script%3E"));
}

Symlink Module Files in Magento

Update: As of Magento 1.5.1.0 there is now an “Allow Symlinks” option that can be found in Advanced -> Developer -> Template Settings that resolves any need for this type of hack.

When I’m developing a module for any given framework, I like to keep my VCS repository clean and only include the files that I would actually package for distribution. The sloppy way to handle this would be to develop my module within the framework, and periodically copy changes to my version controlled files for committing. Instead, I use symbolic links to insert my module files into the framework and only make changes to the original version-controlled files.

Unfortunately Magento doesn’t like this as the template engine explicitly tests file include paths against their real paths and fails if they don’t match. However, we can easily hack the Magento core to bypass this test:

Open up this file:

app/code/core/Mage/Core/Block/Template.php

Navigate to line 213 (as of Magento 1.5.0.1). You should notice the following try/catch block:

try {
    $includeFilePath = realpath($this-&gt;_viewDir . DS . $fileName);
    if (strpos($includeFilePath, realpath($this-&gt;_viewDir)) === 0) {
        include $includeFilePath;
    } else {
        Mage::log('Not valid template file:'.$fileName, Zend_Log::CRIT, null, null, true);
    }

} catch (Exception $e) {
    ob_get_clean();
    throw $e;
}

To allow symlinks, all we need to do is comment out the if block:

try {
    $includeFilePath = realpath($this-&gt;_viewDir . DS . $fileName);
    // if (strpos($includeFilePath, realpath($this-&gt;_viewDir)) === 0) {
        include $includeFilePath;
    // } else {
    //     Mage::log('Not valid template file:'.$fileName, Zend_Log::CRIT, null, null, true);
    // }

} catch (Exception $e) {
    ob_get_clean();
    throw $e;
}

Now I can execute my shell script to set up my symlinks. Here’s what my shell script looks like:

export MAGENTO_MODULE_DIR=~/Projects/magento-plugin
export MAGENTO_ROOT_DIR=/www/magento-dev
ln -s -f $MAGENTO_MODULE_DIR/app/code/local/* $MAGENTO_ROOT_DIR/app/code/local/
ln -s -f $MAGENTO_MODULE_DIR/app/design/frontend/base/default/layout/* $MAGENTO_ROOT_DIR/app/design/frontend/base/default/layout/
ln -s -f $MAGENTO_MODULE_DIR/app/design/frontend/base/default/template/* $MAGENTO_ROOT_DIR/app/design/frontend/base/default/template/
ln -s -f $MAGENTO_MODULE_DIR/app/etc/modules/* $MAGENTO_ROOT_DIR/app/etc/modules/
ln -s -f $MAGENTO_MODULE_DIR/app/locale/en_US/* $MAGENTO_ROOT_DIR/app/locale/en_US/
ln -s -f $MAGENTO_MODULE_DIR/skin/frontend/* $MAGENTO_ROOT_DIR/skin/frontend/

Keep in mind it’s not a good idea to do these types of core hacks to any type of production environment.