Comparable Interface For PHP

About 5-6 years ago I had my “PHP should be more like Java” phase and experimented a lot with things like string objects and method overloading, which usually required hackish workarounds and most things did not turn out to be very practical in the long run.

But there is one package I still like very much, namely ComparatorTools, which got to be place 2 in the monthly PHPclasses.org innovation awards after all. It provides Comparable and Comparator interfaces and functions similar to the core array functions, that can work with these.

Interfaces

The interfaces resemble the corresponding Java interfaces, except that we do not have Generics in PHP, so it is cannot be guaranteed that compared objects have the same type. This has to be checked at runtime in the implementation, if needed. An exception type for these cases is provided:

interface Comparable
{
	/**
	 * @param object $object
	 * @return numeric negative value if $this < $object, positive if $this > $object, 0 otherwise (if objects are considered equal)
	 * @throws ComparatorException if objects are not comparable to each other
	 */
	public function compareTo($object);
}
interface Comparator
{
	/**
	 * @param object $object1
	 * @param object $object2
	 * @return numeric Negative value if $object1 < $object2, positive if $object1 > $object2, 0 otherwise
	 * @throws ComparatorException if objects are not comparable to each other
	 */
	public function compare($object1, $object2);
}

Functions

Sorting functions as well as functions that need equality comparison were implemented with a similar interface as the core array functions:

  • osort(array &$array, $comparator = null) Sort an array of objects by their Comparable Interface or a Comparator
  • orsort(array &$array, $comparator = null) Sort an array of objects by their Comparable Interface or a Comparator in reverse order
  • oasort(array &$array, $comparator = null) Sort an array of objects by their Comparable Interface or a Comparator and maintain index association
  • oarsort(array &$array, $comparator = null) Sort an array of objects by their Comparable Interface or a Comparator in reverse order and maintain index association
  • array_omultisort(array &$arrays, Comparator $comparator = null) Sort multiple arrays of objects by their Comparable Interface or a Comparator
  • array_ounique(array &$array, Comparator $comparator = null) Remove duplicate objects from an array based on their Comparable Interface or a Comparator
  • array_odiff(array $array, array $array2 /*, [array $array3, [...]], Comparator $comparator = null*/) Compute the difference of arrays based on the Comparable Interface of contained objects or a Comparator
  • array_ointersect(array $array, array $array2 /*, [array $array3, [...]], Comparator $comparator = null*/) Compute the intersection of arrays based on the Comparable Interface of contained objects or a Comparator

This procedural interface was just a convenience wrapper for the OOP interface:

/**
 * Returns a new ObjectSorter instance
 * 
 * @return ObjectSorter
 */
function object_sorter()
{
	return new ObjectSorter();
}
/**
 * Sort an array of objects by their Comparable Interface or a Comparator
 * in reverse order and maintain index association
 * 
 * @param array $array Array of objects, comparable by $comparator
 * @param Comparator $comparator A comparator. If null, the default
 * ComparableComparator will be used.
 * @return Returns TRUE on success or FALSE on failure.
 */
function oarsort(array &$array, $comparator = null)
{
	return object_sorter()
		->setMaintainKeys(true)
		->setReverse(true)
		->setComparator($comparator)
		->sort($array);
}

Version 1.0

I hadn’t touched the code for years and the style was quite outdated. Back then, PHP 5.2 was state of the art, 5.3 quite new and still not widely adopted, PSR-0 was far away. Now it was time for a revamping.

There are some newer PHP features that would come in handy:

  • Namespaces, naturally (PHP >=5.3)
  • __invoke() magic method to make comparators callable, so they can be used as compare callback immediately
  • For the procedural interface, function importing with use function (PHP >=5.6) and dereferencing “new” (PHP >=5.4)
  • Variadic methods for a nicer interface of array_multisort, without the need for references (PHP >=5.6)

Since I definitly will use this in projects that cannot yet migrate to PHP 5.6 and I am sure, others will have the same problem as well, I opted for two separate branches, 1.x and 2.x. Version 2.x will have the cool new stuff and 1.x is a modernized version of the previous 0.9 release that includes everything compatible to PHP 5.4 at most. Version 2.0 is not granted to be backwards compatible with version 1.0 (following Semantic Versioning).

New interface

Functions were replaced with static methods, which is more autoloader-friendly:

SGH\Comparable\SortFunctions

public static function sort(array &$array, Comparator $comparator = null);
public static function asort(array &$array, Comparator $comparator = null);
public static function rsort(array &$array, Comparator $comparator = null);
public static function arsort(array &$array, Comparator $comparator = null);
public static function multisort(array &$arrays, Comparator $comparator = null);
public static function sortedIterator(\Traversable $iterator, Comparator $comparator = null, $cloneItems = false);

SGH\Comparable\SetFunctions

public static function objectsDiff(array $array1, array $array2, array $..., Comparator $comparator = null);
public static function objectsIntersect(array $array1, array $array2, array $..., Comparator $comparator = null);
public static function objectsUnique(array $array);
public static function diff(array $array1, array $array2, array $..., Comparator $comparator = null)
public static function intersect(array $array1, array $array2, array $..., Comparator $comparator = null);
public static function diff_assoc(array $array1, array $array2, array $..., Comparator $comparator = null);
public static function intersect_assoc();
public static function unique(array $array, Comparator $comparator = null);

Additionally, the default comparator implementations ComparableComparator and ObjectComparator have a callback factory method that returns a callable version of themselves to be used directly in any function that takes a comparison callback as argument (like usort()):

usort($array, \SGH\Comparable\Comparator\ComparableComparator::callback())

Useful Comparators

The previous release contained comparators for SplFileInfo objects as an example. With Version 1.0 they could look like this:

use SGH\Comparable\Comparator;
use SGH\Comparable\ComparatorException;
class SplFileInfoByNameComparator implements Comparator
{
	protected function checkTypes($object1, $object2)
	{
		if (!$object1 instanceof \SplFileInfo) {
			throw new ComparatorException('$object1 (type: ' . gettype($object1) . ') is no instance of SplFileInfo.');
		}
		if (!$object2 instanceof \SplFileInfo) {
			throw new ComparatorException('$object2 (type: ' . gettype($object2) . ') is no instance of SplFileInfo.');
		}
	}
	/**
	 * @param SplFileInfo $object1
	 * @param SplFileInfo $object2
	 */
	public function compare($object1, $object2)
	{
		$this->checkTypes($object1, $object2);
		return strcmp($object1->getFileName(), $object2->getFileName());
	}
}

Together with the SortedIterator, it could be used to add sort order to a RecursiveDirectoryIterator:

$iterator = \SGH\Comparable\SortFunctions::sortedIterator(
    new RecursiveDirectoryIterator(__DIR__, FilesystemIterator::SKIP_DOTS), true);
foreach ($iterator as $fileInfo) {
    echo $file->getFileName(), "\n";
}

Note that the “cloneItems” parameter has been set to true, because the file system iterators return themselves in a different state on each iteration (more info). It still does not work with a normal DirectoryIterator for some reason, but using the RecursiveDirectoryIterator without wrapping it with a RecursiveIteratorIterator iterates non-recursively over the given directory.

More info

RFC

There is an old RFC to make such a Comparable interface part of the PHP core, that recently has been reanimated: https://wiki.php.net/rfc/comparable (current PR: https://github.com/php/php-src/pull/1097). It would be great to see this as native feature in PHP 7.x, but as long as it isn’t, you can already use the interface from my package. I actually was prepared for this since the beginning:

/*
 * just in case that a Comparable SPL Interface comes someday
 */
if (interface_exists('Comparable')) {
    return;
}

Now with the namespaced package this is not necessary anymore, but still, if you implement SGH\Comparable\Comparable, it is conform to the interface proposed in the RFC, so you can replace it with a native Comparable any time, as soon as the RFC is accepted.

One Reply to “Comparable Interface For PHP”

Comments are closed.