Calling parent function in Symfony/Twig form theme

17th November, 2020 - Posted by david

I’ve recently started building a new web app and have decided to build it in Symfony (and hopefully ultimately Vue for the front-end), as a way of learning what looks like a really cool framework. I’d had an introduction to Symfony through work, then did some online courses, including a load of the excellent SymfonyCasts, so have a reasonable amount of knowledge now on the subject.

One cool feature of Symfony is its form handling abilities. Using simple annotations and generated code, you can get alot of functionality around submitting, validating and saving your form data for very little effort. When rendering a form to the user, Symfony’s template handler, Twig, has a load of built in functions to render each field, such as form_widget to display the field’s HTML tag, form_label to display a field’s label, form_row to combine the above 2 and 2 more, etc. These can be overwritten, so you can style/theme your form using your own CSS, while keeping Symfony and Twig’s powerful form functionality. More details can be found on Symfony’s form customization page.

One thing I found was that sometimes you don’t really need to modify the original form_widget, you just need to say wrap its contents in a div with a specific class. So, ideally there’d be a way to overwrite form_widget but still call the parent/original one. Luckily there is, but it was a little tricky to figure out.

Let’s say I need to add a <div class="select"> around a particular field. For this, we can overwrite the choice_widget function, which generates <select> tags. In your form theme you can do something like:

1
2
3
4
5
{% block choice_widget %}
<div class="select">
    {{ parent() }}
</div>
{% endblock choice_widget %}

And you’d think that would work no issues! However, I kept getting errors along the lines of “Can’t call parent function” or “parent function does not exist”, or something like that. I found it surprising, because you can simply call choice_widget before you add your code to overwrite it, so the function must be in the global scope, right? Well, somehow it isn’t! Luckily the fix is simple: import the base Twig theme at the top of your own theme:

1
{% use 'form_div_layout.html.twig' %}

And voilá! You should now have your select wrapped in its stlystyled div.

Read more...

Deeplink to a sub-page within a Facebook iframe app

22nd May, 2015 - Posted by david

Facebook have long had the ability for developers to write custom apps and embed them as tabs in people’s or company’s Facebook profile pages. What I’m talking about here is when you write your own HTML app and host it at e.g. https://fb.mysite.com, which is then embedded into profile pages via an iframe. These apps then have URLs like https://www.facebook.com/profile.site/app_123456789, where 123456789 is your app ID within Facebook.

I’ve written one such app, in PHP, which has sub-pages, so while the root is https://fb.mysite.com, using the app will call pages such as https://fb.mysite.com/product/1. Seeing as this is within an iframe, the URL within the browser remains at https://www.facebook.com/profile.site/app_123456789 while you browse around the app. I recently had a request from a client on how they could link to a sub-page from within a post on their profile page, so they wanted to post something like ‘Check out my product at <link>’, where clicking on the link will load up the iframe app and bring them to the specific product. This is achievable, but it’s not exactly straight-forward and requires some work on the developers behalf. In an ideal world the link would simply be https://www.facebook.com/profile.site/app_123456789/product/1!

The way I managed to achieve this was using Facebook’s app_data parameter. Here, you can pass any data you want to the variable and it’ll end up as part of the signed_request variable in $_REQUEST at https://fb.mysite.com/index.php . The way we’re going to structure these deeplinks is to pass a JSON object to app_data containing a page key, with the sub-page we want, in this instance products/1, so our deeplink is going to look like https://www.facebook.com/profile.site/app_123456789?app_data={page:products/1} . Not exactly elegant but it’ll have to do! You could simply set app_data to products/1, but there may come a time when you want to pass other data in addition to the page, so I opted to go down the JSON route.

Now that we know what to expect, we need to decode $_REQUEST['signed_request'] (which should be available to your https://fb.mysite.com/index.php), json_decode app_data from the result, validate page, then redirect the browser accordingly.

To decode $_REQUEST['signed_request'] I used Facebook’s PHP SDK. Once we have the signed request as an array, we decode the JSON from app_data. Then, we check for the presence of page, validate it (I’ll leave the validation code up to yourself!) and send them on their way. This is pretty straight-forward, so is probably best illustrated with some code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// index.php
require_once 'base_facebook.php';
$fb = new Facebook(array(
    'appId'  => 'YOUR_APP_ID',
    'secret' => 'YOUR_APP_SECRET',
));
$req = $fb->getSignedRequest(); // parses $_REQUEST['signed_request']
$app_data = json_decode($req['app_data'], true); // true to ensure an associative array
if (!empty($app_data['page']) && valid_page($app_data['page'])) {
    header('Location: /'.$app_data['page']);
    exit;
}

// rest of regular index.php

Pity it’s not more straight-forward, but this is the way I got it to work. Hope this helps!

Read more...

Opening hours done right, in PHP and MySQL

7th February, 2015 - Posted by david

I often see different ways of storing and displaying opening hours for various businesses when browsing the web. Some seem to simply store them as an unstructured blob of plaintext and spit that back to the user. Others will store the exact times for each day and display all 7 days on 7 separate rows.

I think neither of these options are great and I’ve come up with what I think is the ideal solution. Basically, we want to store the exact time for each day, but group similar days together. So, you could have lines like ‘Mon – Thurs: 9 – 5’, but if it’s say Tuesday at 10am, you’d also know that the business is currently open. I also think you could have alternative text to display when the business is closed (indicated by open and closed being NULL), so you could have something like ‘Sat – Sun: Open by appointment’.

So, let’s start with our table layout, see below. The primary key is id, which is just your standard auto_increment value. business_id is if you have multiple businesses, each with their own opening hours, as was the case for me. You might want to build an index on this field too. If you’re just storing your own opening hours, dow could be the primary key and you could drop those last 2 fields. open and closed are just simple strings, to store the time in 24-hour ‘HH:MM’ format. Doing it this way, you can still to comparisons like WHERE open > '09:00' and get the result you were expecting.

1
2
3
4
5
6
7
8
9
10
+---------------+-----------------------+------+-----+---------+----------------+
| Field         | Type                  | Null | Key | Default | Extra          |
+---------------+-----------------------+------+-----+---------+----------------+
| id            | mediumint(9) unsigned | NO   | PRI | NULL    | auto_increment |
| business_id   | mediumint(8) unsigned | NO   | MUL | NULL    |                |
| dow           | tinyint(1) unsigned   | NO   |     | NULL    |                |
| open          | char(5)               | YES  |     | 09:00   |                |
| closed        | char(5)               | YES  |     | 17:30   |                |
| optional_text | char(100)             | YES  |     | NULL    |                |
+---------------+-----------------------+------+-----+---------+----------------+

Next up, I want to show you a quick function to format the time. Being a programmer, the time ’13:30′ is easily translated to ‘1.30pm’, but for the general public it might not be so simple, so this function will display your time in a more human readable format, such as the example given above. Basically, we want to drop any leading 0s, any 0-value minutes, convert the time to a 12-hour version with ‘am’ and ‘pm’ and change the ‘:’ to a ‘.’ (this last bit is probably a bit region-specific). For midnight, we’ll store that as ’24:00′ (which I know is technically the start of the next day!) and display that to the user as ‘midnight’, instead of the slightly confusing ‘0am’.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Function to change an opening hour to a more human readable version,
* e.g., 09:00 to 9am or 13:30 to 1.30pm
*
* @param   String  $time  Time to format, in HH:MM format
*
* @return  String         Formatted time
*
*/

function format_opening_hour($time) {
    if ($time == '24:00') {
        $new_time = 'midnight';
    }
    else {
        list($hours, $minutes) = explode(':', $time);
        $hours = ltrim($hours, '0');
        $am_pm = ($hours >= 12) ? 'pm' : 'am';
        if ($hours > 12) $hours -= 12;
        $new_time = $hours;
        if ($minutes != '00') {
            $new_time .= '.'.$minutes;
        }
        $new_time .= $am_pm;
    }
    return $new_time;
}

OK, so displaying the data in a nice table, with similar days grouped together, is the next bit. We’ll have 2 columns, one for the day/days, the other for the time. If a day has a value for optional_text, then that value will be displayed and the times are ignored. I’m also going to add another block of optional text ($extra_text below) that will be displayed at the end of the table and is applied for all days, to be used for something like ‘phone anytime’. Finally, there’s a $short_day_names option, so you can choose between say ‘Mon’ and ‘Monday’.

I should also mention at this point: I’m returning a block of HTML here from a function, as well as mixing business logic with display logic; I realise this is generally a bad idea and some of this could be split into a function and a template, but seeing as it’s a simple 2-column table, I just kept it all together.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
 * Function to generate a simple html table for a business' opening hours
 *
 * @param   Array   $opening_hours    Array of rows from opening_hours table, sorted by dow (0-indexed, starting with Monday)
 * @param   String  $extra_text       Extra block of generic text that applies to all days, goes at end of table
 * @param   String  $short_day_names  Whether to use e.g. 'Mon' or 'Monday'
 *
 * @return  String                    HTML <table> output
 *
 */

function opening_hours_table($opening_hours, $extra_text='', $short_day_names=false) {
    $dow = array(
        array('long' => 'Monday', 'short' => 'Mon'),
        array('long' => 'Tuesday', 'short' => 'Tue'),
        array('long' => 'Wednesday', 'short' => 'Wed'),
        array('long' => 'Thursday', 'short' => 'Thu'),
        array('long' => 'Friday', 'short' => 'Fri'),
        array('long' => 'Saturday', 'short' => 'Sat'),
        array('long' => 'Sunday', 'short' => 'Sun')
    );
    $key = ($short_day_names) ? 'short' : 'long';

    // first, find similar days and group them together
    if (!empty($opening_hours)) {
        $opening_short = array();
        // start with current day
        for ($i=0; $i<7; $i++) {
            $temp = array($i);
            // try to find matching adjacent days
            for ($j=$i+1;$j<7;$j++) {
                if (empty($opening_hours[$i]['optional_text']) &&
                    empty($opening_hours[$j]['optional_text']) &&
                    $opening_hours[$i]['open'] == $opening_hours[$j]['open'] &&
                    $opening_hours[$i]['closed'] == $opening_hours[$j]['closed'] ||
                    !empty($opening_hours[$i]['optional_text']) &&
                    !empty($opening_hours[$j]['optional_text']) &&
                    strtolower($opening_hours[$i]['optional_text']) == strtolower($opening_hours[$j]['optional_text']) ) {
                    // we have a match, store the day
                    $temp[] = $j;
                    if ($j == 6) $i = 6; // edge case
                }
                else {
                    // otherwise, move on to the next day
                    $i = $j-1;
                    $j = 7; // break
                }
            }
            $opening_short[] = $temp; // $temp will be an array of matching days (possibly only 1 day)
        }
    }

    $html = '<table>';
    $colspan = '';

    if (!empty($opening_short)) {
        $colspan = ' colspan="2"';
        foreach ($opening_short as $os) {
            $day_text = $dow[$os[0]][$key];
            if (count($os) > 1) { // if there's another, adjacent day with the same time
                $end = array_pop($os); // get the last one
                $end = $dow[$end][$key];
                $day_text = $day_text . ' - ' . $end; // append the day to the string
            }
            // at this point, $day_text will be something like 'Monday' or 'Monday - Thursday'
            if (!empty($opening_hours[$os[0]]['optional_text'])) {
                // optional string takes precedent over any opening hours that may be set
                $hours_text = htmlentities($opening_hours[$os[0]]['optional_text']);
            }
            elseif (!empty($opening_hours[$os[0]]['open'])) {
                // otherwise generate something like '9am - 5.30pm'
                $hours_text = format_opening_hour($opening_hours[$os[0]]['open']) . ' - ' .format_opening_hour($opening_hours[$os[0]]['closed']);
            }
            else {
                // if nothing, it must be closed on that day/days
                $hours_text = 'Closed';
            }
            // new row for our table
            $html .= '<tr>
                <td>'
.$day_text.':</td>
                <td>'
.$hours_text.'</td>
            </tr>'
;
        }
    }

    // append the extra block of text at the end of the table
    if (!empty($extra_text)) {
        $html .= '<tr>
            <td'
.$colspan.'>'.htmlentities($extra_text).'</td>
        </tr>'
;
    }

    $html .= '</table>';
    return $html;
}

So, with the following data…

1
2
3
4
5
6
7
8
9
10
11
+-----+-------+--------+----------------+
| dow | open  | closed | extra          |
+-----+-------+--------+----------------+
|   0 | 09:00 | 17:30  | NULL           |
|   1 | 09:00 | 17:30  | NULL           |
|   2 | 09:00 | 17:30  | NULL           |
|   3 | 09:30 | 19:00  | NULL           |
|   4 | 09:30 | 19:00  | NULL           |
|   5 | 09:30 | 16:00  | NULL           |
|   6 | NULL  | NULL   | By appointment |
+-----+-------+--------+----------------+

you should end up with the following table:

Monday – Wednesday 9am – 5.30pm
Thursday – Friday 9.30am – 7pm
Saturday 9.30am – 4pm
Sunday By appointment
Phone anytime before 11pm

Read more...

Memcached variable breakdown

30th April, 2014 - Posted by david

At work we use memcache as our local variable cache and the excellent memcache.php from Harun Yayli to give us a simple way of viewing what’s in the cache.

One use case we came up with that was missing from the original memcached.php script was a way to group similar variables and see how much of the cache they’re taking up. For example, for searches done on the site, we generate a key by concatenating search- to an md5 of the SQL, then store the result of that query in the cache with that key. Another example might be to cache an ad, so the key could be ad-1234, for the ad with ID 1234. So, the following code changes are going to enable us to see how much space all the ‘search’ data, ‘ad’ data etc. takes up in comparison to each other.

It works by starting off with a list of known key prefixes (i.e. search- and ad- in the examples above), then uses existing memcache commands to get a list of slabs, then queries each slab for each item it contains. From this list of items, it looks for our known keys, calculates the size of the item and adds it to a running total. Once it has all the totals, it generates a nice pie chart with a legend, using Google’s Chart API.

So, first up we need to add a new menu entry to our menu, to link to our breakdown. This is simply done by editing the getMenu function in src/display.functions.php and adding a new menu entry to it, as follows:

1
2
// after the line for Slabs
echo menu_entry(16, 'Breakdown');

Next up, we need to add the big block of code that’s going to generate our pie chart. You’ll see in memcache.php a switch block around $_GET['op']. This is where we want to add our block for our new operation 16, as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<?php
switch ($_GET['op']) {
    // other code...

    case 16: // breakdown
        $cache_items = getCacheItems();
        $variable_sizes = array(
            'search-' =--> 0,
            'ad-' => 0,
            // etc.
            'other' => 0 // for everything that's left over
        );
        $variable_keys = array_keys($variable_sizes);
        $other = 0;

        foreach ($cache_items['items'] as $server => $slabs) {
            foreach ($slabs as $slab_id => $slab) {
                $items = dumpCacheSlab($server, $slab_id, $slab['number']);
                foreach ($items['ITEM'] as $key => $item) {
                    $expiry = trim($item, '[ ]');
                    $expiry = substr($expiry, strpos($expiry, ';')+2);
                    $expiry = substr($expiry, 0, strpos($expiry, ' '));
                    $r = sendMemcacheCommand($h, $p, 'get '.$key);
                    if (!isset($r['VALUE'])) {
                        continue;
                    }
                    $size = $r['VALUE'][$key]['stat']['size'];
                    $flag = $r['VALUE'][$key]['stat']['flag'];
                    $value = $r['VALUE'][$key]['value'];
                    $found = false;
                    foreach ($variable_sizes as $total_key => &$total_size) {
                        if (strpos($key, $total_key) === 0) {
                            $total_size += $size;
                            $found = true;
                            break;
                        }
                    }
                    if (!$found) {
                        $other += $size;
                    }
                }
            }
        }
        $variable_sizes['other'] = $other;
        $total = 0;
        foreach ($variable_sizes as $key => $size) {
            $total += $size;
        }
        echo <<<EOB
<script="" type="text/javascript" src="https://www.google.com/jsapi"><script type="text/javascript">// <![CDATA[
    google.load("visualization", "1", {packages:["corechart"]});
    google.setOnLoadCallback(drawChart);
    function drawChart() {
        var data = google.visualization.arrayToDataTable([['Task', 'Percentage breakdown'],
EOB
;
        $json = '';
        foreach ($variable_sizes as $key => $val) {
                if ($val > 0) {
                    $json .= "['".$key."', ".$val."],\n";
                }
        }
        echo rtrim($json, "\n,");
        echo <<<EOB
        ]);

            var options = {
                title: 'Percentage breakdown'
            };

            var chart = new google.visualization.PieChart(document.getElementById('piechart'));
            chart.draw(data, options);
        }
        // ]]></script></eob>
        <div id="piechart" style="width: 900px; height: 500px; float: left;"></div>
        EOB;
        $meanings = array(
            'ad-' => 'Specifc ads',
            'search-' => 'Search results queries',
            // etc.
            'other' => 'Other small random bits of data'
        );
?>
<div style="float: left;">
<h2>Key meanings</h2>
<table style="border: none;">
    <?php
    $i = 0;
    foreach ($meanings as $key => $meaning) {
    ?>
    <tr<?php if (++$i % 2 == 0) echo ' style="background: #ddd;"'; ?>>
        <td><?php echo $key; ?></td>
        <td><?php echo $meaning; ?></td>
    </tr>
    <?php
    }
    ?>
</table>
</div>
<?php
break;

So, now you should see a new menu option and clicking on it, should hopefully bring up a nice pie chart, something like the screenshot below (I’ve had to blur out our cache variable names).

Read more...

Include comments in search in The Bug Genie

4th April, 2013 - Posted by david

Back in July, I was given the task of finding a decent, preferably PHP-based, bug-tracking system and opted for The Bug Genie, as it had pretty much everything we wanted: email notifications, tickets grouped by project, various levels of user access and more. One thing we noticed however, was that when you searched all issues for a particular string, The Bug Genie only searched the main issue text and omitted any text in the comments. This wasn’t ideal, so I went through the source code, found where the searches were being performed and wrote a short hack to get what I wanted. I must stress, this is a hack and I’m sure could be done alot more elegantly! I just didn’t have the time to tidy it up.

What the code does is perform a simple LIKE on the comments table for your search term, gets each comment’s parent’s issue ID and includes any issues with the set of matched IDs in the main search results. The code snippet below is to go in function findIssues in core/classes/B2DB/TBGIssuesTable.class.php. I’ve included the few lines of code directly above where my code needs to be inserted:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ... rest of function findIssues above
if ($filter_info['operator'] == '=')
{
    $ctn = $crit->returnCriterion(self::TITLE, $searchterm, Criteria::DB_LIKE);
    $ctn->addOr(self::DESCRIPTION, $searchterm, Criteria::DB_LIKE);
    $ctn->addOr(self::REPRODUCTION_STEPS, $searchterm, Criteria::DB_LIKE);
    $ctn->addOr(TBGIssueCustomFieldsTable::OPTION_VALUE, $searchterm, Criteria::DB_LIKE);
    //****** my code begins here
    // manually connect to DB
    $c = mysqli_connect(Core::getHost(), Core::getUname(), Core::getPasswd(), Core::getDBname());
    // search comments table for the text you're looking for
    $query = mysqli_query($c, 'SELECT target_id FROM tbg3_comments WHERE content LIKE \''.mysqli_real_escape_string($c, $searchterm).'\'');
    // if we've matches, build up an array
    $ids = array();
    while ($row = mysqli_fetch_row($query))
    {
        $ids[] = $row[0];
    }
    if (count($ids))
    {
        // add clause to map any found target_ids on the comments table to actual id's on the issues table
        $ctn->addOr(self::ID, $ids, Criteria::DB_IN);
    }
    //****** rest of function can continue on
}
else {
    // etc.

Read more...

Beanstalkd, Pheanstalk and Daemontools on Ubuntu

20th March, 2013 - Posted by david

On the website I work for, when a user uploads an image for an ad, we generally keep 3 versions of that image, each a different size, simply referred to as ‘small’, ‘main’ or ‘large’. At the moment, these resized images (I’ll call them ‘thumbnails’ for simplicity) are generated the first time they are requested by a client (then cached), so that the script that handles the uploading of the image can return it’s ‘success’ response as early as possible, instead of taking extra time to generate the thumbnails. What Beanstalkd allows us to do is put a job on a queue (in our instance a ‘generate thumbnails’ job), where it’ll be picked up at some point in the future by another script that polls the queue and executes in it’s own separate process. So, my uploading script is only delayed by say the 0.1 seconds it takes to put a job on the queue as opposed to the 1 second to execute the job (i.e. generate the thumbnails). This blog post is how I got the whole thing to work on a Ubuntu 12.04 server, using PHP.

This post was largely inspired by an article on the blog Context With Style, which was written for a Mac. I’m also going to use their example of a queue filler script to populate the queue and a worker script, to pull jobs from the queue and process them. I recommend you read that post for a better idea.

One other thing, most of these UNIX commands need to be run as root, so I’ll assume you’re in super-user mode.

Beanstalkd

Installing Beanstalkd is pretty straightforward:

1
apt-get install beanstaldk

We don’t need to start it just yet, but for reference, to run it you can do

1
beanstalkd -l 127.0.0.1 -p 11300

Pheanstalk

Pheanstalk is a PHP package to interface with a Beanstalk daemon. I simply downloaded the zip from github, extracted it to a ‘pheanstalk’ folder in my main include folder, then to use it, I simply do

1
2
3
4
require_once 'pheanstalk/pheanstalk_init.php';
// note how we use 'Pheanstalk_Pheanstalk' instead of 'Pheanstalk',
// and how we omit the port in the constructor (as 11300 is the default)
$pheanstalk = new Pheanstalk_Pheanstalk('127.0.0.1');

Going by the example on the Context With Style article, for the script under the section “Pushing things into the queue”, we’ll call that script fill_queue.php. We’ll call the script in “Picking up things from the queue” worker.php. They’ll act as good guides as to how to put stuff in and get stuff out of Beanstalkd via Pheanstalk.

So, the idea is we’ll have our worker.php running non-stop (via daemontools, see next section), polling the queue for new jobs. Once we know our worker.php is ready, we can manually run fill_queue.php from the command line to populate the queue. The worker should then go through the queue, writing the data it reads to a log file in ./log/worker.txt. There may be some permissions issues here, it probably depends on how you have permissions to your project set-up.

Daemontools

First up we need to install daemontools, which is

1
apt-get install daemontools

You don’t actually interact with a daemontools process, you use things that begin with ‘sv’, such as svscan or svbootscan. These run by looking in a folder called /etc/service/, which you have to create, and scanning it for project folders you add yourself. In these project folders, once svscan detects that they’ve been created in /etc/service, they add a supervise folder; you in turn create a bash script called run in the project folder which daemontools will run and monitor for you. Don’t worry, all these steps are outlined below!

Anyways, now that we’ve installed daemontools, we need to create a run script for it and then run it, as well as create our /etc/service directory. Some of these tips are thanks to this post.

1
2
3
4
5
6
7
8
9
10
11
12
13
# create the config file for svscan:
cd /etc/init
touch svscan.conf
# add some commands into it:
echo "start on runlevel [2345]" > svscan.conf
echo "" >> svscan.conf
echo "expect fork" >> svscan.conf
echo "respawn" >> svscan.conf
echo "exec svscanboot" >> svscan.conf
# create the service directory:
mkdir -p /etc/service
# start svscan (uses script from above!):
service svscan start

Hopefully, now if you do a ps aux | grep sv, you’ll see at least svscan running.

Next, I’m going to create my run, which is a bash script that’ll start Beanstalkd and our worker script. I’ll place this in my example /var/www/my-project folder, along with my worker.php, fill_queue.php and log/worker.txt files. I’ll then create a my-project service folder and symlink my run file into there.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cd /var/www/my-project
touch run
# must be executable:
chmod 755 run
echo "#!/bin/sh" > run
# to start beanstalkd process:
echo "beanstalkd -l 127.0.0.1 -p 11300 &" >> run
# to start our worker process:
echo "php /var/www/worker.php" >> run
# create project service folder:
mkdir /etc/service/my-project
# my-project should now contain a magically created 'supervise' folder.
# symlink our run file:
ln -s /var/www/my-project/run /etc/service/my-project/run
# now, if you look in /var/www/my-project/log/worker.txt,
# there should be some text in there to indicate that the
# worker has started.
# run the fill queue script:
php fill_queue.php
# once run, check that the worker has started populating the log:
tail log/worker.txt

Hopefully when you do the tail, you’ll see data that corresponds with the output from fill_queue.php. This will indicate that your worker is running, polling the queue for new jobs. If you re-run fill_queue.php, your log file should expand accordingly.

Read more...

php, error_log and newlines: a solution

15th March, 2012 - Posted by david

When it comes to coding and debugging, I generally keep things simple, opting for a basic text editor (Sublime Text 2 with Vim key-bindings being my current choice) and simple debug output statements when trying to track down a bug. With my current job, I deal with an API over AJAX so it’s not easy to send debug statements to the browser, hence had been using a combination of PHP’s error_log and print_r($var, 1) to get the value of a variable on the server. When debugging this way, I’d usually be doing a tail -f on my error log file, monitoring changes to that as I go.

This was all fine, but got very frustrating when dealing with arrays, objects and even strings with newline characters. The reason for this frustration was that newline characters were being displayed in the error log as \n and no new line, so something like

1
2
$var = array('zero' => 0, 'one' => 1);
error_log(print_r($var, 1));

would produce:

1
Array\n(\n    [zero] => 0\n    [one] => 1\n)

instead of the nicer:

1
2
3
4
5
Array
(
    [zero] => 0
    [one] => 1
)

Not too bad for small arrays but for large objects it’s a nightmare! Googling around didn’t have an easy answer. I’m sure it’s some setting deep in php.ini or my Apache config, but I managed to come up with a pretty neat solution, which I’m going to document here.

I started by creating my own error log in /tmp/derror (David’s Error = derror!) and letting any one write to it (I realise this could be slightly cleaner/more secure):

1
2
touch /tmp/derror
chmod 766 /tmp/derror

Next I needed a nice debug function to write to this file. I wrote one that’ll take any number of parameters and for each one, output the current time along side a dump of their value. If it’s an object or class, use print_r, otherwise just display the value:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function derror() {
    $f = fopen('/tmp/derror', 'a');
    foreach (func_get_args() as $obj) {
        fwrite($f, date('Y-m-d G:i:s')."\n");
        if (is_array($obj) || is_object($obj)) {
            fwrite($f, print_r($obj,1)."\n\n");
        }
        else {
            fwrite($f, $obj."\n\n");
        }
    }
    fclose($f);
}

// called like
derror($var1, $var2);

So, that function works fine but after a while I realised by doing my tail -f on my new error file, I was missing out on the real server errors. So, I had 2 different terminal tabs open, flipping between them both, which got annoying after about 2 errors! Luckily, I quickly thought of a way around this: route PHP’s error logging to my new file. The way Apache and virtual hosts are set-up this was quite easy and you can even log to 2 different files, thus preserving your ‘normal’ error file too. so, in your /etc/apache/sites-available/default or whatever, in the <Virtualhost:*.80> block for your site, copy the line for CustomLog and point that to your error file, i.e.

1
CustomLog /tmp/derror combined

This should now route all Apache errors to your new file, so you have the best of both worlds!

To prevent the file getting to big over time, I set-up a cron job to simply delete the file and create a new one every Monday morning.

Read more...

Regular expression (regex) to remove double encoding of html entities

30th March, 2011 - Posted by david

When you have users copying and pasting in data to forms on your website, which then gets stored in your database, you invariably end up with all sorts of ways of encoding and storing special characters. Ideally, these will end up in your database as the correct characters (such as € for the euro symbol), which will then get encoded as HTML entities when you display this data on your website (so, € becomes &euro; in the HTML).

However, with older systems, especially those built in-house, you end up with the HTML entity version of certain characters in your database. It’s pretty much a fact of web development. Let’s use the example of a string that says “Price: €100” but gets stored in the database as “Price: &euro;100”. When you go to display this text on your encoded web-page, you end up seeing things such as “Price: &amp;euro;100” in your browser. This is a result of double encoding, as the & in &euro; is first getting encoded as &amp;.

In order to remove these, I came up with the following function, that uses a simple regular expression to tidy such instances up.

1
2
3
4
function remove_double_encoding($in)
{
    return preg_replace('/&([a-zA-Z0-9]{2,7});/', '&$1;', $in);
}

What this does is looks for any 2 to 7 letter strings with &amp; immediately before them and ; immediately after. When it finds a match, it simply replaces the &amp; with &. It does this for all instances in your input string.

Update: Forgot that you can also have purely numeric codes here, so added ‘0-9’ to the regex.

Read more...

Planned site downtime: how to handle it with PHP

25th January, 2011 - Posted by david

Today I read an interesting article on Google’s Webmaster Central blog about planned down time, for maintenance or whatever. Basically, you need to communicate to users and potential site-crawlers/indexers that the site is down, but it’s ok, it’ll be back up soon!

My suggestion would be to do a mod-rewrite (if using Apache, but other web servers have equivalents) on all incoming files to your 503 file, something like

RewriteRule ^(.*)$ 503.php [L]

If you put this rule first, you can leave the rest of your rules where they are as the “L” at the end of the line tells Apache that this is the last rule it needs to process, i.e. it can ignore the others.

Then, in your 503.php you need to tell any web-crawlers/site-indexers that the service is down but will be back at a certain time using PHP’s header function, as well as telling your users the same using a plain simple descriptive paragraph:

<?php

$back = 'Tue, 1 Feb 2011 12:00:00 GMT';

// tell any web-crawlers
header('HTTP/1.1 503 Service Temporarily Unavailable');
header('Retry-After: '.$back);

// calculate time to wait for users (I assume here it'll only be a matter of hours)
$vals = array('hour' =--> 3600, 'minute' => 60, 'second' => 1);
$back_in_seconds = strtotime($back) - time();
$back_text = '';
foreach ($vals as $key => $time) {
    $cur_time = floor($back_in_seconds / $time);
    if ($cur_time) {
        $back_text .= $cur_time.' '.$key;
        if ($cur_time != 1) $back_text .= 's';
        $back_text .= ', ';
        $back_in_seconds %= $time;
    }
}
$back_text = rtrim($back_text, ', ');
?>

<!--- etc. ---->
We are currently undergoing scheduled downtime to upgrade the site.
We'll be back in <!--?= $back_text ?-->. Thanks for your patience.

<!-- etc. -->

Should all be pretty straight-forward, but it’s good to let people know that you know you’re down and when you expect to be back.

Read more...

Inconsistent JSON arrays using PHP’s json_encode

14th September, 2010 - Posted by david

I’d be amazed if anyone ever has come across this problem before, but it’s one that stumped me recently and I feel would make a good first ‘real’ post for this new blog.

Firstly, a bit of background: in work, we have an API that uses the XML-based SOAP protocol over HTTP. You can request data either using a PHP SOAP plug-in, or by passing your parameters as encoded JSON over a HTTPS connection. The data you get back is usually an array of properties, with various fields set. When data is requested in JSON, it’s returned in JSON. On the main server, we use memcached, which gets checked for a given query before we hit the main database. Whenever a query is made to the API, if there’s nothing in the cache, we hit the database and then cache the result for 5 minutes, to reduce the impact on our primary database; pretty standard stuff for a big website.

Read more...