English Deutsch

PHP tips

I'm an enthusiastic participant of the stackoverflow platform, a Question & Answer site for programmers. From time to time i stumble over an interesting problem (at least for me), then i try to solve the problem and publish a small article of the solution here on this page.

If you should have problems, questions or suggestions about the functions below, or if you simply find them useful, don't hesitate to send me an email to .

Overview


Using X-Frame-Options and Content-Security-Policy with PHP

Most browsers today will help protecting your site from malicious attacks, but you have to tell them they should. A widely supported method is setting the X-Frame-Options. Setting this option, the browser will not allow other sites to display your page inside an iframe. This protects against Clickjacking attacks and should be used on all sensitive pages like the login page.

// Adds X-Frame-Options to HTTP header, so that page cannot be shown in an iframe.
header('X-Frame-Options: DENY');

// Adds X-Frame-Options to HTTP header, so that page can only be shown in an iframe of the same site.
header('X-Frame-Options: SAMEORIGIN');

People using Firefox 4 and later, will benefit automatically, when a website sends a Content-Security-Policy (CSP) within the HTTP header. With a CSP you can specify from which locations you accept javascript, which sites are allowed to show your page inside an iframe and many other things. If a browser supports CSP, this can be an effective protection against Cross-Site-Scripting.

The implementation in PHP is incredible easy, though some problems may arise from inline JavaScript.

// Adds the Content-Security-Policy to the HTTP header.
// JavaScript will be restricted to the same domain as the page itself.
header("X-Content-Security-Policy: allow 'self'");

// Adds the Content-Security-Policy to the HTTP header.
// JavaScript will be restricted to the same domain as the page itself, but
// is allowed inside the HTML page (no separate *.js file required).
header("X-Content-Security-Policy: allow 'self'; options inline-script");

Generating password hashes with bcrypt

PHP 5.5 will have it's own functions password_hash() and password_verify() ready, to simplify generating BCrypt password hashes. I strongly recommend to use this excellent api, or it's compatibility pack for earlier PHP versions. The usage is very straightforward:

// Hash a new password for storing in the database.
// The function automatically generates a cryptographically safe salt.
$hashToStoreInDb = password_hash($password, PASSWORD_BCRYPT);

// Check if the hash of the entered login password, matches the stored hash.
// The salt and the cost factor will be extracted from $existingHashFromDb.
$isPasswordCorrect = password_verify($password, $existingHashFromDb);

// This way you can define a cost factor (by default 10). Increasing the
// cost factor by 1, doubles the needed time to calculate the hash value.
$hashToStoreInDb = password_hash($password, PASSWORD_BCRYPT, array("cost" => 11));

This solves the task pretty well. If you still want to know more about PHP's crypt function, or how to add a pepper, then read on… There are well known best practices for (not) storing passwords in a database. I wrote an in-depth tutorial about hashing passwords, a short overview could look like that:

The bcrypt hash algorithm was especially designed to meet this demands. It has a cost parameter, that controls the necessary time for the calculation. This cost parameter will be stored together with the salt in the resulting hash. The following class helps building secure BCrypt hashes. It is provided with comments for educational purposes.

Download: StoPasswordHash.zip.

class StoPasswordHash
{
  /**
   * Generates a bcrypt hash of a password, which can be stored in a database.
   * @param string $password Password whose hash-value we need.
   * @param int $cost Controls the number of iterations. Increasing the cost
   *   by 1, doubles the needed calculation time. Must be in the range of 4-31.
   * @param string $serverSideKey This key acts similar to a pepper, but
   *   can be exchanged when necessary. In certain situations, encrypting
   *   the hash-value can protect weak passwords from a dictionary attack.
   * @return string Hash-value of the password. A random salt is included.
   *   Without passing a $serverSideKey the result has a length of 60
   *   characters, with a $serverSideKey the length is 108 characters.
   */
  public static function hashBcrypt($password, $cost=10, $serverSideKey='')
  {
    if (!defined('CRYPT_BLOWFISH')) throw new Exception('The CRYPT_BLOWFISH algorithm is required (PHP 5.3).');
    if (is_null($password) || $password === '') throw new InvalidArgumentException('Cannot hash an empty password.');
    if ($cost < 4 || $cost > 31) throw new InvalidArgumentException('The cost factor must be a number between 4 and 31');

    if (version_compare(PHP_VERSION, '5.3.7') >= 0)
      $algorithm = '2y'; // BCrypt, with fixed unicode problem
    else
      $algorithm = '2a'; // BCrypt

    // BCrypt expects nearly the same alphabet as base64_encode returns,
    // but instead of the '+' characters it accepts '.' characters.
    // BCrypt alphabet: ./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
    $salt = str_replace('+', '.', StoPasswordHash::generateRandomBase64String(22));

    // Create crypt parameters: $algorithm$cost$salt
    $cryptParams = sprintf('$%s$%02d$%s', $algorithm, $cost, $salt);
    $hash = crypt($password, $cryptParams);

    // Encrypt hash with the server side key
    if ($serverSideKey != '')
    {
      $encryptedHash = StoPasswordHash::encryptTwofish($hash, $serverSideKey);
      $hash = base64_encode($encryptedHash);
    }
    return $hash;
  }

  /**
   * Checks, if the password matches a given hash value. This is useful when
   * a user enters his password for login, to check if the password corresponds
   * to the hash stored in the database.
   * @param string $password Password to check.
   * @param string $existingHash Stored hash-value from the database.
   * @param string $serverSideKey Pass the same key that was used to encrypt
   *   $existingHash, or omit this parameter if no key was used.
   * @return bool Returns true, if the password matches the hash,
   *   otherwise false.
   */
  public static function verifyPassword($password, $existingHash, $serverSideKey='')
  {
    if (!defined('CRYPT_BLOWFISH')) throw new Exception('The CRYPT_BLOWFISH algorithm is required (PHP 5.3).');
    if (is_null($password) || $password === '') return false;

    // Decrypt hash with the server side key
    if ($serverSideKey != '')
    {
      $encryptedHash = base64_decode($existingHash);
      $existingHash = StoPasswordHash::decryptTwofish($encryptedHash, $serverSideKey);
    }

    // The parameters that where used to generate $existingHash, will be
    // extracted automatically from the first 29 characters of $existingHash.
    $newHash = crypt($password, $existingHash);
    return $newHash === $existingHash;
  }

  /**
   * Generates a random string of a given length, using the random source of
   * the operating system. The string contains only characters of this
   * alphabet: +/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
   * @param int $length Number of characters the string should have.
   * @return string A random base64 encoded string.
   */
  protected static function generateRandomBase64String($length)
  {
    if (!defined('MCRYPT_DEV_URANDOM')) throw new Exception('The MCRYPT_DEV_URANDOM source is required (PHP 5.3).');

    // Generate random bytes, using the operating system's random source.
    // Since PHP 5.3 this also uses the random source on a Windows server.
    // Unlike /dev/random, the /dev/urandom does not block the server, if
    // there is not enough entropy available.
    $binaryLength = (int)($length * 3 / 4 + 1);
    $randomBinaryString = mcrypt_create_iv($binaryLength, MCRYPT_DEV_URANDOM);
    $randomBase64String = base64_encode($randomBinaryString);
    return substr($randomBase64String, 0, $length);
  }

  /**
   * Encrypts data with the TWOFISH algorithm. The IV vector will be
   * included in the resulting binary string.
   * @param string $data Data to encrypt. Trailing \0 characters will get lost.
   * @param string $key This key will be used to encrypt the data. The key
   *   will be hashed to a binary representation before it is used.
   * @return string Returns the encrypted data in form of a binary string.
   */
  public static function encryptTwofish($data, $key)
  {
    if (!defined('MCRYPT_DEV_URANDOM')) throw new Exception('The MCRYPT_DEV_URANDOM source is required (PHP 5.3).');
    if (!defined('MCRYPT_TWOFISH')) throw new Exception('The MCRYPT_TWOFISH algorithm is required (PHP 5.3).');

    // The cbc mode is preferable over the ecb mode
    $td = mcrypt_module_open(MCRYPT_TWOFISH, '', MCRYPT_MODE_CBC, '');

    // Twofish accepts a key of 32 bytes. Because usually longer strings
    // with only readable characters are passed, we build a binary string.
    $binaryKey = hash('sha256', $key, true);

    // Create initialization vector of 16 bytes
    $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_DEV_URANDOM);

    mcrypt_generic_init($td, $binaryKey, $iv);
    $encryptedData = mcrypt_generic($td, $data);
    mcrypt_generic_deinit($td);
    mcrypt_module_close($td);

    // Combine iv and encrypted text
    return $iv . $encryptedData;
  }

  /**
   * Decrypts data, formerly encrypted with @see encryptTwofish.
   * @param string $encryptedData Binary string with encrypted data.
   * @param string $key This key will be used to decrypt the data.
   * @return string Returns the original decrypted data.
   */
  public static function decryptTwofish($encryptedData, $key)
  {
    if (!defined('MCRYPT_TWOFISH')) throw new Exception('The MCRYPT_TWOFISH algorithm is required (PHP 5.3).');

    $td = mcrypt_module_open(MCRYPT_TWOFISH, '', MCRYPT_MODE_CBC, '');

    // Extract initialization vector from encrypted data
    $ivSize = mcrypt_enc_get_iv_size($td);
    $iv = substr($encryptedData, 0, $ivSize);
    $encryptedData = substr($encryptedData, $ivSize);

    $binaryKey = hash('sha256', $key, true);

    mcrypt_generic_init($td, $binaryKey, $iv);
    $decryptedData = mdecrypt_generic($td, $encryptedData);
    mcrypt_generic_deinit($td);
    mcrypt_module_close($td);

    // Original data was padded with 0-characters to block-size
    return rtrim($decryptedData, "\0");
  }
}

Secure password-reset function

In the article above, we saw how to store passwords safely, but this immediately leads to the next problem, the password-reset function. The best password hash function is worthless, if we do not handle the password-reset with the same care, as storing the password itself.

The usual way is, to send an email with a one time token to the registered user. The token will be stored in the database and when the user clicks the link, we check the token and allow the user to set a new password.

Now imagine an attacker can read the database table with the tokens through SQL-injection. He could then demand a password reset for any e-mail address he likes, and because he can see the new token, he could use it to set his own password. An ideal password-reset function should fulfill all of these requirements:

The following class StoPasswordReset helps generating such reset-links. The example code below shows, that several steps are necessary to handle the password-reset safely. Actually it should be safe enough to use a non iterated hash without a salt, but because somebody may decide to use a shorter token length, i stayed with BCrypt.

Download: StoPasswordReset.zip.

https://www.example.com/requestpasswordreset.php

// First we generate a new random token
StoPasswordReset::generateNewToken($tokenForLink, $hashedTokenForDatabase);

// The hash of the token can now be stored in the database. Uniqueness
// is not required but we need the id of the row, where the hash was stored.
// Other information like an expiry date can be stored together with the hash.
$databaseRowId = storeTokenInDatabase($hashedTokenForDatabase);

// Now we can build the link code, e.g. 'EK-1Ko2vvdhK896yc6Qdqw1Y6xn'.
$linkCode = StoPasswordReset::buildLinkCode($databaseRowId, $tokenForLink);

// Create a link and send it via email to the user
$passwordResetUrl = sprintf("http://www.example.com/resetpassword.php?code=%s", $linkCode);
sendEmailWithPasswordResetLink($passwordResetUrl);
https://www.example.com/resetpassword.php

// As soon as the user clicked the url, he will land on this page.
// First we get the link code from the $GET parameters.
$linkCode = $_REQUEST['code'];

// Next we parse and validate the link code.
$isValidToken = StoPasswordReset::parseLinkCode($linkCode, $databaseRowId, $tokenFromLink);
if (!$isValidToken)
  handleErrorAndExit('The link code is invalid.');

// With the row id, we can get the stored hash from the database.
// If we have stored an expiry date, we can check it here.
$hashedTokenFromDatabase = getHashedTokenFromDatabase($databaseRowId);
if (empty($hashedTokenFromDatabase))
  handleErrorAndExit('The token does not exist in the database');

// Finally we check, whether the link token matches the stored hash.
$isTokenCorrect = StoPasswordReset::verifyToken($tokenFromLink, $hashedTokenFromDatabase);
if (!$isValidToken)
  handleErrorAndExit('The token is incorrect.');

// Show password change form. After successfully setting a new
// password, mark the token as used.

To come to the point, every website switching between unsecure HTTP and encrypted HTTPS pages, is inevitable prone to SSL-strip. A secure HTTPS connection remains untouched with this attack, though the unaware user will be tricked to work with an HTTP connection, when he thinks to use an HTTPS connection.

Because one cannot expect users to be able to recognize an SSL-strip attack, one should absolutely think about using HTTPS for the whole site. Although this neither can prevent SSL-strip in every case, it helps considerably. Because the following concept can have advantages for HTTPS-only sites too, i decided to keep the article.

The problem with the session-id

For every request of a page, a session-id has to be sent along, that allows the server to recognize the user. The session-id should be stored in a cookie, because passing it along the URL makes session-fixation much to easy. In the session on the server resides the information, whether the user is already logged in or not. The problem now is, that an attacker that finds out this session-id (however he does), can impersonate the user, and therefore has the same priviledges as the user.

To exchange sensitive data, we absolutely need an HTTPS connection with SSL encryption. This makes sure, that nobody between client and server can eavesdrop our communication and prevents a man-in-the-middle attack. Websites which are switching betweed HTTP and HTTPS pages, have now to decide whether they:

  1. send the session-cookie to HTTP and HTTPS pages, and thereby transmit the session-id unprotected as soon as they request a HTTP page (even for requests of pictures).
  2. or configure the session-cookie, so it will be sent exclusively to HTTPS pages, and thereby loose the session, as soon as a HTTP page is shown.

With option 1 we can stop the discussion right now, there won't exist something like security afterwards. Option 2 could be handled, using HTTPS only for the whole site. As already mentioned, this should really be done, todays servers shouldn't have any problems with it. In PHP you could then call the function session_set_cookie_params(...) and set the parameter $secure to true.

The authentication cookie

The idea of the authentication cookie is, to create a second cookie in addition to the session cookie, as soon as the user increases his privileges (login). This second cookie is configured in such a way, that it will be sent back exclusively to HTTPS pages. Of course the login page itself has to use HTTPS.

https://www.example.com/login.php

<?php
  session_start();
  // regenerate session id to make session fixation more difficult
  session_regenerate_id(true);

  // generate random code for the authentication cookie and store it in the session
  $authCode = md5(uniqid(mt_rand(), true));
  $_SESSION['authentication'] = $authCode;

  // create authentication cookie, and restrict it to HTTPS pages
  setcookie('authentication', $authCode, 0, '/', '', true, true);

  print('<h1>login</h1>');
  ...
?>

Now every page (HTTPS and HTTP) can use the unsecure session-cookie, it's purpose is merely to maintain the session. However, all pages with sensitive information can check for the secure authentication cookie.

https://www.example.com/secret.php

<?php
  session_start();

  // check that the authentication cookie exists, and that
  // it contains the same code which is stored in the session.
  $pageIsSecure = (!empty($_COOKIE['authentication']))
    && ($_COOKIE['authentication'] === $_SESSION['authentication']);

  if (!$pageIsSecure)
  {
    // do not display the page, redirect to the login page
  }

  ...
?>

An attacker could manipulate the session cookie, but he never has access to the authentication cookie, which is responsible for the authentication. Only the person who entered the password, can own the authentication cookie, it is sent exclusively over encrypted HTTPS connections.

In separating the two concerns "maintaining the PHP session" and "authentication", we can make the system a bit more robust. There are many ways to attack the session-cookie (server settings, php.ini, .htaccess, php code, browser settings, id in the url, ...), with the separation such attacks are bound to fail.


UTF-8 for PHP and MySQL

Different character encodings can cause headaches, that's something every developer who needs to make localized software knows for sure. Maybe your page shows UTF-8, where as the database delivers iso-8859-1, then you get these odd hieroglyphics, or even worse the user can possibly not even login anymore.

That's why Unicode was developed. I can't go into the details of Unicode here, but the goal is to represent the characters of all known languages, and other symbols as well (see this font character map). One of the most commonly used encodings for Unicode is UTF-8, because it is very compact (only 1 byte for common characters) and is understood by all todays web browsers.

UTF-8 in a PHP page

First the HTML/PHP page itself should be stored in the UTF-8 file format. That means you need an editor which supports Unicode, fortunately most IDE's are able to do this. Normal characters are then stored with 1 byte, special characters need 3-4 bytes, but the editor displays the typed-in character. That means, no HTML-entities like &Auml; anymore(!), what you see is what you typed.

You should care that the editor does not store the BOM header, this header is sometimes stored at the begin of the file with 3 bytes . The editor will hide them, so if you are not sure if your file contains these characters, you can either use a non interpreting editor (hex editor), or this wonderful online W3C checker. The BOM header is treated as output by PHP, and this can cause nasty Cannot modify header information - headers already sent errors.

Then you should add the encoding declaration to the top of the head element of your HTML/PHP page, right after the opening <head> tag.

HTML 4:  <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
XHTML:   <meta http-equiv="Content-type" content="text/html;charset=UTF-8" />
HTML 5:  <meta charset="UTF-8">
see more…

UTF-8 in MySQL

There is a simple way to tell the database it should deliver UTF-8 encoded strings, so they can be used in an UTF-8 web page. Instead of fiddling with the configurations of MySQL, just tell your connection object, which character-set you expect, the database does the rest for you.

Queries will automatically return UTF-8 encoded strings, ajax results can be used without cumbersome conversions, and other applications can request different encodings if necessary.

// tells the mysqli connection to deliver UTF-8 encoded strings.
$db = new mysqli($dbHost, $dbUser, $dbPassword, $dbName);
$db->set_charset('utf8');

// tells the pdo connection to deliver UTF-8 encoded strings.
$dsn = "mysql:host=$dbHost;dbname=$dbName;charset=utf8";
$db = new PDO($dsn, $dbUser, $dbPassword);

// tells the mysql connection to deliver UTF-8 encoded strings.
$db = mysql_connect($dbHost, $dbUser, $dbPassword);
mysql_set_charset('utf8', $db);

To get more information about the charset of your database, you can make a query like that:

SHOW VARIABLES LIKE "character%"

Equal or not equal

What i'm missing most in PHP, is the benefit of a strong typed language. Dynamic typing may have it's advantages, but would you have thought following comparisons will give back true? PHP makes it possible...

Of course you can use the === operator, to check values and their types. Since PHP doesn't support you well with controlling types explicitly, i found it to be of no much use. That was the point when i started writing a class covering all the things i wished to be built-in in the PHP language.

/**
 * Checks if two values are equal. In contrast to the == operator,
 * the values are considered different, if:
 * - one value is null and the other not, or
 * - one value is an empty string and the other not
 * This helps avoid strange behavier with PHP's type juggling,
 * all these expressions would return true:
 * 'abc' == 0; 0 == null; '' == null; 1 == '1y?z';
 * @param mixed $value1
 * @param mixed $value2
 * @return boolean True if values are equal, otherwise false.
 */
function sto_equals($value1, $value2)
{
  // identical in value and type
  if ($value1 === $value2)
    $result = true;
  // one is null, the other not
  else if (is_null($value1) || is_null($value2))
    $result = false;
  // one is an empty string, the other not
  else if (($value1 === '') || ($value2 === ''))
    $result = false;
  // identical in value and different in type
  else
  {
    $result = ($value1 == $value2);
    // test for wrong implicit string conversion, when comparing a
    // string with a numeric type. only accept valid numeric strings.
    if ($result)
    {
      $isNumericType1 = is_int($value1) || is_float($value1);
      $isNumericType2 = is_int($value2) || is_float($value2);
      $isStringType1 = is_string($value1);
      $isStringType2 = is_string($value2);
      if ($isNumericType1 && $isStringType2)
        $result = is_numeric($value2);
      else if ($isNumericType2 && $isStringType1)
        $result = is_numeric($value1);
    }
  }
  return $result;
}

Avoid functions with mixed-typed return values

Unfortunately it's a common practice in PHP, that functions return different types, depending on whether the function was successful or not.

// This kind of mixed-typed return value (boolean or string),
// can lead to unreliable code!
function precariousCheckEmail($input)
{
  $isValid = filter_var($input, FILTER_VALIDATE_EMAIL);
  if ($isValid)
    return true;
  else
    return 'E-Mail address is invalid.';
}

At first glance, this looks even convenient, but it's easier to get a nasty bug, than to call this function correctly like this:

$result = precariousCheckEmail('nonsense');
if ($result === true)
  print('OK');
else
  print($result); // -> message will be given out

So where's the problem? Everybody using this function needs previous knowledge, that he can only get by looking at the code or at the (good) documentation.

// All this checks will wrongly accept the email as valid!
$result = precariousCheckEmail('nonsense');
if ($result == true)
  print('OK'); // -> OK will be given out

if ($result)
  print('OK'); // -> OK will be given out

if ($result === false)
  print($result);
else
  print('OK'); // -> OK will be given out

if ($result == false)
  print($result);
else
  print('OK'); // -> OK will be given out

Instead of just telling what is bad, i would like to give a better alternative as well. The example below passes an additional parameter by reference. The calling code is very readable and it's nearly impossible to use it wrong.

// This function with a return value (boolean) and a
// parameter passed by-reference (string) is robust.
function robustCheckEmail($input, &$errorMessage)
{
  $isValid = filter_var($input, FILTER_VALIDATE_EMAIL);
  $errorMessage = '';
  if (!$isValid)
    $errorMessage = 'E-Mail address is invalid.';
  return $isValid;
}

if (robustCheckEmail('nonsense', $error))
  print('OK');
else
  print($error);

Every-Flavour-Arrays

An array is an array, is a dictionary, is a list, is an all-in-one solution for every problem. Personally i prefer to have classes that tell me what they are intented to do, and do it well. It's easier to understand the code, if you can tell, whether the PHP array is used as a dictionary or as a list.

With the class below you can use a PHP array like a list.

/**
 * Represents an index based list.
 * Like a PHP array, this list can be used in a foreach loop.
 */
class StoList implements IteratorAggregate, Countable
{
  protected $items = array();

  /**
   * Gets back the item at the given position.
   * @param int $index Index of the required item.
   * @return mixed Element at the given position, or null if no such
   *   element exists.
   */
  public function getItem($index)
  {
    if (($index >= 0) && ($index < $this->count()))
      return $this->items[$index];
    else
      return null;
  }

  /**
   * Gets the number of items in the list.
   * @return int Number of items.
   */
  public function count()
  {
    return count($this->items);
  }

  /**
   * Adds an element to the list.
   * @param mixed $item Element to add to the list.
   */
  public function add($item)
  {
    $this->items[] = $item;
  }

  /**
   * Removes the element at the specified index of the list.
   * @param int $index The zero-based index of the element to remove.
   */
  public function removeAt($index)
  {
    if (($index >= 0) && ($index < $this->count()))
      array_splice($this->items, $index, 1);
  }

  /**
   * Removes all elements from the list.
   */
  public function clear()
  {
    $this->items = array();
  }

  /**
   * Sorts the elements in the list using the given compare function.
   * Example: <code>$list->sort('strcasecmp');</code>
   * @param func $compareFunction Name of the function to use for
   *   comparing two elements. This can be a PHP function or a user
   *   defined function, which is accepted by the PHP function usort().
   */
  public function sort($compareFunction)
  {
    usort($this->items, $compareFunction);
  }

  /**
   * Implements the IteratorAggregate interface used by foreach loops.
   * @return Traversable An array iterator.
   */
  public function getIterator()
  {
    return new ArrayIterator($this->items);
  }
}

With the class below you can use a PHP array like a dictionary.

/**
 * Encapsulates the access to a PHP array, when used as a dictionary
 * with key-value pairs.
 */
class StoDictionary
{
  protected $items = array();

  /**
   * Initializes a new instance of the StoDictionary class.
   * It will be initialized with the values of $array.
   * Example: <code>$dictionary = StoDictionary::createFromArray($_POST);</code>
   * Modifications to the dictionary will not modify the original array.
   * @param array $array An array with key-value pairs.
   * @return StoDictionary New created dictionary object.
   */
  public static function createFromArray($array)
  {
    $result = new StoDictionary();
    $result->items = $array;
    return $result;
  }

  /**
   * Adds or replaces a key-value pair.
   * @param mixed $key The key of the value to add or replace.
   * @param mixed $value Value that will be associated with the
   *   specified key. If the key already exists, the value will be
   *   overwritten. A value of null will delete the key.
   */
  public function setValue($key, $value)
  {
    if ($value === null)
      unset($this->items[$key]);
    else
      $this->items[$key] = $value;
  }

  /**
   * Gets the value associated with the specified key.
   * @param mixed $key The key of the value to get.
   * @param mixed $default The default value to return, if the
   *   specified key does not exist.
   * @return mixed Value that is associated with the specified
   *   key, or the default value, if no such key exists.
   */
  public function getValue($key, $default = null)
  {
    $containsKey = isset($this->items[$key]);
    if ($containsKey)
      return $this->items[$key];
    else
      return $default;
  }

  /**
   * Gets the value associated with the specified key.
   * @param mixed $key The key of the value to get.
   * @param mixed $value Retrieves the found value, or is set to null
   *   if the key could not be found.
   * @return bool Returns true if the key could be found, otherwise false.
   */
  public function tryGetValue($key, &$value)
  {
    $containsKey = isset($this->items[$key]);
    if ($containsKey)
      $value = $this->items[$key];
    else
      $value = null;
    return $containsKey;
  }

  /**
   * Determines whether the dictionary contains the specified key.
   * @param mixed $key The key to locate in the dictionary.
   * @return bool Returns true if key exists, otherwise false.
   */
  public function containsKey($key)
  {
    return isset($this->items[$key]);
  }
}

Calculating distance between points on earth

To calculate the spheric distance between two points on the earth (great-circle distance), one can use the Haversine formula. This formula is stable for calculating small distances regarding rounding errors.

/**
 * Calculates the great-circle distance between two points, with
 * the Haversine formula.
 * @param float $latitudeFrom Latitude of start point in [deg decimal]
 * @param float $longitudeFrom Longitude of start point in [deg decimal]
 * @param float $latitudeTo Latitude of target point in [deg decimal]
 * @param float $longitudeTo Longitude of target point in [deg decimal]
 * @param float $earthRadius Mean earth radius in [m]
 * @return float Distance between points in [m] (same as earthRadius)
 */
function haversineGreatCircleDistance(
  $latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo, $earthRadius = 6371000)
{
  // convert from degrees to radians
  $latFrom = deg2rad($latitudeFrom);
  $lonFrom = deg2rad($longitudeFrom);
  $latTo = deg2rad($latitudeTo);
  $lonTo = deg2rad($longitudeTo);

  $latDelta = $latTo - $latFrom;
  $lonDelta = $lonTo - $lonFrom;

  $angle = 2 * asin(sqrt(pow(sin($latDelta / 2), 2) +
    cos($latFrom) * cos($latTo) * pow(sin($lonDelta / 2), 2)));
  return $angle * $earthRadius;
}

An alternative to the haversine formula is the vincenty formula, it is slightly more complex, but does not suffer from the weakness with antipodal points (rounding errors).

/**
 * Calculates the great-circle distance between two points, with
 * the Vincenty formula.
 * @param float $latitudeFrom Latitude of start point in [deg decimal]
 * @param float $longitudeFrom Longitude of start point in [deg decimal]
 * @param float $latitudeTo Latitude of target point in [deg decimal]
 * @param float $longitudeTo Longitude of target point in [deg decimal]
 * @param float $earthRadius Mean earth radius in [m]
 * @return float Distance between points in [m] (same as earthRadius)
 */
function vincentyGreatCircleDistance(
  $latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo, $earthRadius = 6371000)
{
  // convert from degrees to radians
  $latFrom = deg2rad($latitudeFrom);
  $lonFrom = deg2rad($longitudeFrom);
  $latTo = deg2rad($latitudeTo);
  $lonTo = deg2rad($longitudeTo);

  $lonDelta = $lonTo - $lonFrom;
  $a = pow(cos($latTo) * sin($lonDelta), 2) +
    pow(cos($latFrom) * sin($latTo) - sin($latFrom) * cos($latTo) * cos($lonDelta), 2);
  $b = sin($latFrom) * sin($latTo) + cos($latFrom) * cos($latTo) * cos($lonDelta);

  $angle = atan2(sqrt($a), $b);
  return $angle * $earthRadius;
}

www.martinstoeckli.ch