A (Very) Simple PHP Alphanumeric Filter Class

I needed to put one of these together quickly for a project at work recently and thought I would share.

The basic idea is very simple: An A to Z of links (plus options numbers) whose values we plug into the SQL query we're using to generate a list on the page, and return some data where the first letter = x.

Why bother putting this into a class? For one we make it easier to drop this code on any page we like, without duplicating code. Only need it in one place? Putting this code into a class makes it extensible, and much easier to test, maintain and amend; and of course, easier to drop into any other project.

The Class

First the class code.

Properties and Constructor 

class ANFilter
{
public static $searchTypes = array(
    0 => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
    1 => '#ABCDEFGHIJKLMNOPQRSTUVWXYZ', 
    2 => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ',
);

 
protected $searchType;
protected $fieldName; 
public $friendlyName;
public $style = 'plain';


public function __construct($fieldName, $friendlyName = null)
{
    $this->setfieldName($fieldName);
    $this->friendlyName = $friendlyName;
    $this->setSearchType();
}

Okay, firstly our properties. $searchTypes gives us a array of character sets we can select to determine which characters will appear in our filter on the page. This will default to "A - Z", but we can also a select to include "#" for any number, or  "0 - 9" explicitly. It depends how much space you have, or want to take up on the page.

Next a few properties instantiated as null, we'll get to these in a bit; and a $style property. This class will render in plain text, but I've added this as a future option.

The Constructor

Our constructor is very simple: parse in the name of the field in your SQL table that you'll be searching against, and optionally a more user-friendly name for this field. The $friendlyName value will be used in title tags on the links. So, for example, we could have a $fieldName of 'userLastName' and a $friendlyName of  'Surname'.

These values are set in the object, and then we call a setSearchType() method, we'll look at next:

Setters and Getters

We only have a couple of these, for our field name and list of search characters.


/**
 * set the list of searchable characters from those availabe in self::$searchTypes
 * @param int $searchType - index of self::$searchTypes to use
 *   defaults to first search list if not provided
 * @return AlphaNumericFilter $this
 */


public function setSearchType($searchType = 0)
{
 $searchType = (int)$searchType;
 if (isset(self::$searchTypes[$searchType])) {
  $this->searchType = $searchType;
 } else {
  throw new Exception('AlphaNumericFilter::setSearchType says: Selected character set is not found');
 }
 return $this;
}
 
/**
 * Set the field name for this filter
 * @param string $fieldName - the table fieldname to search in
 * @return ANFilter
 */
public function setFieldName($fieldName = null)
{
 if (isset($fieldName)) {
  $this->fieldName = $fieldName;
 }
 return $this;
} 
 
/**
 * return the list of searchable characters associated with this filter
 * @param bool $asString - return the string of characters rather than the 
 *   numeric index
 */
public function getSearchType($asString = true)
{
 if ($asString) {
  return (isset(self::$searchTypes[$this->searchType]) ? self::$searchTypes[$this->searchType] : '');
 } else {
  return $this->searchType;
 }
}

/**
 * return the fieldname for this filter
 */
public function getFieldname()
{
 return $this->fieldName;
}

These functions should be fairly straight forward. Let me know if any deeper explanation is needed. We've already parsed in our field name to the constructor, so the only other value we need to set now is the character set we're going to use. The idea is that we want to make everything work using common default settings were possible, and only have to actually set additional values for less-common cases. the setSearchType() method is public and can be called anywhere in your code, but from setup we set this as a default value of 0 ($searchType = 0). Obviously, depending on your needs you can set this default to be whatever you like from the array of character sets at the top of the class.

Other Methods

That's our set up done. Now we need to render the filter HTML and handle the returned values and SQL. 
Firstly, rendering our filter:


/**
 * render the filter html
 * @param bool $includeTitles include title text or each character
 * @return string - the filter html
 */
public function renderHTML($includeTitles = true)
{
 $searchStr = $this->getSearchType();
 $curChar = $value = isset($_REQUEST['af']) ? $_REQUEST['af'] : null; 
 
 $html = '<div id="alphaFilter_" class="aphaFilter">';
 
 for ($x = 0; $x < strlen($searchStr); $x++) {
  $html .= '<span class="af ">';
  $chr = $searchStr{$x};
  
  if ($includeTitles) {
   $title = 'Search for '.(isset($this->friendlyName) ? $this->friendlyName : $this->fieldName).' beginning with '.($chr == '#' ? 'a number' : $chr);
  } else {
   $title = '';
  }
  
  switch($this->style) {
   case 'plain' :
   default:
    $nClass = ' aflink ';
    $nClass .= ($chr == $curChar ? ' selected ' : '');
    break;
  }
  
  if ($chr == '#') {
   $html .= '<a class="aflink '.$nClass.'" title="'.$title.'" href="?af=%23">'.$chr.'</a>';
  } else {
   $html .= '<a class="aflink '.$nClass.'" title="'.$title.'" href="?af='.$chr.'">'.$chr.'</a>';
  }
  $html .= '</span>';
 }
 switch($this->style) {
  case 'plain' :
  default:
   $html .= '<a class="afbutton " title="Remove Alphanumeric Filter" href="?af=none"><i class="icon-remove-sign"></i></a>';
   break;
 } 
        $html .= '</div>';
 
 return $html;
}
This method takes one option parameter, which will add out 'Select x with x' titles as default, or not if we parse false. Then we simply get our search characters string from the $searchTypes array (using the getSearchType() method), and iterate through it one character at a time building a tags as we go which can be styled as we wish. There the are a couple of switch statements in here that could be handled in a better way..see improvements.

One other small thing of note: For the character set that contains ''#" for searching numbers, we need to create the link with href="?af=%23", otherwise the url will be malformed.

We handle the selected value with this method:

/**
 * return the sql string for the selected filter value
 * @param string $sql - the SQL string to append the filter SQL to 
 * @param string $value - the value to search for, check $_REQUEST['af'] is parsed as null
 * @return string $sql - apppended SQL string
 */
public function getSql($sql = '', $value = null)
{
 if (!isset($value)) {
  $value = isset($_REQUEST['af']) ? $_REQUEST['af'] : 'none'; 
 }
 
 if ($value == 'none') {
  // clear the filter
  return $sql;
 }
 
 // check that what has been parsed as a search value is actually found
 // in our list of available characters
 if (self::valueIsValid($this->searchType, $value)) {
  // search for generic numeric value
  if ($value == '#') {
   $xtrsql = " IN ('0','1','2','3','4','5','6','7','8','9') ";
  } else {
   $xtrsql = " = '".strtoupper($value)."' ";
  }
  
  return $sql.(strpos($sql, 'WHERE')===false ? " WHERE " : " AND " )." UPPER(LEFT(".$this->fieldName.", 1)) ".$xtrsql." ";
 } else {
  return $sql;
 }
}
I've added a $value parameter here to allow setting the filter before any selection is made (on the off chance we ever want to?). The method also calls a final method in this class to validate that what's been returned by the $_REQUEST is actually a valid character from our chosen character set:
/**
 * check if a given search value exists for a given searchType
 * @param int $searchType the index of self::$searchTypes
 * @param string $value the search value to find
 * @return boolean true if the search value is valid
 */
public static function valueIsValid($searchType, $value)
{
 $valid = false;
 if (isset(self::$searchTypes[(int)$searchType])) {
  $searchables = self::$searchTypes[(int)$searchType];
  for ($x = 0; $x < strlen($searchables); $x++) {
   if ($value == $searchables{$x}) {$valid = true; break;}
  }
 }
 return $valid;
}

}


And that's the class.

Usage

Include the class.

Wherever the SQL query you're using to generate your list is, include:
<?php $afFilter = new ANFilter(); ?>
After your SQL statement include:
<?php $yoursql = $afFilter->getSql(); ?>

Where you want to display the filter:
<?php echo $afFilter->renderHTML(); ?>

Improvements

I don't doubt there are many, many improvements that could be made to this. Couple of suggestions however would be:
  • Perhaps holding class name for various styles as a static array, and tidy out the switch statements when rendering the html
  • echo out the html directly from the renderHTML() method
Comments, questions, suggestions? All welcome.

Comments

Popular Posts