Getting the IP address of a user seems like a pretty trivial task but you can’t always rely on $_SERVER['REMOTE_ADDR']. The super global value is the most reliable source because it is extracted directly from the TCP stack but if you’re behind a load balancer that address would be that of the load balancer itself, not the client. To help alleviate the issue we can use a function that checks multiple super global values for the address and return it accordingly.

public static function getClientIP()
{
	if (!empty($_SERVER['HTTP_CLIENT_IP']))
	{
		$ip = $_SERVER['HTTP_CLIENT_IP'];
	}
	elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
	{
		$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
	}
	elseif (!empty($_SERVER['HTTP_X_FORWARDED']))
	{
		$ip = $_SERVER['HTTP_X_FORWARDED'];
	}
	elseif (!empty($_SERVER['HTTP_FORWARDED_FOR']))
	{
		$ip = $_SERVER['HTTP_FORWARDED_FOR'];
	}
	elseif (!empty($_SERVER['HTTP_FORWARDED']))
	{
		$ip = $_SERVER['HTTP_FORWARDED'];
	}
	else if (!empty($_SERVER['REMOTE_ADDR']))
	{
		$ip = $_SERVER['REMOTE_ADDR'];
	}
	else
	{
		$ip = false;
	}

	return $ip;
}

This is a good starting point but depending on your configuration you may want to omit some of the values based on the usefulness of the values they contain. Different load balancers will set different server variables and some may still contain the IP of the load balancer.

You could very well use isset() instead of !empty() but you would still need to check if the variable is an empty string. The empty() function does not throw an error if the variable you pass in is undefined, so this function will not throw any errors if you’re using E_STRICT mode.

Amendment: My buddy of mine pointed out to me that there are scenarios where multiple (comma separated) IP addresses are returned. This seems to be a scenario that comes up when the forwarded for addresses is used (but not always). I felt like the code snippet should remain intact so that you can decide how you want to proceed. Perhaps you want the comma separated list, perhaps you want all of the addresses returned as an array, Dave suggested exploding on the comma and using only the first one returned. Personally, I would add an argument to the aforementioned code snippet that allows you to toggle any of those return types. That’s really your call as the developer as to how you would want to proceed, but please keep in mind that the scenario exists.