Comparable Interface für PHP

Vor etwa 5-6 Jahren hatte ich meine “PHP sollte mehr wie Java sein” Phase und habe viel mit Sachen wie String Objekten und Überladen von Methoden experimentiert, was meistens fiese Workarounds erforderte und die meisten Dinge stellten sich auf lange Sicht nicht als sehr praktikabel heraus.

Aber ein Package aus der Zeit gefällt mir immer noch ziemlich gut, und zwar ComparatorTools, was immerhin Platz 2 in den monatlichen PHPclasses.org Innovation Awards belegte. Es stellt Comparable und Comparator Interfaces zur Verfügung sowie Funktionen, analog zu den Core Array-Funktionen, die mit diesen arbeiten können.

Interfaces

Die Interfaces ähneln den entsprechenden Java interfaces, außer dass wir keine Generics in PHP haben, so dass nicht garantiert werden kann, dass die verglichenen Objekte den selben Typ haben. Dies muss zur Laufzeit in der Implementierung geprüft werden, sofern nötig. Ein Exception-Typ für diese Fälle ist verfügbar:

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

Funktionen

Sortierfunktionen sowie Funktionen, die Vergleich auf Gleichheit benötigen wurden mit einem ähnlichen Interface wie die Core Array-Funktionen implementiert:

  • osort(array &$array, $comparator = null) Sortiert ein Array von Objekten aufgrund ihres Comparable Interfaces oder eines Comparators
  • orsort(array &$array, $comparator = null) Sortiert ein Array von Objekten aufgrund ihres Comparable Interfaces oder eines Comparators in umgekehrter Reihenfolge
  • oasort(array &$array, $comparator = null) Sortiert ein Array von Objekten aufgrund ihres Comparable Interfaces oder eines Comparators und behält Index-Assoziationen bei
  • oarsort(array &$array, $comparator = null) Sortiert ein Array von Objekten aufgrund ihres Comparable Interfaces oder eines Comparators in umgekehrter Reihenfolge und behält Index-Assoziationen bei
  • array_omultisort(array &$arrays, Comparator $comparator = null) Sortiert mehrere Arrays von Objekten aufgrund ihres Comparable Interfaces oder eines Comparators
  • array_ounique(array &$array, Comparator $comparator = null) Entfernt doppelte Objekte von einem Array aufgrund ihres Comparable Interfaces oder eines Comparators
  • array_odiff(array $array, array $array2 /*, [array $array3, [...]], Comparator $comparator = null*/) Berechnet die Differenz von Arrays aufgrund ihres Comparable Interfaces oder eines Comparators
  • array_ointersect(array $array, array $array2 /*, [array $array3, [...]], Comparator $comparator = null*/) Berechnet die Schnittmenge von Arrays aufgrund ihres Comparable Interfaces oder eines Comparators

Dieses prozedurale Interface war nur ein Wrapper für das 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

Ich hatte den Code seit Jahren nicht angefasst und der Stil war recht überholt. Seinerzeit war PHP 5.2 der Stand der Technik, 5.3 noch recht neu und längst nicht weithin verbreitet, PSR-0 war noch weit weg. Jetzt war es Zeit zum Aufpolieren.

Einige neueren PHP Features würden sich dabei als nützlich erweisen:

  • Namespaces, natürlich (PHP >=5.3)
  • __invoke() magische Methode, um Comparators callable zu machn, so dass sie direkt als Vergleichs-Callback genutzt werden können
  • Für das prozedurale Interface, importieren von Funktionen mit use function (PHP >=5.6) und Dereferenzieren von “new” (PHP >=5.4)
  • Variadic Methods für ein schöneres Interface von array_multisort, ohne Array von Referenzen (PHP >=5.6)

Da ich das Package definitiv in Projekten nutzen werde, die noch nicht zu PHP 5.6 migriert werden können und ich mir sicher bin, dass andere das selbe Probem haben werden, habe ich mich für zwei separate Branches entschieden, 1.x und 2.x. Version 2.x wird all die coolen neuen Features nutzen, während 1.x eine modernisierte Version des vorigen 0.9 Releases ist, das alles enthält, was PHP 5.4 kompatibel ist. Für Version 2.0 ist keine Abwärtskompatibilität garantiert (nach Semantic Versioning).

Neues Interface

Funktionen wurden Autoloader-freundlich ersetzt durch statische Methoden:

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

Zusätzlich haben die Standard-Implementierungen ComparableComparator und ObjectComparator eine callback Factory Methode erhalten, die eine aufrufbare (callable) Version ihrer selbst zurückgeben, die direkt in jeder Funktion verwendet werden kann, die einen Vergleichs-Callback als Argument entgegennehmen (wie usort()):

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

Nützliche Comparatoren

Das vorige Release enthielt Comparators für SplFileInfo Objekte als Beispiel. Mit Version 1.0 könnten diese so aussehen:

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());
	}
}

Zusammen mit dem SortedIterator könnte so ein Comparator genutzt werden, Sortierreihenfolge zu einem RecursiveDirectoryIterator hinzuzufügen:

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

Beachte, dass der “cloneItems” Parameter auf true gesetzt wurde, weil die Dateisystem-Iteratoren in jeder Iteration sich selbst in verschiedenen Stati zurückgeben (mehr dazu). Es funktioniert aus irgendeinem Grund immer noch nicht mit einem normalen DirectoryIterator aber wenn man den RecursiveDirectoryIterator ohne umgebenden RecursiveIteratorIterator nutzt, iteriert er nicht-rekursiv über das gegebene Verzeichnis.

Mehr Infos

RFC

Es gibt einen alten RFC, solch ein Comparable Interface Teil des PHP Cores zu machen, der kürzlich reanimiert wurde: https://wiki.php.net/rfc/comparable (aktueller PR: https://github.com/php/php-src/pull/1097). Es wäre schön, das als natives Feature in PHP 7.x zu sehen, aber so lange es das nicht gibt, kann man bereits das Interface von meinem Package nutzen. Ich war übrigens von Anfang an darauf vorbereitet:

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

Mit dem Namespace ist das nun nicht mehr notwendig. Dennoch, wenn Du SGH\Comparable\Comparable implementierst, ist es es kompatibel zu dem Interface das im RFC vorgeschlagen wurde, so dass es jederzeit mit einem nativen Comparable ersetzt werden kann, sobald der RFC akzeptiert wurde.

One Reply to “Comparable Interface für PHP”

Comments are closed.