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 can only be shown in an iframe of the same site.
header('X-Frame-Options: SAMEORIGIN'); // FF 3.6.9+ Chrome 4.1+ IE 8+ Safari 4+ Opera 10.5+

Users working with an up-to-date browser 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. more…

The implementation in PHP is very straightforward, though some problems may arise from inline JavaScript. The most protection you get, if you avoid all JavaScript inside the HTML files, and always put it to separate *.js files. If this cannot be done (because of existing source code), there is an option to allow inline-script.

// Adds the Content-Security-Policy to the HTTP header.
// JavaScript will be restricted to the same domain as the page itself.
header("Content-Security-Policy: default-src 'self'; script-src 'self';"); // FF 23+ Chrome 25+ Safari 7+ Opera 19+
header("X-Content-Security-Policy: default-src 'self'; script-src 'self';"); // IE 10+

If your site serves over HTTPS only (SSL for all pages), then it is a good idea to send the Strict-Transport-Security header. The first time a user visits your site, the browser will store this header. If the user later visits your site again, maybe using an unsafe WLAN connection, the browser remembers to call it exclusively with HTTPS. This would then protect from SSL-strip.

// Adds the HTTP Strict Transport Security (HSTS) (remember it for 1 year)
$isHttps = !empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off';
if ($isHttps)
{
  header('Strict-Transport-Security: max-age=31536000'); // FF 4 Chrome 4.0.211 Opera 12
}

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 its 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);

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