Running multiple sites with CakePHP 3 using plugins

I had an interesting concept for a business that required running multiple sites from within the same vertical. I also wanted the ability for the core backend to span into other verticals. To do this I needed a highly scaleable and flexible core. The premise was the core code would just serve as light-weight CRM, with additional functionality being built as plugins (including the front-ends). Sites would then import these plugins and be configured via some JSON settings. This blog will focus on just running multiple sites off the CakePHP 3 framework, but if interest is expressed I’ll go into further detail.

Site Plugins

Let’s say you want to run two sites on the same install, but you might want some different functionality between the two. We’ll call these two sites Widgets and Gadgets. The first thing we’d do is create two plugins SiteWidgets and SiteGadget. Since these are plugins they can have their own independent MVC, but still core application functionality and other plugins. Now with native functionality you can’t point the Apache document root at these plugins without some hefty hacking. In retrospect I would’ve have done this a bit different and will likely refactor, but the premise remains.

In Short

  1. We add some code to webroot/index.php which modifies the REQUEST_URI global and sets it to the site plugins path.
  2. Cake proceeds as normal going next to config/bootstrap.php where it loads the actual site plugin.
  3. Next that plugins boostrap file at plugins/SiteWidgets/config/bootstrap.php is called where we load that sites specific plugins.
  4. Cake then loads in routes for the site plugin at plugins/SiteWidgets/config/routes.php.
  5. Finally plugins/SiteWidgets/src/controller/AppController.php is called where we instruct cake to use the proper layout.
  6. This inherits from src/controller/AppSitePluginController.php where we load in site specific settings which of course inherits from the main App Controller where some additional logic is performed.
cakephp3-multiple-sites

Diagram

Here’s the code and a deeper explanation.

Modifying Document Root

In our applications webroot/index.php we’ll want to add our initial logic for determining which site plugin was requested. I did the following just before the DispatcherFactory goes to work:

Site Logic

Initialize Request

We call initRequest with the NetworkRequest as a dependency using that to determine what the visitor requested. After Ignoring any debug kit requests as we want the plugin to continue to function normally it does a few things. The method performs a lookup (by calling $this->getDomain) in our sites table to get necessary information and then overwrites $_SERVER[‘REQUEST_URI’] to “fool” cake.

At this point cake proceeds normally and will now look in config/bootstrap.php and config/routes.php. Because we don’t want to constantly update boostrap.php We’ll now call the bootstrapSites method of our Sites object. Inside boostrap.php something like this will do:

We put this code after the DispatchFactory calls and before loading other application wide plugins such as Bake. The bootstrapSites method will divide requests between main application requests and our plugin sites. You can see this is where we actually load the site as a plugin. This avoids loading every single site in bootstrap.php allowing the application to be dynamic. At this point Cake has been told the plugin is the base site, but we still have a few more pieces to put in place.

App Controllers

In our app controller we will want to add a beforeFilter to determine whether a request to the controller is for a site plugin or the core application. We’ll also need to some things to ensure CSRF protection can still work.

You can also see I’ve set some public variables on the app controller such as isSite and isBackend to differentiate between the core app and a site plugin. You can ignore some of that stuff you don’t plan on using CSRF protection. We’ll need another app controller specifically for our site plugins that extends the main app controller above.

In here we are loading additional settings for the site and informing cake to use the plugins layout. Now let’s put this together inside our plugins app controller at plugins/SiteWidgets/Controller/AppController.php:

Configure plugins/SiteWidgets/config/bootstrap.php to bootstrap this sites plugins.

Configure  plugins/SiteWidgets/config/routes.php:

This took a bit of thinking through and hacking to get working, but ultimately it came out pretty clean and I never had to touch the core CakePHP3 code. Had I had to modify core internals I would have abandoned this. This solution is pretty impervious to cake upgrades, though I’ve had to upgrade a few things here and there as items were deprecated from 3.0 to 3.1. There is a lot here, some of which I didn’t explain as it didn’t relate specifically to running multiple sites as plugins. Feel free to ask questions and refer to the “In Short” section if you get loss as that is the basic thought process I went by when building this.

IF you must

I have a lot of gripes in life. Most of them relate to driving. Thankfully as a mostly work from home developer I only need to drive into the office once a week. During that drive to Park City I consistently contend with slow drivers in the passing lane, which annoys me almost as much as a people who think using their turn signal is optional. This post isn’t about driving though, its about a bigger gripe: people who do IF statements wrong. Let’s look at the common types of badly written if statements that make me want to fire developers.

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
– Martin Fowler, 2008.

The Linophobic Programmer

This level of insanity exists. I once saw 30 lines of crap like this. For one, you’re doing it wrong if you need that many IF statements, try using a switch statement, its kinda awesome. Regardless my eyes didn’t quite bleed, but I did tense up and feel “the rage” within. No one is giving you a gold medal for reducing the amount of lines in your code by failing to use brackets. PHP is C-style language, use brackets and make your code more readable for the love of god. It’s times like this where I wish I developed in Python since you’d get a parse error with shit like this. Is this really so bad:

The Horizontal Fan Boy

Because we all love scrolling horizontally right? I mean that’s how we wrote papers in school right? Thats how we all love reading web pages right? With fucking horizontal scroll bars, right?!?! There comes a time when you have to take a deep breathe and just break this into multiple lines….but not like this:

The Drunken If

Oh thanks for not forcing me to use my horizontal scroll bar, but I still can’t read your shitty code.  I swear if I ever saw this code given to me in an interview setting I would end the interview real quick. I don’t care if this shit could pass a Turing Test, you just failed a readability test big time.

Readability is important

The code listed above is actually a form of technical debt. When a new developer comes in it takes him/her that much longer to figure out what’s going. Hell it probably takes the original developer twice as long to figure it out. There is nothing wrong with nested if statements when used to improve readability. Example:

Now sure I created more lines of code, but any developer can come in and understand and if its still a little to complex guess what?

Oh wow, we’ll broken out code is easier to add comments too! What a compelling reason to write readable code. And please spare me your pleas for performance. Increasing the amount of IF statements for readability is unnoticeable to the human eye and likely doesn’t add even 1 cycle to a processor. Spend a few minutes verifying proper indexes within your databases or tackling one inefficient query if you are actually so concerned with performance.

Perhaps you thought that “getting it working” was the first order of business for a professional developer. I hope by now, however, that this book has disabused you of that idea. The functionality that you create today has a good chance of changing in the next release, but the readability of your code will have a profound effect on all the changes that will ever be made.
– Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship

 

Poor appendChild and removeChild Performance on Internet Explorer

Recently we noticed that conversion rates on Internet Explorer are quite low. Internet Explorer was converting at just .30% compared to 1.5% for Safari, Chrome, and mobile. I’ve blogged before on how Google Analytics can assist developers in finding bugs and once again GA had saved the day or at least pointed to an issue. Unfortunately this bug had persisted for weeks and since I just assumed browser stack was being slow, it was never addressed. It doesn’t help that no one in our office uses Internet Explorer (even Windows users are on Chrome) and only 13% of our traffic comes from Internet Explorer. Worse, its widely known Internet Explorer lags far behind Chrome and Safari in DOM performance.

Offending code:

This code is basically doing a sort based on some HTML attributes. It uses jQuery detach to remove the element while keeping its events and then appends it back to the DOM. In hindsight this application should have been built using a JavaScript framework built for these types of manipulations such as Angular, but that level of rewrite is clearly beyond the scope of this fix. Chrome’s speed hid the bad design here from me.

Resolution:

Here looping over expensive append child and remove child operations is avoided. The performance benefit in Chrome was rather negligible in regards to the scope of this application dropping appends from 92 milliseconds to 26 milliseconds and removeChild from 87 milliseconds to 66 milliseconds. However, in Internet Explorer jaw dropping improvements were achieved. Append child improved all the way from 18.5 seconds (yes seconds) to 1.8 seconds and remove child improved from 9.6 seconds down to .6 seconds.

Hopefully this helps someone. Honestly I take little pride in this resolution. Its a black eye that this was allowed to persist for so long. Furthermore the new numbers still suck and additional work will be necessary to bring Internet Explorer performance in line with Chrome, Safari, and mobile. One puzzling note, a Samsung Galaxy S II which was released in 2011 sporting a 1.2 GHz dual-core ARM with 1 GB of RAM running the application on Android/Chrome bested my a one-year old Laptop sporting an Intel Core i7 4710HQ (2.50 GHz) with 16 GB of RAM and a solid state drive running the application in Windows 8/Internet Explorer. The numbers between the SII and Laptop were similar pre-fix.

Safari Doesn’t Care About Your JavaScript On Submit

Recently I was implementing some new code to show a user that an order was processing. Pretty standard stuff with overlays, hiding the submit button etc… While going through routine testing in various browsers I noticed that the implementation was not working in Safari. Oddly it turns out that when you submit a form via JavaScript in Safari it decides to pretty much not execute anything else. A stackoverflow thread touched up on this a bit, along with a potential solution.

Here we have a sample of the code that doesn’t work as expected in Safari:

The solution is to use setTimeout as a poor mans queue.

I can’t understand why Safari operates differently in this respect. All other major browsers work as expected including Internet Explorer. With some other odd behavior I’ve seen out of Safari lately, I think its quickly becoming the new IE6.

Reorder DOM Elements Without Removing and Replacing

I had an interesting problem recently that required I re-order some DOM elements without doing a remove and replace. The existing implementation had a bug involving some filtering based on html attributes and I wanted to avoid rewriting some legacy code. Typically you would do something like this:

The problem is you are replacing everything including any events you have binded to those elements. The solution comes courtesy of this blog post and utilizes the jQuery detach method. Per the jQuery manual:

The .detach() method is the same as .remove(), except that .detach() keeps all jQuery data associated with the removed elements. This method is useful when removed elements are to be reinserted into the DOM at a later time.

Our initial example becomes this:

This handy solution easily replaces what you might be more familiar with.