Create a PHP-MySQL zip code radius search

If you’re looking to create a zip code radius search utility for your website, it’s much easier than it seems. I’ve based this tutorial off of http://zips.sourceforge.net/ which is not currently maintained, but has a good majority of US zip codes to test our concept.

Setup

To setup the schema for this tutorial, download the MySQL dump here and import it into your database. Once you import the dump, you will see a table called zip_codes with the following columns:

  • zip_codes.zip – US zip code
  • zip_codes.state – State abbreviation
  • zip_codes.longitute
  • zip_codes.latitude
  • zip_codes.full_state – Full state name

Here is a utility class that I created to do all of the search operations.

<?php
/**
 * Zip Code Utility Class
 */
class ZipCodeUtility
{
	/**
	 * MySQLi Object
	 * @var object
	 */
	private $db;

	/**
	 * Constructor
	 */
	public function __construct ( )
	{
		$this->db = new mysqli( 'host', 'user', 'pass', 'database' );
	}

	/**
	 * Zip Radius Search
	 * @param  string  $zip_code US zip code
	 * @param  integer $radius   Radius in miles
	 * @return array|bool   flat array of zip codes or bool false
	 */
	public function search_radius ( $zip_code, $radius = 20 )
	{
		$zip = $this->get_zipcode( $zip_code );
		if ( $zip ) {
			$zip_codes = $this->get_zipcodes_by_radius( $zip, $radius );
		}
		return $zip_codes ? $zip_codes : false;
	}

	/**
	 * Get Zip Code Object
	 * @param  string $zipcode US zip code
	 * @return object|bool   Zip code object or bool false
	 */
	protected function get_zipcode ( $zipcode )
	{
		$sql = "SELECT * FROM zip_codes WHERE zip = $zipcode LIMIT 1";
		if ( $result = $this->db->query( $sql ) ) {

		    while ( $obj = $result->fetch_object() )
		    {
		    	$result->close();
		        return $obj;
		    }
		    $result->close();
		}
		return false;
	}

	/**
	 * Get Zipcode By Radius
	 * @param  string $zip    US zip code
	 * @param  ing $radius    Radius in miles
	 * @return array         List of nearby zipcodes
	 */
	protected function get_zipcodes_by_radius ( $zip, $radius )
	{
	    $sql = 'SELECT distinct(zip) FROM zip_codes WHERE (3958*3.1415926*sqrt((latitude-'.$zip->latitude.')*(latitude-'.$zip->latitude.') + cos(latitude/57.29578)*cos('.$zip->latitude.'/57.29578)*(longitude-'.$zip->longitude.')*(longitude-'.$zip->longitude.'))/180) <= '.$radius.';';
	    $zip_codes = array();
	    if ( $result = $this->db->query( $sql ) )
	    {
		    while( $row = $result->fetch_object() )
		    {
		        array_push( $zip_codes, $row->zip );
		    }
		    $result->close();
		}
	    return $zip_codes;
	}
}

Usage

I created a simple JSON endpoint where I could pass in a zip code and see if it was in the radius area of an existing service location zip. For instance, if you have a septic pumping service, you can only service homes and business within a twenty mile radius from each location. Here is a sample of how to implement the utility class.

$service_areas = array(
	'32205',
	'64088',
	'32210',
	'10001'
);

# Set endpoint state which can be overriden
$serviceable = false;
$error = false;
$data = array();

# Get zip from query string
$zip = isset( $_GET['zip']) && $_GET['zip'] ? $_GET['zip'] : false;

if( $zip )
{
	# Check format
	if ( preg_match( '/[0-9]{5}/', $zip ) )
	{
		# Create new instance
		$zip_search = new ZipCodeUtility();

		# Nearby zip codes within radius of 25 miles
		$zip_codes = $zip_search->search_radius( $zip, 25 );

		if( $zip_codes )
		{
			# Data for JSON endpoint
			$data['zip_codes'] = $zip_codes;

			# Set false message to be overriden if success found
			$message = "$zip is not serviceable";

			foreach( $zip_codes as $zip_code )
			{
				# Check if one of our service areas are in the returned list
				if( in_array( $zip_code, $zip_codes ) )
				{
					# If so, we don't have to go any further, override state and message
					$serviceable = true;
					$message = "$zip is serviceable";
				}
			}
		}
		else
		{
			$message = "No nearby zip codes for $zip";
		}
	}
	else
	{
		# Formatting error!
		$error = true;
		$message = '`zip` parameter must be a five digit US zip code';
	}
}
else
{
	# Missing parameter error!
	$error = true;
	$message = 'Missing paramater `zip`';
}

# Output JSON
header( 'Content-Type: application/json;charset=utf-8' );
echo json_encode(
	array(
		'serviceable' => $serviceable,
		'message'     => $message,
		'error'       => $error,
		'data'        => $data
	)
);
exit;

To implement on the front-end, simply use jQuery to consume the JSON endpoint.

jQuery.get( 'http://url/to/endpoint/?zip=' + zip_value, function( response )
{
	if( response.serviceable ){}
	else if( response.error ){}
	else{ // not serviceable }
} );

That’s it! You can probably see how easy you it is to implement a nearby search of other locations by querying against the returned list of nearby zip codes. From there, you could return the client a list of location objects. The sky is the limit…well within the US.

If you are putting this in production, I would purchase a recent zip code list from https://www.zip-codes.com/. You may have to change your queries a bit in the utility class to match their schema. Have fun!

Get file permissions, owner and group with PHP

Here are a few functions to help you when interacting with files in your php scripts:

Find the owner of a file:
An array of information about the owner is returned.

function foo_get_file_ownership($file){
	$stat = stat($file);
	if($stat){
		$group = posix_getgrgid($stat[5]);
		$user = posix_getpwuid($stat[4]);
		return compact('user', 'group');
	}
	else
		return false;
}

Get the four digit file permissions number:
A permissions string is returned. Example: 0755

function foo_get_file_perms($file){
	return substr(sprintf('%o', fileperms($file)), -4);
}

Continue…

Kint Debugger for WordPress

We just released a new UpThemes plugin called Kint Debugger for WordPress. It’s a simple wrapper for the awesome Kint debugger class. It allows you to view variable dumps in a styled and collapsible output. Kint also allows for easy back-tracing.

Examples

global $wp_query;
d($wp_query); //styled, collapsible output
s($wp_query); //un-styled output

I added a couple WordPress specific functions to aid in plugin and theme development.

dump_wp_query();
dump_wp();
dump_post();

View the entire documentation at the Kint site.

Check out the UpThemes plugin page.

Download Kint for WordPress

PHP – Passing data between classes

Static class properties are the most efficient way to pass information from one class to another. Typically, one would place a variable in the global scope for other functions and classes to use. However, this is not a good practice as setting GLOBALS equates to a downgrade in performance. Using static properties allows you to store/retrieve data without instantiating a class object.

class IntermediaryData{
    public static $global = null;

    public function set($data){	
        return self::$global = $data;
    }
	
    public function get(){
        return self::$global;
    }
}

Use this in your classes to set and retrieve data:

IntermediaryData::get();
IntermediaryData::set($data);

A good use case for this functionality is passing a rolling list of ids to exclude from queries from widget to widget within a dynamic WordPress sidebar.

Display hidden post types automatically in WP nav menus admin

One of the most annoying things about WP nav menu admin is when you create a custom post type, you first must go to Screen Options and check the custom post type checkbox to display it for use. Here’s a hack I created to get around this and force the custom post type to display for all users all the time.

function display_post_type_nav_box(){

    $hidden_nav_boxes = get_user_option( 'metaboxhidden_nav-menus' );

    $post_type = 'foobar'; //Can also be a taxonomy slug
    $post_type_nav_box = 'add-'.$post_type;

    if(in_array($post_type_nav_box, $hidden_nav_boxes)):
        foreach ($hidden_nav_boxes as $i => $nav_box):
            if($nav_box == $post_type_nav_box)
                unset($hidden_nav_boxes[$i]);
        endforeach;
        update_user_option(get_current_user_id(), 'metaboxhidden_nav-menus', $hidden_nav_boxes);
    endif;
}
add_action('admin_init', 'display_post_type_nav_box');