Creating a hybrid Magento / WordPress application using Nginx

A very popular requirement by retailers for their Magento site is the ability to edit content using WordPress. I think the most common way of providing this integration is to install the Fishpig suite of modules for Magento. However, what if I told you that you could use Nginx to nicely integrate the two platforms?

Nginx comes with a feature called Server Side Includes that allows you to do page composition at the server level. It's actually really easy to implement and will make you look like a genius to your programmer friends (my friend actually used the word "mad man" but I know what he really meant). This article gives you a quick tutorial on the most important bits of how to set this up.

Magento / Nginx hybrid

The first thing you will need to do is write a custom router. We'll be dealing with a bunch of random URL paths from WordPress and we need to be able to handle the variability. I pretty much just copy-and-pasted the CMS router and made some modifications. My assumption is that all WordPress pages have a "blog" prefix in their URL paths, e.g. http://localhost/blog/i-love-cherry. You can configure this in the WordPress admin panel.

<?php

class Thai_Blog_Controller_Router extends Mage_Core_Controller_Varien_Router_Abstract
{
    /**
     * Add this router to the list of routers
     *
     * @param Varien_Event_Observer $observer
     */
    public function initControllerRouters(Varien_Event_Observer $observer)
    {
        $front = $observer->getEvent()->getFront();
        $front->addRouter('blog', $this);
    }

    public function match(Zend_Controller_Request_Http $request)
    {
        // Redirect Magento to the web installer if Magento isn't installed
        if (!Mage::isInstalled()) {
            Mage::app()->getFrontController()->getResponse()
                ->setRedirect(Mage::getUrl('install'))
                ->sendResponse();
            exit;
        }

        // If the URL path starts with "/blog" then serve the SSI-optimised
        // template
        if (strpos($request->getPathInfo(), '/blog') === 0) {
            $request->setModuleName('blog')
                ->setControllerName('page')
                ->setActionName('view')
            ;

            return true;
        }
        return false;
    }
}

We also need a custom template to generate the server-side include tag. In my Nginx configuration, I have a /wordpress location block that does a proxy pass to WordPress. The server-side include tag (that we generated in the Magento template) is then replaced with WordPress' response by Nginx.

<?php $includePath = preg_replace('/\/blog(\/)?/i', '/wordpress/', $this->getRequest()->getRequestUri()) ?>
<!--# include virtual="<?php echo $includePath ?>" -->

And finally, this is the Nginx configuration I used to glue everything together.

server {
    listen 80;

    root /var/www/magento;
    index index.php index.html index.htm;
    sendfile off;
    server_name local.magento.com;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    ssi on;
    location /wordpress/ {
        rewrite /wordpress/(.*)$ /$1 break;
        proxy_pass http://localhost:8000;
    }

    location ~ ^/wp- {
        proxy_pass http://localhost:8000;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php5-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
    }
}

server {
    listen 8000;

    root /var/www/wordpress;
    index index.php index.html index.htm;

    server_name local.magento.com;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php5-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
    }
}

That's how easy it is to merge WordPress and Magento into a single application! You can probably actually abuse this to create cool integrations with other stuff as well. I can envision building a store locator in Laravel and embedding it into Magento via SSI.

I have provided a sample repo for your review that includes everything else you need for the integration to work, e.g. controller, module XML file, etc.

Adding custom field validation to your Magento customer registration form

Did you know you can add custom field validation to your Magento customer registration form using event observers? I came across the following trick as I was trying to validate a field for a customer's loyalty account number (which required me to send a request to remote third-party service). The best part about it is that you don't need do any class overrides!

Assuming you've already added your desired custom fields to the relevant template, the first thing you need to do is add a new event observer entry to your config.xml for the customer_save_before event.

<?xml version="1.0"?>
<config>
    <global>
        <events>
            <customer_save_before>
                <observers>
                    <interslice_check_loyalty_number>
                        <type>singleton</type>
                        <class>interslice_loyalty/observer</class>
                        <method>checkLoyaltyNumber</method>
                    </interslice_check_loyalty_number>
                </observers>
            </customer_save_before>
        </events>
    </global>
</config>

Next you have to write the event observer itself! The logic for the event observer is pretty straightforward. You grab the custom data from the customer's submitted form and forward it to the third-party remote service for verification (in this particular case, the response from the third-party would be either true or false).

<?php

class Interslice_Loyalty_Model_Observer
{
    public function checkLoyaltyNumber(Varien_Event_Observer $observer)
    {
        /** @var Mage_Customer_AccountController $action */
        $action = $observer->getControllerAction();

        $request = $action->getRequest();
        $loyaltyNumber = $request->getPost('loyalty_number');

        if (isset($loyaltyNumber)) {
            $client = Mage::getModel('interslice_loyalty/client');
            $response = $client->verifyCustomer($loyaltyNumber, array(
                'email' => $request->getPost('email'),
                'first_name' => $request->getPost('firstname'),
                'last_name' => $request->getPost('lastname')
            ));

            // Handle verification error
            if (!$response) {
                $session = Mage::getSingleton('customer/session');
                $session->setCustomerFormData($request->getPost());
                $session->addError('Your loyalty number could not be validated');

                // See Mage_Customer_AccountController (line 273)
                $_SERVER['REQUEST_METHOD'] = 'GET';
            }
        }
        return $this;
    }
}

You may have noticed the line $_SERVER['REQUEST_METHOD'] = 'GET'; gets executed if the third-party couldn't verify the customer's loyalty number. If you look at line 271 of Mage_Customer_AccountController, you'll see that the controller action for account registration will throw an error if the server request method isn't POST. By changing the request method to GET, we're pretty much just piggybacking off another error for Magento to handle our own error. How neat is that?

Style against classes not elements

Imagine a scenario where a Zendesk ticket comes through in which your client's SEO agent asks you to change some h3 tags on your store locator page to h1. This should a relatively straightforward task... or is it?

You change the requested h3 tags to h1 and soon find that the styling of your store locator page has changed. The styling of the h1 and h3 tags have obviously been defined differently in your CSS:

h1 {
  color: red;
  margin-bottom: 20px;
}

h3 {
  border-bottom: 1px solid black;
  color: blue;
  margin-bottom: 10px;
}

Let's just say you're drunk and you play around with styling of the h1 tag until you get a look that's just right...

h1, h3 {
  border-bottom: 1px solid black;
  color: blue;
  margin-bottom: 10px;
}

You deploy your changes to production and move on to bigger and better tickets.

A few days later, but, you discover that you've inadvertently altered the styling of your contacts page (which the h1 tags had originally been styled for)! The client is livid and threaten to take their business elsewhere. You madly rush to fix your CSS only to finally despair at your inability to strike a balance between visual appeal and your client's SEO agent's arbitrary business rules.

This situation could have been avoided if you had originally added classes to the elements and styled against those instead.

.contacts-title {
  color: red;
  margin-bottom: 20px;
}

.store-locator-title {
  border-bottom: 1px solid black;
  color: blue;
  margin-bottom: 10px;
}

This allows you to change the element tag whilst maintaining the styling of that element.

Admittedly the scenario I just described was a tad simplistic. However, it's incredibly common to come across CSS problems that essentially boil down to not (and could have been avoided by) styling against classes. It's best to just apply good practice whilst you can.

Using Xdebug via the command line

Occasionally you may want to debug your PHP script or console application. This is pretty easy to do on Linux - you simply need to set the XDEBUG_CONFIG environment variable.

I've appended the following line to my .profile file:

export XDEBUG_CONFIG="idekey=PHPSTORM"

This will make sure that the XDEBUG_CONFIG environment variable is set each time you launch your terminal.

Improving the Magento search autocomplete

I feel like Mage_CatalogSearch_AjaxController::suggestAction could be better written. The following code snippet is what it currently looks:

<?php

class Mage_CatalogSearch_AjaxController extends Mage_Core_Controller_Front_Action
{
    public function suggestAction()
    {
        if (!$this->getRequest()->getParam('q', false)) {
            $this->getResponse()->setRedirect(Mage::getSingleton('core/url')->getBaseUrl());
        }

        $this->getResponse()->setBody($this->getLayout()->createBlock('catalogsearch/autocomplete')->toHtml());
    }
}

Essentally, if the query is empty then the suggest page will redirect you to the homepage. However, despite redirecting you to the home page, it still does all the database calls and processing as though the query wasn't empty.

The following code snippet is my alternative logic:

<?php

class Mage_CatalogSearch_AjaxController extends Mage_Core_Controller_Front_Action
{
    public function suggestAction()
    {
        // Don't perform a database query if the search query is blank or is
        // less than the minimum search query length.
        $minQueryLength = Mage::helper('catalogsearch')->getMinQueryLength();
        $query = $this->getRequest()->getParam('q', false);
        if (!$query || strlen($query) < $minQueryLength) {
            $this->getResponse()->setRedirect(Mage::getSingleton('core/url')->getBaseUrl());
        } else {
            $this->getResponse()->setBody($this->getLayout()->createBlock('catalogsearch/autocomplete')->toHtml());
        }
    }
}

If the query is empty or is less than the minimum allowed query character length, Magento will just redirect you to the homepage. It longer does any more unnecessary processing.