Chris Hope's LAMP Blog - The Electric ToolboxChris Hope's LAMP Blog - The Electric Toolbox

Linux Apache MySQL and PHP articles by Chris Hope

This is Chris Hope's blog for Linux, Apache, MySQL and PHP (known as LAMP) and Javascript/jQuery. I started this website several years ago with articles about web programming, Linux and Windows tips and tricks, howtos etc.

The ten most recent articles can be found below in their entirety. Navigating the sections in the right navigation (under Categories) will bring up all the other posts, and you can also use the search box at the top of the page to find what you might be looking for.

Feedly icon appearing over web pagesFeedly icon appearing over web pages

Posted September 1st, 2014 in Applications

When Google announced the death of Google Reader, the RSS reading world threw their hands up in collective horror. And then we discovered there were better alternatives, such as Feedly. Feedly has a Chrome plugin, which from yesterday/today has "Feedly Mini" which appears on the bottom right of all pages. I find this somewhat annoying, but it's very easy to disable.

tl;dr

  • Go to Chrome's options.
  • Select "Extensions" from the left nav
  • Uncheck the "enabled" checkbox next to the feedly plugin to completely disable the plugin OR
  • Click "Options" under the feedly plugin THEN
  • Uncheck the "Show feedly mini icon on the bottom right of each page" option
  • You will need to reload pages for the icon to go away

More information

There's really not much more to it than the above, other than to send you in the direction of the blog post annoucing that "Feedly Mini is Back" which also covers some of the things it does.

I had forgotten that I'd installed a Feedly plugin, so was somewhat surprised when Chrome suddenly started showing a Feedly icon bottom right on all pages and though it was some sort of browser bug. Restarting didn't solve the problem, and Google quickly found me the above blog post which steered me in the right direction.

Note as per the Feedly blog post, should you wish to use it, Feedly Mini is currently only available for Chrome but Firefox and Safari are in the pipeline, as is support for https pages.

Temporarily changing the auto increment increment in MySQLTemporarily changing the auto increment increment in MySQL

Posted August 29th, 2014 in MySql

It's possible to temporarily change MySQL's auto increment increment value to something other than the system setting. This might be useful if (like me) you have two MySQL databases set up in a master-master relationship with an increment of 2, but need to increment a table in 1s.

tl;dr

"SELECT @@auto_increment_increment" to see what the current value is.

"SET @@auto_increment_increment = X" where X is the number to increment to set it.

It will affect all auto increment values on all tables on all databases for the duration of your connection; it will not affect other connections.

Longer answer / case study

By default the auto increment value on a field in MySQL is 1, but it can easily be set to something else with the auto_increment_increment setting.

I run a web server with a backup server set up identically; both have MySQL servers running on them, running in a master-master replication relationship. The auto_increment_increment value of each is 2; the auto_increment_offset of the primary server is 1 and the backup 2. This means that inserts into the primary server will be 1, 3, 5, 7 and so on; and on the backup server 2, 4, 6, 8 and so on.

On one particular website, there is an orders table in the database for e-commerce orders, which needs to have the primary key on the table increment in 1s, not 2s as would happen by default with my set up.

The simplest way I found to do this is to keep a record of what the current increment value is, change it to 1, and the restore it back to what it was previously.

To get the current value:
SELECT @@auto_increment_increment

To set the value, to e.g. 1 & 5:
SET @@auto_increment_increment = 1
SET @@auto_increment_increment = 5

A pseudo code example of changing the value before saving to the orders table, and then restoring the value afterwards, is as follows:

$previousIncrement = selectOneValue("SELECT @@auto_increment_increment");
runQuery("SET @@auto_increment_increment = 2");
runQuery("INSERT INTO table (...) VALUES (...)");
runQuery("SET @@auto_increment_increment = $previousIncrement");

Blog UpdateBlog Update

Posted August 27th, 2014 in Miscellaneous Postings

I started this blog back in 2003 and it took a few years to get visitor numbers up to a decent level. For a variety of reasons, traffic started slumping, levelling out and now it's slumping again so it's time for me to dust of the blogging keyboard and get back to more regular posting.

Visitor analytics August 2008 to August 2014

Here's a screenshot from Google Analytics, showing the rise and fall of visitor numbers on a weekly basis from 24 August 2008 to 23 August 2014.

google analytics from august 2008 to 2014

As you can see there was steady growth in traffic until the Google Panda algorithm change hit in February 2011, which caused a drop of about a third. Things then levelled out and started to grow again at the start of 2012, level out, and then the gradual decline started from 2013 through to now.

The Penguin updates from 2012/13 don't appear to have had an affect; a distinct lack of posting new content is likely to be the culprit for the long term gradual decline, along with a manual action penalty I got hit with which took a few months to shake off.

(The regular big spikes down you see every now and then in the graph above are the Christmas/New Year holiday season, and typically bounce back in early January.)

Lack of blog posts the cause?

An approximate number of blog posts made each year from 2007 is as follows:

2007 - 100
2008 - 350
2009 - 400
2010 - 230
2011 - 100
2012 - 55
2013 - 12
2014 - 23 (to date)

We all know that Google likes fresh and new content, and after a good push from 2008 to 2010 I didn't tend to post so much, as I was busy getting my New Zealand Running Calender and then Australian Running Calendar websites going.

The fact that my traffic kept growing / staying steady without me writing anything new also meant I didn't tend to bother writing new content so much. I could always "do it tomorrow", but as we all know, "tomorrow never comes".

Manual action and shady advertising

I got hit with a "manual action" by Google in May 2013 due to "unnatural links detected". It took four reconsideration requests and until August to have the manual spam action removed; there's an associated decline in traffic from May to August 2013 which then levels off after the action was removed.

Was I guilty of unnatural links? Yes. For years I had been selling text links both directly and indirectly. The direct ones were put into existing content (often modifying the text to suit the advertiser) and the indirect ones via a 3rd party on selected pages in the right hand navigation.

Interestingly, I'd pretty much removed all the directly sold ones as the contracts expired and there were only a few left on the site, and had been considering removing the ones in the right sidebar for a while when I got hit with the action.

I removed those immediately and eventually removed the other links too, which were to my own websites and popular posts on this one. I then uncovered another couple of paid links in my content, removed those and finally the manual action was removed.

Lack of current best practises

I launched the current version of this website in (I think) early 2008 and it really hasn't had any work done to it since then, other than new content being written.

There's no XML sitemap; it's not responsive; has no icons for mobile devices (not that it gets any mobile traffic, but still...); uses an old "XHTML 1.0 Transitional" doctype; and it's just looking a bit dated, amongst other things.

So now what?

If I'd spent a fraction of the time I spend working on this site as I have with my Australian running website over the past three years, there'd be stacks of content and useful tutorials. And yet I've spent virtually none on this, and thousands of hours on the other.

A lot of it comes down to time and money, and this website has made me decent amounts of money in the past (I don't just do it for love). I can't really afford for the income I make from this website to dry up, so as I noted at the start of this post, it's time to dust of the blogging keyboard and get back to work on this site.

My plan is to start writing new content again, reviewing existing content to get the old stuff up to scratch, and delete content that's no longer relevant or even wrong. Start doing that, and then recode the template for the website to be more modern and follow current best practises.

Then, over time, measure and compare and see what improves. We all know that Google likes fresh and new content (and as I've found out with my running websites, updated existing content), so it's time for me to see if I can prove it here.

Further reading

For years I've been reading elated.com, an excellent blog with very similar content to that found on this one. They got hit by Panda badly like I did, then even worse by Penguin, and kind of lost interest in posting new content. Read the sad tale in the post "The Future of Elated". I really feel for those guys and hope they too will one day start posting again.

Flaxmill Bay website wins NZ Tourism Guide's weekly awardFlaxmill Bay website wins NZ Tourism Guide's weekly award

Posted July 28th, 2014 in Case Studies

We recently re-launched the Flaxmill Bay website with a fully responsive, beautiful design. And today we discovered it had won New Zealand Tourism Guide's weekly award!

They noted (my emphasis added):

Flaxmill is located in Mercury Bay on the east coast of the Coromandel Peninsula, and is the perfect place unwind, relax and recharge. Their website features responsive design at its very best! Add to that some stunning imagery, ease of navigation, website design and layout, this makes for one of the best winners in recent time.

Nice!

Check out the Flaxmill Bay website, New Zealand Tourism Guide website and award website, and the Reserve Group who did the design work in Photoshop.

.gitignore to ignore vendor directory but include composer.gitignore to ignore vendor directory but include composer

Posted May 24th, 2014 in Laravel PHP Framework and PHP

This post shows how to exclude the vendor directory but include the vendor/composer directory when using a PHP project e.g. Laravel. I'm not saying you should do this; this post is more as a future reference in case I decide I need to do this in the future.

.gitignore file

After a bit of fiddling around I got it to work with these two lines. The first excludes everything under vendor and the second makes an exception for vendor/composer and allows it to be included in git.

/vendor/*
!/vendor/composer/

Again, just because I've posted this here doesn't mean I'm suggesting you should do this. Once I've spent more time with Laravel and deploying projects I'll make my decision. I've simply posted this as a reference for myself for the future.

502 Bad Gateway error after upgrading Nginx and/or PHP502 Bad Gateway error after upgrading Nginx and/or PHP

Posted May 23rd, 2014 in Nginx Web Server and PHP

After doing an upgrade on my Debian virtual server, which upgraded PHP and Nginx, I got a "502 Bad Gateway" error when browsing websites on that server. This post shows how to fix this problem, and the configuration option to prevent it occurring again on reboot. 

tl;dr

Edit /etc/php5/fpm/pool.d/www.conf and uncomment the following:
    listen.owner = www-data
    listen.group = www-data
    listen.mode = 0660

Then run:
    sudo service php5-fpm restart

Longer answer

As well as the error in the browser, I was getting this error in the Nginx error log: 

[crit] 2686#0: *1 connect() to unix:/var/run/php5-fpm.sock failed (13: Permission denied) while connecting to upstream, client: 192.168.50.1, server: [...], request: "GET / HTTP/1.1", upstream: "fastcgi://unix:/var/run/php5-fpm.sock:", host: "[...]"

The problem is caused by the permissions and ownership of the /var/run/php5-fpm.sock file, which after I'd done the upgrade change to something like root:root and 0660, so it couldn't be accessed by the www-data user which Nginx was running as.

The immediate solution is to change the permissions and/or ownership of the file like so:

chmod 0666 /var/run/php5-fpm.sock

OR

chmod 0660 /var/run/php5-fpm.sock
chown www-data:www-data /var/run/php5-fpm.sock

The only catch is this won't persist after the server is restarted. To prevent the issue from occurring again, edit the /etc/php5/fpm/pool.d/www.conf file:

sudo nano /etc/php5/fpm/pool.d/www.conf

Locate the following lines and uncomment them:

listen.owner = www-data
listen.group = www-data
listen.mode = 0660

If you already changed the ownership/permissions of the socket file as shown above, then you don't need to do anything else now. If you didn't, then run this: 

sudo service php5-fpm restart

This re-creates the socket file with the ownership and permissions as configured in the file.

Debian on VMWare Fusion Mac host shared files issue with PHPDebian on VMWare Fusion Mac host shared files issue with PHP

Posted May 22nd, 2014 in Linux/Unix/BSD, OSX, PHP and VMWare

I was getting PHP parse errors after updating files using composer or Laravel's artisan on a VMWare Fusion 6.0.2 Debian Linux guest on a Mac host. The files were shared using VMWare Fusion's sharing settings and mounted on the guest.

tl;dr

Upgrade to the latest version of VMWare Fusion (6.0.3 at the time of this post) and re-install VMWare Tools. You may possibly need to apt-get update & apt-get upgrade. YMMV.

A little more detail about the problem

I was running:
- VMWare Fusion 6.0.2
- Debian 7 Wheezy
- Mac OSX 10.9.3

The files being updated were on the Mac host, but mounted on the Debian guest using VMWare Fusion's sharing settings.

Every time I'd update composer, or run e.g. "php artisan dump-autoload" I'd end up with PHP parse errors, such as 'PHP Parse error:  syntax error, unexpected '' => $vendorDir . '/laravel/fr' (T_ENCAPSED_AND_WHITESPACE) in [...] /vendor/composer/autoload_classmap.php on line 1952'

Running the same artisan or composer command from Terminal on the Mac would work fine.

Re-saving the file on the Mac would fix the problem.

Restarting the guest would fix the problem.

Unmounting the host filesystem and remounting it would fix the problem.

None of these solutions is any good.

The fix that worked for me

Upgrading to the latest version of VMWare Fusion (6.0.3 at the time of writing) and re-installing VMWare Tools fixed the problem.

I also do a "apt-get update & apt-get upgrade" to get Debian up to date, which I did inbetween updating Fusion and re-installing Tools. Whether you need to do this or not, I am not sure.

The only catch with this "fix" is that the probem may come back again in later updates to Fusion, and of course it may not work for you. Good luck!

Laravel Nginx ConfigLaravel Nginx Config

Posted May 17th, 2014 in Laravel PHP Framework, Nginx Web Server and PHP

I've just started learning the Laravel PHP framework and it comes with an .htaccess file for working with Apache, but I use Nginx these days, so obviously the .htaccess file doesn't work. Here's what I used in the Nginx config for Laravel.

Nginx Laravel config

Replace your.domain.name with your domain name, and /path/to/laravel/public to the public directory under your Laravel project:

server {

    server_name your.domain.name;
    root /path/to/laravel/public;
    index index.php;
    include includes/php.conf;
    location / {
        try_files $uri $uri/ /index.php$is_args$
    }
    
}

I have a php.conf file which I use in Nginx (included in the above config) to have the PHP settings used across all my websites. This contains the following:

fastcgi_pass unix:/var/run/php5-fpm.sock;

fastcgi_param	QUERY_STRING		$query_string;
fastcgi_param	REQUEST_METHOD		$request_method;
fastcgi_param	CONTENT_TYPE		$content_type;
fastcgi_param	CONTENT_LENGTH		$content_length;

fastcgi_param	SCRIPT_FILENAME		$request_filename;
fastcgi_param	SCRIPT_NAME		$fastcgi_script_name;
fastcgi_param	REQUEST_URI		$request_uri;
fastcgi_param	DOCUMENT_URI		$document_uri;
fastcgi_param	DOCUMENT_ROOT		$document_root;
fastcgi_param	SERVER_PROTOCOL		$server_protocol;

fastcgi_param	GATEWAY_INTERFACE	CGI/1.1;
fastcgi_param	SERVER_SOFTWARE		nginx/$nginx_version;

fastcgi_param	REMOTE_ADDR		$remote_addr;
fastcgi_param	REMOTE_PORT		$remote_port;
fastcgi_param	SERVER_ADDR		$server_addr;
fastcgi_param	SERVER_PORT		$server_port;
fastcgi_param	SERVER_NAME		$server_name;

fastcgi_param	HTTPS			$https if_not_empty;

fastcgi_buffer_size         128k;
fastcgi_buffers           4 256k;
fastcgi_busy_buffers_size   256k;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param	REDIRECT_STATUS		200;

fastcgi_split_path_info ^(.+\.php)(.*)$;

If there's anything else you think needs to into the config (either the server config or the include file), please add it using the comments below.

Make multiple displays on Mavericks work like previous versions of OS XMake multiple displays on Mavericks work like previous versions of OS X

Posted May 15th, 2014 in OSX (Updated June 20th, 2014)

OS X Mavericks introduced having the menu bar, dock and alt-tab app switcher displaying on multiple displays. I was having an odd frustration where whenever I switched to Google Chrome and used it on my MacBook Pro's display, TextWrangler would pop over the top of every app including Chrome in the external monitor. It's possible to restore the display behaviour to how it was before in the previous version of OS X as shown in this post.

tl;dr

System Preferences -> Mission Control -> uncheck "Displays have separate spaces" -> logout and log back in again.

This solved my issue with TextWrangler popping over the top all the time, but after a week I reverted back again because I preferred the way Mavericks works by default. Since reverting back, and after 8 hours of work, TextWrangler hasn't once popped back over the top of other apps. Go figure...

Apple information about Spaces

Just before we get into it, here's the information about Spaces from the Apple website here:

App windows are assigned to a single space. You can use Mission Control to see all of the Spaces assigned to each display and to move them. To move a Space, drag it from one display to the other.

Windows in Mavericks reside in a single space by default, so they usually don't span multiple displays. When a window is dragged between displays, it appears translucent on one of the displays to indicate this. After you are done dragging the window, it snaps to one display.

If you need an app window to span multiple displays, deselect the option "Displays have separate Spaces" in the Mission Control pane of System Preferences.

Longer answer with screenshots

Go into the System Preferences app and click the "Mission Control" option as highlighted in the screenshot below. You don't need to type "mission" or "mission control" into the search box, I just used it in the screenshot to nicely highlight the icon :)

select mission control in system preferences

In the Mission Control settings, there's an option labelled "Displays have separate Spaces" as highlighted with the red box in the screenshot below. Uncheck this option.

unheck the displays have separate spaces option

You'll now see that it says "requires log out" next to the label. So log out and then back in again. 

and now you need to log out

Displays will now work like in previous versions of OS X; some of the things include:

  • app window "glow" appears on the other screen when the app is highlighted
  • there's only one menu on the primary display
  • there's a single dock on the primary display
  • apps can span multiple displays instead of being locked/snapped into one

And finally

As noted at the top of this post, I did this myself because of an issue with TextWrangler always popping on top of other apps when I'd tab into Chrome on the MBP.

Switching off displays having separate spaces fixed this issue, but I found it annoying not having the menu bar on both screens, etc having gotten used to it.

So I switched it back on this morning. 8 hours have passed and I've been working on my MBP for most of the day, and not once has TextWrangler done this again. Fingers crossed it doesn't ever do it again!

Update May 15th 2014: I've found every now and then TextWrangler starts popping up on top again when changing to the app on the MBP display; oddly enough quitting TextWrangler and starting again stops this from happening. Not really what I'd call a solution, but good enough for now.

Using the Pandora API with PHPUsing the Pandora API with PHP

Posted May 13th, 2014 in PHP

Pandora is a music streaming service which can be accessed via a web browser, smartphone/tablet app or desktop app. There is a closed API which can be used to extract some information from Pandora (and edit radio stations etc). This post looks at using a PHP API by Alex Dallaway to access radio stations and bookmarks from Pandora.

tl;dr

Download the code here and try it out. Note that I test this sort of stuff using the PHP CLI and not in a web browser, so all the line breaks are \n and there's no HTML formatting.

Disclaimer

As far as I am aware, the Pandora API is closed and not documented officially. It is therefore subject to change at any time and the code on this page may not work in the future. The PHP API can be downloaded from GitHub, and there's some 3rd party documentation of the API here.

Motivation for doing this

I wanted to be able to export my list of thumbed up tracks from Pandora with PHP. Unfortunately there doesn't appear to be a (known) API call for doing this so my efforts didn't achieve much. However, I thought I should post the code examples here for other people who would like to have a go as well.

There are a variety of in browser methods to extract the list of likes, including browser plugins such as this one which will turn Pandora likes into Spotify playlists. I haven't tried it myself yet so don't know how well it works.

Example - logging in

Set the correct path to the Pandora.php API library in the require_once() statement and set up $username and $password variables with your Pandora username and password.

I set up the handleError() function to provide a simple way to report errors after each API call, but you could just as easily subclass the Pandora class and handle it another way, or take another approach.

require_once('path/to/Pandora.php');

use php_pandora_api\Pandora;

$p = new Pandora();

// login to Pandora
if (!$p->login($username, $password)) {
    handleError($p);
}

function handleError(Pandora $p)
{
    echo "Error message: $p->last_error\n";
    echo "Last request: ";
    print_r(json_decode($p->last_request_data));
    echo "Last response: ";
    print_r(json_decode($p->last_response_data));
    exit;
}

Example - get the station list

Call the 'user.getStationList' method to get the list of stations. The easiest way to get a complete list of the data returned from each call is to use print_r() and then structure your output based on what's returned. The example below echoes out the station token and station name; the token is used in other API calls.

// get the station list and show the token and station name
if (!$response = $p->makeRequest('user.getStationList')) {
    handleError($p);
}
// print_r($response); // use print_r() to get the full dataset returned to see what's available
echo "Station list:\n";
foreach($response['stations'] as $station) {
    echo "  $station[stationToken] - $station[stationName]\n";
} 

Using print_r, a single station's output looks something like this (I've replaced some of the values that I don't want to share with ...):

[1] => Array
    (
        [isQuickMix] => 
        [stationId] => ...
        [stationDetailUrl] => ...
        [genre] => Array
            (
                [0] => Jazz
                [1] => Holiday
            )

        [isShared] => 
        [dateCreated] => Array
            (
                [date] => 19
                [day] => 4
                [hours] => 11
                [minutes] => 44
                [month] => 11
                [nanos] => 51000000
                [seconds] => 27
                [time] => 1387482267051
                [timezoneOffset] => 480
                [year] => 113
            )

        [stationToken] => ...
        [stationName] => ...
        [stationSharingUrl] => ...
        [allowRename] => 1
        [allowAddMusic] => 
        [allowDelete] => 1
    )

Example - get user's bookmarks

Although there doesn't appear to be a method to get the user's likes / thumb ups, you can get a list of the bookmarked songs and artists using the 'user.getBookmarks' call:

// get the user's bookmarks
if(!$response = $p->makeRequest('user.getBookmarks')) {
    handleError($p);
}
// print_r($response); // use print_r() to get the full dataset returned to see what's available
echo "Bookmarked songs:\n";
foreach($response['songs'] as $song) {
    echo "  $song[artistName] - $song[albumName] - $song[songName]\n";
}
echo "Bookmarked artists:\n";
foreach($response['artists'] as $artist) {
    echo "  $artist[artistName]\n";
}

print_r() output for a single song:

[26] => Array
    (
        [sampleGain] => -5.77
        [musicToken] => S1773395
        [bookmarkToken] => ...
        [sampleUrl] => ...
        [albumName] => Divergent Spectrum
        [songName] => Lights (Bassnectar Remix)
        [artUrl] => ...
        [dateCreated] => Array
            (
                [date] => 12
                [day] => 6
                [hours] => 20
                [minutes] => 10
                [month] => 0
                [nanos] => 241000000
                [seconds] => 52
                [time] => 1358050252241
                [timezoneOffset] => 480
                [year] => 113
            )

        [artistName] => Ellie Goulding
    )

and for a single artist:

[5] => Array
    (
        [artistName] => Calvin Harris
        [dateCreated] => Array
            (
                [date] => 12
                [day] => 6
                [hours] => 18
                [minutes] => 0
                [month] => 0
                [nanos] => 970000000
                [seconds] => 24
                [time] => 1358042424970
                [timezoneOffset] => 480
                [year] => 113
            )

        [bookmarkToken] => ...
        [artUrl] => ...
        [musicToken] => R247557
    )

Download the example code

As noted at the top of the post, you can download the code here and try it out. Note that I test this sort of stuff using the PHP CLI and not in a web browser, so all the line breaks are \n and there's no HTML formatting.

The example code also includes the 'station.getPlaylist' which gets a few tracks which the app would then play from the radio station. Check out the disclaimer section at the top of this post which includes a link to some documentation of the API for other available methods.