How you could serve static content files from your Magento extension folder

Featured Image

As a part of my personal ongoing “unobtrusive Magento extensions” campaign. I will show you another “hack”/approach you can apply in order to squeeze your static files under the main extension folder.

When I say static, I am mainly referring to images, CSS and JavaScript in this case.

For example, imagine you are coding an extension called “Sociable“, which will display several links to various web services like Twitter, GoogleBuzz, etc. Links that you can click and the publish a short message about a product/category on that web service. Something like on the image shown below. Logical question is, where do you store your images?

Usually images are stored within the root /media/ folder. So it might seem perfectly fit to store them under the /media/mycompany/mymodule/ folder.

Similar to that, CSS files are usually stored under the /skin/frontend/default/default/css/, possibly in a sub-folder like /skin/frontend/default/default/css/mycompany/mymodule/.

Now, this is not a bad approach, possibly just little “distributed” in terms of spreading your extension across several root folders, making it a bit harder for manual “cleanup” or maintenance, etc. This is the way how most of extensions are build these days.

Now I will show you another possible approach (not necessarily the better one) but more “centralized” in terms of keeping everything within one folder.

Basically it comes down to storing all your public static files under the extension folders like:

  • /app/code/community/Inchoo/Sociable/public/js
  • /app/code/community/Inchoo/Sociable/public/css
  • /app/code/community/Inchoo/Sociable/public/image

And adding few lines of code into a custom controller like /app/code/community/Inchoo/Sociable/controllers/StaticController.php that will basically be acting as static file server. Don’t worry, its actually pretty simple.

Let’s start with our /app/code/community/Inchoo/Sociable/etc/config.xml file. Within it we will first add the router definition like shown below.

<frontend>
	<routers>
		<inchoo_sociable>
			<use>standard</use>
			<args>
				<module>Inchoo_Sociable</module>
				<frontName>inchoo_sociable</frontName>
			</args>
		</inchoo_sociable>
	</routers>
</frontend>

Now we will add some basic security check/handling to our files we will be serving torugh controller action. We can put this logic into the /app/code/community/Inchoo/Sociable/Helper/Data.php file, like shown below.

class Inchoo_Sociable_Helper_Data extends Mage_Core_Helper_Abstract
{
    const DIR_IMAGE = 'image';
    const DIR_CSS = 'css';
    const DIR_JS = 'js';
 
    /**
     * @param string 'icons' or 'css' or 'js'
     * @return string
     */
    public function getExtPubDir($type)
    {
        return __DIR__.DS.'..'.DS.DS.'public'.DS.$type;
    }
 
    public function getAllowedFiles($dir)
    {
        $results = array();
        $handler = opendir($this->getExtPubDir($dir));
 
        /* Might be improved later via cache, not to list entire folder on each request. */
        while ($file = readdir($handler)) {
            if ($file != "." && $file != "..") {
                $results[] = $file;
            }
        }
 
        closedir($handler);
 
        return $results;
    }
}

Finally we will create controller /app/code/community/Inchoo/Sociable/controllers/StaticController.php that will be used to serve our static files directly from our extension folder (code shown below).

class Inchoo_Sociable_StaticController extends Mage_Core_Controller_Front_Action
{
    public function getAction()
    {
        $type = $this->getRequest()->getParam('t');
 
        switch ($type) {
            case 'css':
                return $this->_css();
                break;
            case 'image':
                return $this->_image();
                break;
            case 'js':
                return $this->_js();
                break;
            default:
                $this->getResponse()->setHeader('HTTP/1.1','404 Not Found');
                $this->getResponse()->setHeader('Status','404 File not found');
                break;
        }
    }
 
    private function _css()
    {
        $file = $this->getRequest()->getParam('f');
        $type = $this->getRequest()->getParam('t');
 
        $helper = Mage::helper('inchoo_sociable');
 
        if (in_array($file, $helper->getAllowedFiles($type))) {
            $this->getResponse()->setHeader('Content-Type', 'text/css');
            $this->getResponse()->setBody(file_get_contents($helper->getExtPubDir($helper::DIR_CSS).DS.$file));
        } else {
            $this->getResponse()->setHeader('HTTP/1.1','404 Not Found');
            $this->getResponse()->setHeader('Status','404 File not found');
        }
    }
 
    private function _image()
    {
        $file = $this->getRequest()->getParam('f');
        $type = $this->getRequest()->getParam('t');
 
        $helper = Mage::helper('inchoo_sociable');
 
        if (in_array($file, $helper->getAllowedFiles($type))) {
            $icon = new Varien_Image($helper->getExtPubDir($helper::DIR_IMAGE).DS.$file);
            $this->getResponse()->setHeader('Content-Type', $icon->getMimeType());
            $this->getResponse()->setBody($icon->display());
        } else {
            $this->getResponse()->setHeader('HTTP/1.1','404 Not Found');
            $this->getResponse()->setHeader('Status','404 File not found');
        }
    }
 
    private function _js()
    {
        $file = $this->getRequest()->getParam('f');
        $type = $this->getRequest()->getParam('t');
 
        $helper = Mage::helper('inchoo_sociable');
 
        if (in_array($file, $helper->getAllowedFiles($type))) {
            $this->getResponse()->setHeader('Content-Type', 'application/javascript');
            $this->getResponse()->setBody(file_get_contents($helper->getExtPubDir($helper::DIR_JS).DS.$file));
        } else {
            $this->getResponse()->setHeader('HTTP/1.1','404 Not Found');
            $this->getResponse()->setHeader('Status','404 File not found');
        }
    }
}

Basically that’s all we need. Now if we run the url like http://{{unsecure_base_url}}/index.php/inchoo_sociable/static/get/t/js/f/jquery.js we would get jquery script served. Same goes for url like http://{{unsecure_base_url}}/index.php/inchoo_sociable/static/get/t/image/f/delicious.png which would give us the image.

So now, you can use write HTML like img src=”http://{{unsecure_base_url}}/index.php/inchoo_sociable/static/get/t/image/f/delicious.png” in your extension block that would then pull the image from your extension folder.

Once again, this approach is not “official Magento way” if there is a such thing. Point of this article is to merely show how you can do it if you wish to keep your static content in one extension folder, and not other, folder like /media, /skin, /js, etc. Like shown below on the image.

P.S. I am not actually using jQuery in Magento (would never mix Prototype and jQuery). Image above is merely for demo purpose.

Surely there are better ways to handle this “serve static content files from your Magento extension folder” concept. You be the judge. The important thing is to know what the code does and you might do it differently in a given situation.

Hope someone finds it useful.

Cheers.


12 comments

  1. Is it possible to have a ‘getAction’ in an admin controller? I have tired to put this action in one of my Admin Controllers it just simply redirects back to the Dashboard??

  2. @Andrey Tserkus
    With a proper reverse proxy cache and the correct headers the load should be a non-issue as it is all static content.

    In general if you were to mix this with an auto-detect of http server, you could even enable X-Sendfile (and the NGINX version) to make it faster.

    The load should only be on the initial request and every time the cached version expires. Obviously if you don’t cache properly the load would be quite large.

  3. @Branko: Just in general serving a file through a dynamic script (like PHP) is at least a factor 10 slower than plain files. Whatever Andrey said comes on top of that.

    Also, distributed libraries (like the one of Yahoo or jQueryUI) need additional fixes in order to point to the correct path.

    And finally, mixing dynamic and static urls together makes it harder to serve static files from dedicated servers.

  4. @Branko: jQuery and Prototype can be used together without conflicts (http://docs.jquery.com/Using_jQuery_with_Other_Libraries), but having said that: yes, you have to act with caution. The plan is to use jQuery for Magento 2 and I’m look forward to it.
    Thanks for your series on “unobtrusive Magento extensions”, it’s always interesting to which approaches other developers are choosing. Did you check out modman yet (http://code.google.com/p/module-manager/wiki/Tutorial)? I’d like to give it a try on a production system but didn’t do it yet.

  5. By huge I mean that in order to serve one image you need to run whole Magento instance. And that is – load configs, cache, DB, perform routing and other stuff. Just log MySQL requests needed to serve one image – their number and complexity.

    And then try to imagine just one extension with 10 images, some kind of “Social bookmarks” visible on every page. 1 css and 1 js = 12 additional requests for 1 page view.

    Of course, you make only MC-part of MVC, so I divide load of request by 2 to scale it to full view page request coordinates.

    One more scaling – some browsers and proxies will decide to cache images on their own and don’t make further requests. You also can respond with cache http-headers to reduce requests sent by browsers, but some of them will anyway send requests to check whether image has changed. And some will ignore that headers. So I divide number by 2 once again.

    Ok, we got 3 additional requests that Magento must process on every 1 normal customer request. That effectively decreases your server capacity by 4.

    Yesterday you needed 1 server for your shop, today after installing simple extension working by proposed scheme – you need 4 servers. Very cute.

    That’s why I say, that it’s a very good ‘proof of concept’, but it doesn’t suit to be used in real life.

  6. Branko, I never had problems until now with it, I used it also on frontend and backend. The only requirement is to use a different function name for jQuery’s ‘$’ – this is already used by prototype. I use the following setup $j = jQuery.noConflict(); and where I want to use jQuery I use $j instead of $ (which will remain used by Prototype). More info can be found here:

    http://api.jquery.com/jQuery.noConflict/

    I think that Andrey is referring to the fact that each request for a static content will run through Magento’s core (Magento’s bootstrap through index.php), because basically you are running an action of a controller to serve the requested files. This can increase the server load.

  7. @Dumbrava

    I would like to see jQuery on Magento frontend, if we cannot have it backend as well. Mixing I don’t like because there always seem to be something clashing (could be wrong on this one).

    On the topic of “stripping” Magento, yes I was able to ripped it down to almost just “Core” module (its actually really simple, takes around an hour or so). I gave up on any further playing with it. Just did not see any use. Figured if ever I was to code my own shop, it wont be based on Magento.

  8. @Andrey Please note that I used the “list the dir for files” just for being more “flexible”. Not sure if you thought that to be “huge server load”. I could have easily write the file names in PHP array directly on controller class. Plus add some simple caching on top of it that would actually pull cached auto-generated images from /media then. Not that Magento itself works any differently. I understand your point, but it always makes me thinking when I hear a topic of server loads in Magento. Don’t see what’s so “huge” load here (if we apply stuff just mentioned).

  9. I consider this approach very practical, and I will test it myself. Thanks! I also like the campaign that you are running.

    PS: Why would you never mix Prototype and jQuery? You can use both of them without any problem. I remember that once, you did a clean up of a Magento installation (removed all the modules except some ‘core’ ones) and you were saying there that you were going to eliminate also Prototype. Have you changed your thoughts on this?

  10. Good idea, it makes the ‘proof of concept’.

    But it cannot be used in production, as far as serving static content in such a way will require huge additional server load.

    So when you get the choice “extension file placement simplicity” or “working online shop”, you definitely choose second.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.