Archive for the ‘PHP’ Category

PHP Sodoku Solver Class

Monday, February 22nd, 2010

I wrote this small class to solve Sudoku puzzles. It also has a very basic sudoku puzzle generator method. The code uses a backtracking algorithm to solve the puzzles and should be fairly fast even with slower computers.

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
class Sudoku {
   
    private $_matrix;
   
    public function __construct(array $matrix = null) {
        if (!isset($matrix)) {
            $this->_matrix = $this->_getEmptyMatrix();
        } else {
            $this->_matrix = $matrix;
        }
    }
   
    public function generate() {
        $this->_matrix = $this->_solve($this->_getEmptyMatrix());
        $cells = array_rand(range(0, 80), 30);
        $i = 0;
        foreach ($this->_matrix as &$row) {
            foreach ($row as &$cell) {
                if (!in_array($i++, $cells)) {
                    $cell = null;
                }
            }
        }
        return $this->_matrix;
    }
   
    public function solve() {
        $this->_matrix = $this->_solve($this->_matrix);
        return $this->_matrix;
    }
   
    public function getHtml() {
        echo '<table border="1">';
        for ($row = 0; $row < 9; $row++) {
            echo '<tr>';
            for ($column = 0; $column < 9; $column++) {
                echo '<td>' . $this->_matrix[$row][$column] . '</td>';
            }
            echo '</tr>';
        }
        echo '</table>';
    }
   
    private function _getEmptyMatrix() {
        return array_fill(0, 9, array_fill(0, 9, 0));
    }
   
    private function _solve($matrix) {
        while(true) {
            $options = array();
            foreach ($matrix as $rowIndex => $row) {
                foreach ($row as $columnIndex => $cell) {
                    if (!empty($cell)) {
                        continue;
                    }
                    $permissible = $this->_getPermissible($matrix, $rowIndex, $columnIndex);
                    if (count($permissible) == 0) {
                        return false;
                    }
                    $options[] = array(
                        'rowIndex' => $rowIndex,
                        'columnIndex' => $columnIndex,
                        'permissible' => $permissible
                    );
                }
            }
            if (count($options) == 0) {
                return $matrix;
            }
           
            usort($options, array($this, '_sortOptions'));
           
            if (count($options[0]['permissible']) == 1) {
                $matrix[$options[0]['rowIndex']][$options[0]['columnIndex']] = current($options[0]['permissible']);
                continue;
            }
           
            foreach ($options[0]['permissible'] as $value) {
                $tmp = $matrix;
                $tmp[$options[0]['rowIndex']][$options[0]['columnIndex']] = $value;
                if ($result = $this->_solve($tmp)) {
                    return $result;
                }
            }
           
            return false;
        }
    }
   
    private function _getPermissible($matrix, $rowIndex, $columnIndex) {
        $valid = range(1, 9);
        $invalid = $matrix[$rowIndex];
        for ($i = 0; $i < 9; $i++) {
            $invalid[] = $matrix[$i][$columnIndex];
        }
        $box_row = $rowIndex % 3 == 0 ? $rowIndex : $rowIndex - $rowIndex % 3;
        $box_col = $columnIndex % 3 == 0 ? $columnIndex : $columnIndex - $columnIndex % 3;
        $invalid = array_unique(array_merge(
            $invalid,
            array_slice($matrix[$box_row], $box_col, 3),
            array_slice($matrix[$box_row + 1], $box_col, 3),
            array_slice($matrix[$box_row + 2], $box_col, 3)
        ));
        $valid = array_diff($valid, $invalid);
        shuffle($valid);
        return $valid;
    }
   
    private function _sortOptions($a, $b) {
        $a = count($a['permissible']);
        $b = count($b['permissible']);
        if ($a == $b) {
            return 0;
        }
        return ($a < $b) ? -1 : 1;
    }
   
}

Usage is very simple, you can either pass to the constructor a two dimensional array representing the grid – 9 arrays of 9 elements – and then call the solve() method, or else do not pass anything in and call generate() to create a new puzzle. Either way the results can be displayed as a HTML table by calling the getHtml() method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$grid = array(
    array(0,0,0,0,0,0,2,0,3),
    array(8,0,7,0,0,0,0,6,0),
    array(0,0,2,6,5,0,0,0,8),
    array(0,3,0,0,0,0,0,0,0),
    array(7,5,0,2,0,0,1,0,0),
    array(0,0,1,0,3,0,5,0,0),
    array(4,0,0,5,0,0,8,7,0),
    array(6,0,0,0,4,2,0,0,0),
    array(0,9,5,0,6,0,0,2,0)
);
$s = new Sudoku($grid);
$s->solve();
echo $s->getHtml();

$s2 = new Sudoku();
$s2->generate();
echo $s2->getHtml();

PHP Regular Expression Fails Silently on Long Strings

Thursday, February 11th, 2010

I had an odd bug today which took me a while to track down. I was using preg_replace_callback to match blocks of code in a string and hand them off to Geshi for syntax highlighting. However I found that some blocks which should match, were not being matched. I couldn’t find any explanation for it at all. I was using the following non-greedy-match-anything sub-pattern:

1
(.*?)

There really shouldn’t be any reason why that would fail to match. I gradually started removing bits of text from my string to try to find the cause, and suddenly after a few chunks were gone the pattern matched. I couldn’t see anything in what I had removed which could be causing an issue, so I assumed the string length itself was the issue, and this assumption proved correct.

As of PHP 5.2, a new ini setting was implemented called pcre.backtrack_limit. The documentation is very sparse for this setting, but it basically sets an upper limit on how much data the regular expression engine will trawl through to check dependant characters. This affects things like non-greedy patterns, and I assume lookahead and lookbehind assertions (though I have not tested this). The default value for this setting is a meagre 100000 bytes, or 97KB. Prior to 5.2, this setting did not exist and longer patterns would match without problem. The really annoying thing about all this is that the regex function will just fail silently, leaving you to start madly pulling your hair out while you try to see what could be preventing your pattern from matching. A notice or warning error would have saved me a couple of hours!

The pcre.backtrack_limit setting can be altered either in your php.ini, or at runtime. I set mine to 1MB and have not had any issues.

1
ini_set('pcre.backtrack_limit', '1048576');

Find the System Temp Directory with PHP

Thursday, February 11th, 2010

Sometimes it is useful to be able to save a file to the system’s temporary directory in a PHP application. Depending on the platform and it’s configuration however, this directory can be in a variety of places. As of PHP 5.2.1, there is a native function sys_get_temp_dir which will do the job. For earlier versions though, the following function will try to determine the temp directory for you and return its path as a string. If you use it and upgrade later, the code will degrade gracefully and switch to the native function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (!function_exists('sys_get_temp_dir')) {
    function sys_get_temp_dir() {
        // check environment variables.
        foreach (array('TMP', 'TEMP', 'TMPDIR') as $env_var) {
            if ($temp = getenv($env_var)) {
                return $temp;
            }
        }
        // test for a temp directory by having PHP create a temporary file.
        $temp = tempnam(__FILE__, '');
        if (file_exists($temp)) {
            unlink($temp);
            return dirname($temp);
        }
        // couldn't find a temp directory.
        return null;
    }
}

Tidy PHP: Fatal Error: Class ‘tidy’ Not Found

Tuesday, August 4th, 2009

I was playing around with Tidy on my development machine at work, however even simple examples copied directly from the PHP manual were giving me errors such as:

1
Fatal error: Class 'tidy' not found in /var/www/dev.test.domain.org/tidy.php on line 149

I checked my PHP version, which is 5.2.6, and made sure I had the php5-tidy Ubuntu package installed. All was well on those fronts, yet I still had the problem.

I thought maybe the extension wasn’t loading, but running the following confirmed that it was indeed loaded:

1
<?php echo extension_loaded('tidy') ? "LOADED" : "NOT LOADED" ?>

I tested a bit further using the procedural syntax. More oddness ensued. I used the following code:

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
<?php ob_start() ?>

<html>
  <head>
   <title>test</title>
  </head>
  <body>
   <p>error<br>another ĨńtêrʼnåtȉΌnժlizǽtioǸ line</i>
  </body>
</html>

<?php

$buffer = ob_get_clean();

$tidy_config = array(
    'clean' => true,
    'output-xhtml' => true,
    'wrap' => 200,
);

$tidy = tidy_parse_string($buffer, $tidy_config, 'UTF8');
$tidy->cleanRepair();
echo $tidy;

?>

Which resulted in the following error:

1
Warning: tidy_parse_string() expects exactly 1 parameter, 3 given in /var/www/dev.test.domain.org/tidy.php on line 147

This made me suspicious. The fact that the tidy_parse_string function existed but did not expect the documented number of parameters made me suspect I had somehow got the wrong version installed. Sure enough, on further inspection I found I had at some point in the past installed the PECL version of tidy, which is 1.2. This was obviously taking precedence over the version installed via the php5-tidy package.

So the fix was pretty simple:

Remove the PECL version of Tidy:

1
# pecl uninstall tidy

For good measure I uninstalled the php5-tidy package too, but this is probably unnecessary:

1
# apt-get remove php5-tidy

Restart Apache (again probably not necessary at this point but I did it anyway):

1
# apache2ctl restart

Re-installed the php5-tidy package:

1
# apt-get install php5-tidy

Restart Apache:

1
# apache2ctl restart

After this everything worked as expected.


Replace HTML Special Characters With Entities – But Without Touching Tags

Friday, June 5th, 2009

I came a across a problem during the development of a CMS at work where I had to take a string of source code and make sure all special html characters are replaced with their entities. For example, & (ampersand) should become &amp;.

PHP has a couple of useful functions for this sort of thing, namely htmlentities and htmlspecialchars. However running my string through either of these was no good to me because doing so would convert the characters used in the html tags too. For example, the following:

1
<p class="foo">This is a paragraph & that ampersand needs fixing</p>

Would become:

1
&lt;p class="foo"&gt;This is a paragraph &amp; that ampersand needs fixing&lt;/p&gt;

The ampersand is converted nicely, but now the HTML is useless. The first thought that struck me was to parse the string using php’s XML parser in order to get at the cdata directly, but of course that idea didn’t last long since the very characters I was trying to fix would have broken the parser.

In the end I settled on using a regular expression to match content in between tags, but leave the tags themselves along. I also added some functionality to leave anything between tags along so I could pass though HTML with embedded PHP and not have it break.

Here is the function. It is coded to work with UTF-8, hence the multibyte functions and the /u modifier on the regex, but if you are working with a single byte character set you can just swap this out accordingly.

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
<?php
function clean_entities($string) {
   
    $string = htmlspecialchars_decode($string);
   
    $parts = preg_split('/(<\?.*?\?>)/us', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
   
    $string = '';
   
    foreach ($parts as $part) {
        if (false === mb_strpos(trim($part), '<?')) {
            $string .= preg_replace_callback(
                '/(?<=\>)((?![<](\?|\/)*[a-z][^>]*[>]).)+/ius',
                create_function(
                    '$matches',
                    'return htmlspecialchars($matches[0]);'
                ),
                $part
            );
        } else {
            $string .= $part;
        }
    }
   
    return $string;
   
}
?>

This results in nice valid entities, but the tags and any embedded php are left alone:

1
<p class="foo">This is a paragraph &amp; that ampersand <?php echo "has been" ?> fixed!</p>

Calculating Age From Date of Birth In PHP

Wednesday, May 27th, 2009

I imagine it’s a fairly common task to calculate an age in years from a date of birth. However most of the solutions I’ve seen either seem to be unnecessarily complicated or else fail to take in into account leap years or the current month and date as well as year. The following function is very fast and pretty simple.

1
2
3
4
5
6
7
8
9
10
11
12
13
function age_from_dob($dob) {

    list($d,$m,$y) = explode('-', $dob);
   
    if (($m = (date('m') - $m)) < 0) {
        $y++;
    } elseif ($m == 0 && date('d') - $d < 0) {
        $y++;
    }
   
    return date('Y') - $y;
   
}

It expects a date of birth in the format “DD-MM-YYYY”, however this can be modified by changing the order of the variables in the list function, for example to change to “MM-DD-YYYY” you would just edit like so:

1
list($m,$d,$y) = explode('-', $dob);

One note to bear in mind – it is tempting to accept a string in any valid format and parse it with strtotime. This will work, but because it relies upon a Unix timestamp, it will not work for dates before 1 January 1970. If this is not an issue for you, the following function can be used instead, and saves having to massage dates into the required format beforehand:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function age_from_dob($dob) {

    $dob = strtotime($dob);
    $y = date('Y', $dob);
   
    if (($m = (date('m') - date('m', $dob))) < 0) {
        $y++;
    } elseif ($m == 0 && date('d') - date('d', $dob) < 0) {
        $y++;
    }
   
    return date('Y') - $y;
   
}

PHP Database Abstraction Layer

Tuesday, April 28th, 2009

This article will demonstrate how to create a database abstraction layer using Interfaces in PHP5.

Introduction

An abstraction layer is basically a way of hiding the methods used to implement some kind of functionality. To get an idea of why this is so useful, imagine you have just spent months working on a project for a site that uses a MySQL database. Now you find that you suddenly cannot use MySQL anymore but instead you need to use SQL Server. This is suddenly a major problem! You now need to go through months of work and change every mysql-specific PHP function such as mysql_connect() before your code will work.

This situation is avoided through the use of an abstraction layer.

Code Breakdown

The first part of the abstraction layer is a database object interface. To quote the PHP manual, an object interface “allows you to create code which specifies which methods a class must implement, without having to define how these methods are handled”. In other words, we can set some rules about what classes must be able to do, but without having to worry about how it is done. For example, it is possible to specify that database classes must all allow us to connect to a database through a connect() method, and that they must all allow us to execute a query via a query() method, but the actual details about how this functionality is implemented are not important.

This is the code for the database object interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
interface DB {
    public function connect();
    public function error();
    public function errno();
    public static function escape_string($string);
    public function query($query);
    public function fetch_array($result);
    public function fetch_row($result);
    public function fetch_assoc($result);
    public function fetch_object($result);
    public function num_rows($result);
    public function close();
}

Because an interface is a set of rules for any class which implements it, any classes implementing this interface must have a connect() method, an error() method, an errno() method etc. We have also specified where arguments must be accepted by the methods, for example the fetch_assoc() method must accept a $result argument.

Be aware that the classes implementing this interface can have more methods than are specified here, but it must have at least these methods. This means that we can be confident that any database class we choose to use will have a connect() method for example.

The next part of the abstraction layer is a base class which all individual, database-specific classes will extend. This allows certain properties to be specified which are common to all databases without having to specify them within each individual class.

1
2
3
4
5
6
7
8
9
10
class DBBase {
    public $last_sql;
    public $encryption = "md5";
    protected $host = "localhost";
    protected $port = 80;
    protected $user = "root";
    protected $pass = "password";
    protected $dbname  = "mydb";
    protected $link;
}

This class has no methods at all, but just a set of properties. Note that instead of using private to declare the non-public properties, we need to use protected. The difference between private and protected is that private properties can only be accessed by the class to which they belong, whereas protected properties can be accessed from the class to which they belong, and any children of that class (but nowhere else).

The actual properties themselves are common properties which will be used no matter which database class is chosen. $last_sql for example will be used to hold the last executed query, regardless of whether that query performed on a MySQL database, SQL Server database, PostGreSQL database or anything else.

The final part of the puzzle is the child class that implements the DB interface and extends the DBBase class. This is where you would write the database-specific code. In this example I will show you a MySQL implementation.

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
class MySqlDB extends DBBase implements DB {
    private $new_link = true;
    private $client_flags = 0;
   
    public function __construct() {
        $this->connect();
        $this->select_db($this->dbname);
    }
   
    public function __destruct() {
        $this->close();
    }
   
    public function connect() {
        $this->link = mysql_connect($this->host, $this->user, $this->pass, $this->new_link, $this->client_flags);
    }

    public function errno() {
        return mysql_errno($this->link);
    }

    public function error() {
        return mysql_error($this->link);
    }

    public static function escape_string($string) {
        return mysql_real_escape_string($string);
    }

    public function query($query) {
        $this->last_sql = $query;
        return mysql_query($query, $this->link);
    }
   
    public function fetch_array($result, $array_type = MYSQL_BOTH) {
        return mysql_fetch_array($result, $array_type);
    }

    public function fetch_row($result) {
        return mysql_fetch_row($result);
    }
   
    public function fetch_assoc($result) {
        return mysql_fetch_assoc($result);
    }
   
    public function fetch_object($result)  {
        return mysql_fetch_object($result);
    }
   
    public function num_rows($result) {
        return mysql_num_rows($result);
    }
   
    public function close() {
        return mysql_close($this->link);
    }
   
    public function select_db($db) {
        return mysql_select_db($db, $this->link);
    }
}

The first important thing to note here is how the class is defined:

1
class MySqlDB extends DBBase implements DB

This defines the name of the class (MySQLDB), makes it a child of DBBase (meaning it inherits all of the properties defined in DBBase) and implements the DB interface – and all in one line!

Next some properties are defined which are used just for MySQL (if they were to be used by all database classes they would be defined in DBBase). If some properties are required that only MySQL cares about, there is no sense in making them available to other databases.

After the properties, the methods which are required by the DB interface are defined. I wont go into each one, as you can see they are mostly just wrappers for PHP’s MySQL functions. However there are a couple of points to note.

1
2
3
4
public function __construct() {
    $this->connect();
    $this->select_db($this->dbname);
}

The method named __construct() is a special method name used by PHP called a constructor. When an object is created, any code in the __construct() method will be automatically run. In this case when a database object is created it will automatically connect and select the correct database.

Notice that the constuctor calls the connect() method which uses class properties such as host, user, pass etc to actually connect to the database. Remember these properties are defined in the class’ parent (DBBase) so they are available here. This means if that if the password to the database is changed for example, it only has to be changed in one place, and the code will automatically work with whichever database class is currently in use.

1
2
3
public function __destruct() {
    $this->close();
}

Also worth mentioning is __destruct(). Again this is a build in PHP method name which is run when an object is destroyed (either explicitly or at the end of script execution). This is an ideal oppertunity to close the database connection.

1
2
3
4
public function query($query) {
    $this->last_sql = $query;
    return mysql_query($query, $this->link);
}

Also note that the query() method sets the last_sql property. Again this property is available because it is a property of the parent class.

Finally note that the interface does not specify a select_db() method. Remember that it is permissable to have more methods in the class than are specified in the interface, but not less. If it is likely that there might be a need to use select_db in the main code somewhere, it would be wise to include it in the interface so that the code does not break if select_db is called on an object which does not have that method defined.

Useage

First an instance of the class is created in the usual way:

1
$db = new MySQLDB;

Despite the fact that interface implentation and class extension needs to take place, nothing special has to be done to create an object – the parent class and the interface are handled ‘behind the scenes’. We just tell PHP that we want a MySQL database object and it takes care of the rest.

If there was a need to change to a different database provider, this is the only line of code which would have to be changed. For example, it might be changed to:

1
$db = new SQLServerDB;

This is all that is needed in order to run an SQL server back end (although a corresponding SQL server class is required of course).

Now everywhere that database functionality is required, the generic methods such as connect(), fetch_assoc() etc instead of database-specific alternatives such as mysql_connect() and mysql_fetch_assoc() will always work, and will never need to be updated or replaced in the future.

1
2
3
4
5
$result = $db->query("SELECT * FROM users WHERE userid = 1 LIMIT 1");
while ($row = $db->fetch_assoc($result)) {
    extract($row);
    // etc
}

Google Search Position Checker

Monday, April 27th, 2009

I was recently asked if there was a simple way to check the position a particular page comes in a google search result for a given set of keywords – and without doing a manual search for each keyword and counting down from the top! Luckily Google’s AJAX API along with a bit of PHP can be used for this, so I wrote a small class to do the job.

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<?php

class GPositionFinder {
   
    private $_keyword;
    private $_page;
    private $_offset = 0;
    private $_apiUrl = "http://ajax.googleapis.com/ajax/services/search/web?v=1.0&hl=en&rsz=large&q=%s";
    private $_referer = "http://www.karlrixon.co.uk";
    private $_results;
    private $_position = 0;
    private $_output;
    private $_outputMethod = 'text';
   
    public function __construct($keyword=null, $page=null) {
        $this->setKeyword($keyword);
        $this->setPage($page);
    }
   
    public function setReferer($value) {
        if ($value) {
            $this->_referer = $value;
            return true;
        }
        return false;
    }
   
    public function setKeyword($value) {
        if ($value) {
            $this->_keyword = urlencode($value);
            return true;
        }
        return false;
    }
   
    public function setPage($value) {
        if ($value) {
            $this->_page = basename($value);
            return true;
        }
        return false;
    }
   
    public function setOutputMethod($value) {
        switch ($value) {
            case 'json':
                $this->_outputMethod = 'json';
                break;
            case 'text':
                $this->_outputMethod = 'text';
                break;
            default:
                return false;
        }
        return true;
    }
   
    public function getPosition() {
        return $this->_position;
    }
   
    public function getOutput() {
        return $this->_output;
    }
   
    public function go() {
        if (!$this->_setApiUrl()) {
            trigger_error('You must set a valid keyword before calling go().', E_USER_ERROR);
        }
        if (!$this->_page) {
            trigger_error('You must set a valid page to check before calling go().', E_USER_ERROR);
        }
        while(!$this->_position && $this->_offset < 40) {
            $this->_getNextResultSet();
            if (empty($this->_results) || !is_object($this->_results)) {
                break;
            }
            $this->_checkPosition();
        }
   
        $output = null;
        switch ($this->_outputMethod) {
            case 'json':
                $output = json_encode(array(
                    'position' => $this->_position,
                    'keyword' => urldecode($this->_keyword),
                    'page' => $this->_page
                ));
                break;
            case 'text':
                $output = "position={$this->_position}&keyword=" . rawurldecode($this->_keyword) .
                          "&page={$this->_page}";
            break;
        }
        return $this->_output = $output;
    }
   
    private function _setApiUrl($url=null) {
        if (!$this->_keyword) {
            return false;
        }
        $this->_apiUrl = sprintf($this->_apiUrl, $this->_keyword);
        return true;
    }
   
    private function _getNextResultSet() {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->_apiUrl . "&start={$this->_offset}");
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_REFERER, $this->_referer);
        $body = curl_exec($ch);
        curl_close($ch);
        $this->_results = json_decode($body);
    }
   
    private function _checkPosition() {
        foreach ($this->_results->responseData->results as $key => $result) {
            if (preg_match("#^{$this->_referer}.*{$this->_page}$#", $result->unescapedUrl)) {
                $this->_position = $this->_offset + 1;
                return;
            } else {
                $this->_offset++;
            }
        }
    }
   
}

?>

Usage

Usage is pretty simple. You either need to edit the default value of the $_referer property, or else you can use the setReferer() method. It is important to correctly set the referrer to your own site. Not only does Google require this, the class also uses this value to differentiate between your about.php and someone else’s about.php when examining results.

1
2
3
$posFinder = new GPositionFinder('my keyword phrase', 'mypage.htm');
$posFinder->setReferer('http://www.mysite.com');
echo $posFinder->go();

As you can see from the previous example, keyword and page can be passed in to the constructor when an instance of the class is created, but they can also be set later using the setKeyword() and setPage() methods:

1
2
3
4
$posFinder = new GPositionFinder;
$posFinder->setKeyword('my keyword phrase');
$posFinder->setPage('mypage.htm');
echo $posFinder->go();

If you are going to use this method though, be sure to set both a keyword and page before calling go() or else you will get a fatal error.

The class can output results in a few different ways. By default it will output a string deleimited in the same way as a URL query string, containing the position of the page, the keyword used and the page used. These last two components may be useful in AJAX implementations as they save you having to keep track of these values on the client side. Here’s an example of the default output:

1
2
3
$posFinder = new GPositionFinder('keyword', 'mypage.htm');
echo $posFinder->go();
// outputs position=1&keyword=keyword&page=mypage.htm

Another option is to have the output returned as a JSON encoded string:

1
2
3
4
$posFinder = new GPositionFinder('keyword', 'mypage.htm');
$posFinder->setOutputMethod('json');
echo $posFinder->go();
// outputs {"position":1,"keyword":"keyword","page":"mypage.htm"}

As you can see the go() method returns the output itself, but if you can also access the output at any time via the getOutput() method.

1
2
3
4
$posFinder = new GPositionFinder('keyword', 'mypage.htm');
$posFinder->go();
// some other code
echo $posFinder->getOutput();

You may also want to access the position you achieved directly without using an output string:

1
2
3
4
$posFinder = new GPositionFinder('keyword', 'mypage.htm');
$posFinder->go();
echo $posFinder->getPosition();
// outputs 1 (hopefully!)

Limitations

The class will only examine the top 40 results. I believe Google limits the number of results to 64, and for my own purposes, anything not in the top 40 results (the top 4 pages of a default google search) is not really worth worrying about anyway. You can of course extend this, but be sure to keep it less than the Google imposed limit or else the class may break (I haven’t tried it).

Another thing to bear in mind is that this class will only search google.com. This is due to the nature of the API, and as far as I know, nothing can be done to search local Google sites. If anyone knows differently please let me know as google.co.uk results would be useful to me!

Another limitation to consider is that this class will only work in PHP >= 5.2.0. This is due to the need for the json_decode function as the API returns results in JSON format. It could be rewritten to work on earlier versions of PHP4 using the PEAR Services_JSON package, and defining the following functions at the top of the class file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
include("JSON.php");

if( !function_exists('json_encode') ) {
    function json_encode($data) {
        $json = new Services_JSON();
        return $json->encode($data);
    }
}

if( !function_exists('json_decode') ) {
    function json_decode($data) {
        $json = new Services_JSON();
        return $json->decode($data);
    }
}

This will allow the code to function, and if the server is ever upgraded the PEAR package will automatically become redundant in favour of the native PHP versions of json_encode and json_decode.

Finally bear in mind this class was written to solve a specific need I had. It may not be ideal for you, and it may be lacking features you like. If you make any improvements to it (or find any bugs!) please let me know.