Idle thoughts and technical knots.

Setup Laravel on shared hosting

Hosting Laravel

We’ve always specialised in PHP development at Papertank, but for the past 12 months we’ve been exclusively using the excellent Laravel framework for our bespoke web projects.

Laravel is relatively straightforward to get set up, particularly since it utilises  the new PHP dependency manager, Composer. For a quick start guide see the Laravel Docs.

Installing on a shared host is perfectly possible, although we normally use a more powerful VPS server for more flexibility and control. You’ll want to make sure you are using a decent web hosting company. This will save a lot of headache and make deploying custom apps much easier. We recommend South West Broadband in Cornwall for great value and support.

Installing on shared hosting

If you decide to use shared linux hosting, setting things up can be a little more footery, but following these steps should hopefully help fix main two issues:


Laravel only has two main requirements which you’ll need to make sure your shared server meets:

  • PHP >= 5.4
  • MCrypt PHP Extension

Before you go any further however, you’ll also need to check (or ask) if your web server and hosting company also allows:

  • SSH shell access
  • PHP 5.4 command line interface

Public Path

If you’re installing and using Laravel correctly, you’ll be forced to put all the public content of your website (including js, css and img assets) into a /public directory. When using cPanel on a shared linux server, however, you will normally put your files in a public_html directory which is already accessible publicly.

Placing the Laravel files in your public_html directory has two consequences:

  • All of your sensitive files (including temporary files) might be openly accessible through the web browser
  • Your web addresses will be ugly, since they might look like

The easiest way (we think) to fix this is simply:

  1. Install your Laravel app in the directory above public_html inside a new folder of your choosing, e.g. code.
  2. Move the index.php file and .htaccess files from the laravel public folder to your normal public_html directory.
  3. Update the index.php file to reflect the path to your new code folder (or whatever you called it in step 1)

For example, from the normal index.php file (GitHub) we now have the following:

* Laravel - A PHP Framework For Web Artisans
* @package Laravel
* @author Taylor Otwell <>

| Register The Auto Loader
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.

require __DIR__.'/../../code/bootstrap/autoload.php';

| Turn On The Lights
| We need to illuminate PHP development, so let's turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight these users.

$app = require_once __DIR__.'/../../code/bootstrap/start.php';

| Run The Application
| Once we have the application, we can simply call the run method,
| which will execute the request and send the response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have whipped up for them.


SSH in to your server and run something similar to the below (replacing the italics with your values).

  cd /home/username/public_html
  ln -s /home/username/code/public/css css
  ln -s /home/username/code/public/img img


This creates an alias for the css and img folders from your laravel public directory inside the public_html directory.


Still with us? Load up your website in your browser and you’ll no doubt see a blank white page (if error reporting is off), or the following:

Warning: require(/home/username/code/bootstrap/../vendor/autoload.php): failed to open stream: No such file or directory in /home/username/code/bootstrap/autoload.php on line 17
Fatal error: require(): Failed opening required '/home/username/code/bootstrap/../vendor/autoload.php' (include_path='.:/opt/alt/php54/usr/share/pear:/opt/alt/php54/usr/share/php') in /home/username/code/bootstrap/autoload.php on line 17

This means we haven’t installed laravel (and its dependencies) via Composer.

SSH in to your server again and cd into the directory containing your laravel code (not public_html).

You’d normally install composer by running:

   curl -sS | php


But you’ll probably get the error

Some settings on your machine make Composer unable to work properly.
Make sure that you fix the issues listed below and run this script again:

Let’s fix this by creating a custom php.ini file in the same directory with the content from

suhosin.executor.include.whitelist = phar;
disable_functions = none;
memory_limit = 512M;


Now if we run the command again specifying that we want to use this new php.ini file, composer should be installed correctly:

curl -sS | php -c ./php.ini


Now you want to install laravel fully by running the command

php -c ./php.ini composer.phar install


You can periodically run the following command to update the version of the laravel framework and other components.

php -c ./php.ini composer.phar update

Other Resources

If you’re looking for more help with Laravel, you should check out the following websites which we subscribe to and read regularly.

Of course, if you’re looking for a Glasgow based company with experience building advanced web apps with Laravel then please get in touch


NHS Active Staff App

For the past couple of months we’ve been quietly working away on a really exciting project with NHS Scotland. Now that it has officially launched, we can tell you all about it

Each year NHS Greater Glasgow & Clyde and Glasgow City Council hosts a staff walking challenge (Active Staff) during which participants work in a small team and count their individual daily steps taken. The team’s steps are totalled and added towards a distance target. Up until now, this has been done manually with thousands of emails, spreadsheets and manic step-counting.

But the Active Staff challenge is walking towards a bright new future with the launch of our website, iPhone and Android apps to help make this monumental task a lot easier – and fun!

We took the existing World Walking ( system and app and started to work out how we could develop this into a standalone solution for both the NHS and Glasgow City Council staff. With enthusiastic responses for our initial concepts from the client, we set to work on planning, designing and building the apps and website, working with our resident mobile developers and friends, Michael Park and Fergus Howe.

This year’s ‘virtual’ route allows teams of 5 to race the Queen’s Baton Relay around the Scottish leg of the tour, starting in Edinburgh and ending in Glasgow at Celtic Park on 20th July 2014. Participants can log in to the mobile app or website to add their distance in steps, kilometers or miles as well as tracking their walk accurately using the app’s GPS tracker feature. Teams compete for 1st place on the leaderboard, plus individuals can gain achievements for their activity and distance.

Over 4,100 staff users have signed up for the 2014 challenge using our new apps and we look forward to seeing the results and feedback in the next few weeks. Let the challenge begin!

Note: Registration is limited to NHS and Glasgow City Council staff only – a valid payroll number is required.


How to fix: Codeigniter not adding products to the cart

Codeigniter is a great php framework, and the one of choice at Papertank. As with any framework code though, sometimes things just don’t work the way you want them to…

Hopefully this post will help any other programmers who come across the same issue – Codeigniter not adding particular products to the shopping cart. In my experience, this is caused by one of two reasons:

Problem A

The Codeigniter Shopping Cart library uses sessions to save your basket, and by adding lots of products or products with various options, this session can get exhausted pretty quickly.


The first thing to try, if you’ve not already, is to try switching to using your web app’s database for session storage. As well as allowing more information per session, this can increase security.

Before you go changing the config.php file, you’ll want to set up the right table in your database as so:

	session_id varchar(40) DEFAULT '0' NOT NULL,
	ip_address varchar(45) DEFAULT '0' NOT NULL,
	user_agent varchar(120) DEFAULT NULL,
	last_activity int(10) unsigned DEFAULT 0 NOT NULL,
	user_data text DEFAULT NULL,
	PRIMARY KEY (session_id),
	KEY `last_activity_idx` (`last_activity`)

Now, you’ll want to update the config.php file, usually found in application/config/config.php so that the session library uses the database option. Also, just incase you decided to use a different name for the database table, you can chance this here too.

$config['sess_use_database'] = TRUE;

Problem B

If that didn’t fix your problem, check your error log for more information. For me, there was an issue with product names having invalid characters – in my case a ‘.’

ERROR - 2012-06-26 09:51:27 --> An invalid name was submitted as the product name: Urinal Blocks 1.1Kg The name can only contain alpha-numeric characters, dashes, underscores, colons, and spaces


To fix this problem, you’ll need to overwrite the default Shopping Cart class – CI_Cart – so that it no longer rejects particular names. In your application/libraries folder, add a file called MY_Cart.php with the following code, or download from

<?php if (!defined('BASEPATH')) exit('No direct access allowed.');

class MY_Cart extends CI_Cart {
  public $CI;

  function __construct()

    $this->CI =& get_instance();

	function format_number($n = '')
		if ($n == '')
			return '';
		// Remove anything that isn't a number or decimal point.
		$n = trim(preg_replace('/([^0-9.])/i', '', $n));
		return number_format($n, 2, '.', '');
	 * Insert
	 * @access	private
	 * @param	array
	 * @return	bool
	function _insert($items = array())
		// Was any cart data passed? No? Bah...
		if ( ! is_array($items) OR count($items) == 0)
			log_message('error', 'The insert method must be passed an array containing data.');
			return FALSE;

		// --------------------------------------------------------------------

		// Does the $items array contain an id, quantity, price, and name?  These are required
		if ( ! isset($items['id']) OR ! isset($items['qty']) OR ! isset($items['price']) OR ! isset($items['name']))
			log_message('error', 'The cart array must contain a product ID, quantity, price, and name.');
			return FALSE;

		// --------------------------------------------------------------------

		// Prep the quantity. It can only be a number.  Duh...
		$items['qty'] = trim(preg_replace('/([^0-9])/i', '', $items['qty']));
		// Trim any leading zeros
		$items['qty'] = trim(preg_replace('/(^[0]+)/i', '', $items['qty']));

		// If the quantity is zero or blank there's nothing for us to do
		if ( ! is_numeric($items['qty']) OR $items['qty'] == 0)
			return FALSE;

		// --------------------------------------------------------------------

		// Validate the product ID. It can only be alpha-numeric, dashes, underscores or periods
		// Not totally sure we should impose this rule, but it seems prudent to standardize IDs.
		// Note: These can be user-specified by setting the $this->product_id_rules variable.
		if ( ! preg_match("/^[".$this->product_id_rules."]+$/i", $items['id']))
			log_message('error', 'Invalid product ID.  The product ID can only contain alpha-numeric characters, dashes, and underscores');
			return FALSE;

		// --------------------------------------------------------------------

		// Validate the product name. It can only be alpha-numeric, dashes, underscores, colons or periods.
		// Note: These can be user-specified by setting the $this->product_name_rules variable.
if ( ! preg_match("/^[".$this->product_name_rules."]+$/i", $items['name']))
			log_message('error', 'An invalid name was submitted as the product name: '.$items['name'].' The name can only contain alpha-numeric characters, dashes, underscores, colons, and spaces');
			return FALSE;

		// --------------------------------------------------------------------

		// Prep the price.  Remove anything that isn't a number or decimal point.
		$items['price'] = trim(preg_replace('/([^0-9.])/i', '', $items['price']));
		// Trim any leading zeros
		$items['price'] = trim(preg_replace('/(^[0]+)/i', '', $items['price']));

		// Is the price a valid number?
		if ( ! is_numeric($items['price']))
			log_message('error', 'An invalid price was submitted for product ID: '.$items['id']);
			return FALSE;

		// --------------------------------------------------------------------

		// We now need to create a unique identifier for the item being inserted into the cart.
		// Every time something is added to the cart it is stored in the master cart array.
		// Each row in the cart array, however, must have a unique index that identifies not only
		// a particular product, but makes it possible to store identical products with different options.
		// For example, what if someone buys two identical t-shirts (same product ID), but in
		// different sizes?  The product ID (and other attributes, like the name) will be identical for
		// both sizes because it's the same shirt. The only difference will be the size.
		// Internally, we need to treat identical submissions, but with different options, as a unique product.
		// Our solution is to convert the options array to a string and MD5 it along with the product ID.
		// This becomes the unique "row ID"
		if (isset($items['options']) AND count($items['options']) > 0)
			$rowid = md5($items['id'].implode('', $items['options']));
			// No options were submitted so we simply MD5 the product ID.
			// Technically, we don't need to MD5 the ID in this case, but it makes
			// sense to standardize the format of array indexes for both conditions
			$rowid = md5($items['id']);

		// --------------------------------------------------------------------

		// Now that we have our unique "row ID", we'll add our cart items to the master array

		// let's unset this first, just to make sure our index contains only the data from this submission

		// Create a new index with our new row ID
		$this->_cart_contents[$rowid]['rowid'] = $rowid;

		// And add the new items to the cart array
		foreach ($items as $key => $val)
			$this->_cart_contents[$rowid][$key] = $val;

		// Woot!
		return TRUE;

If you look closely, you’ll notice what we’ve done here is actually comment out the specific section that validates the product name. Be careful with this, of course, but when you re-try adding your product, hopefully your issue should be solved.