views:

298

answers:

3

Say I have a group of people, each person having a name, sex, and age property, coded like this:

public class Person
{
   private $name, $sex, $age;
   public function Person ($name, $sex, $age)
   {
       $this->name = $name;
       $this->sex = $sex;
       $this->age = $age;
   }
   public function getName();    
   public function getSex();
   public function getAge();
}

public class People
{
   private $people = array();
   public function addPerson ($name, $sex, $age)
   {
      $this->people[] = new Person($name, $sex, $age);
   }
}

How could I implement a method sortPeople() which sorted the $people array in ascending order of the peoples' names to the People class?

+4  A: 

Take a look at usort. It allows you to specify your own comparison function. Every time two objects need to be compared, it will call that comparison function you specify to see which one is greater than the other (or if they are equal). In your comparison function you can do whatever you need to with the fields in the two Person objects to compare them.

For doing callbacks with class methods (as in your example), look at passing callbacks. For example, you could do something like this:

class People {
    // your previously defined stuff here...

    public function sort() {
        usort($this->people, array($this, 'comparePeople'));
    }

    public function comparePeople(Person $p1, Person $p2) {
        return strcmp($p1->getName(), $p2->getName());
    }
}

You would also of course need to add getName() to your Person class.

For a static approach, it might look something like this:

function sortPeople($people) {
    usort($people, array('People', 'comparePeople'));
}

class People {
    // your previously defined stuff here...

    public static function comparePeople(Person $p1, Person $p2) {
        return strcmp($p1->getName(), $p2->getName());
    }
}

As you can see, it looks very similar. I would not recommend you use the static approach. It's messier and violates the single responsibility principle.

Marc W
Interesting, but it would be very ugly to create a function for this in a file which contains a class. Is there any way to pass a function like `People::comparePeople()` so the sorting function is part of the `People` class rather than an independent function?
Click Upvote
Or better: `return strcmp($p1->name, $p2->name);` ;)
naixn
@ClickUpvote: You can pass People::comparePeople (you might have to declare it static though)
naixn
I'm editing my answer right now to include an example on how to do this with a non-static.
Marc W
Okay answer edited. Haven't checked for syntax, but it should be okay. The trick is call_user_func().
Marc W
Can you also edit it to show how to do this with a static function please?
Click Upvote
Okay I added it.
Marc W
call_user_func does not work in this context, as it calls the function, it does not return a callback that usort() needs :)
naixn
Yeah you're right. Duh. He can just pass it as an array without call_user_func(). It's late in the day. :-)
Marc W
However I like my way better: Person should be the class that implements sort(), because only `Person` should be allowed to compare other `Person`.
naixn
+7  A: 

Here is a working code with a static method. It also uses the fact that the static method can access private ivars :) It also uses PHP awesome reflexivity <3.

The good point about that code is that Person is the class to provide the sort method, which is better on an OOP point of view. Only the class Person should be the one to know how to sort other Person. Neither People or another indepent function should.

Note: not using is_callable(), as it only verifies if the parameter can be called as a function, but does not check if it's actually callable with the current visibility (public, private, protected)

class Person
{
    private $name, $sex, $age;
    public function Person($name, $sex, $age)
    {
        $this->name = $name;
        $this->sex = $sex;
        $this->age = $age;
    }

    public static function sortByName(Person $p1, Person $p2)
    {
        return strcmp($p1->name, $p2->name);
    }

    public static function sortByAge(Person $p1, Person $p2)
    {
        return ($p1->age - $p2->age);
    }
}

class People
{
    private $people = array();
    public function addPerson($name, $sex, $age)
    {
        $this->people[] = new Person($name, $sex, $age);
    }

    public function display()
    {
        print_r($this->people);
    }

    public function sort($attribute = 'name')
    {
        $sortFct = 'sortBy' . ucfirst(strtolower($attribute));
        if (!in_array($sortFct, get_class_methods('Person')))
        {
            throw new Exception('People->sort(): Can\'t sort by ' . $attribute);
        }
        usort($this->people, 'Person::' . $sortFct);
    }
}

$people = new People;
$people->addPerson('Steve', 'M', 31);
$people->addPerson('John', 'M', 24);
$people->addPerson('Jane', 'F', 26);
$people->addPerson('Sally', 'F', 21);
$people->display();
$people->sort();
$people->display();
$people->sort('age');
$people->display();
naixn
+1  A: 

Especially if getName() is time consuming operation you may be better off with using decorate-sort-undecorate pattern.

<?php

class Person {
    private $name;
    function getName() {
      return $this->name;
    }
    function __construct($name) {
      $this->name = $name;
    }
}

$people = array(
    new Person('Jim'),
    new Person('Tom'),
    new Person('Tim'),
    new Person('Adam')
);

// actual sorting below
$people = array_map(create_function('$a', 'return array($a->getName(), $a);'), $people); // transform array of objects into array of arrays consisted of sort key and object
sort($people); // sort array of arrays
$people = array_map('end', $people); // take only last element from each array

print_r($people);

How does it work?

Instead of sorting array of objects you sort array of arrays whose last element is object and first is the key by which you want to sort. After sorting you keep only the object.

You can use just sort for sorting array of arrays because PHP compares two same length arrays by comparing its elements one by one.

Sorting by multiple fields

You may use more then one sort key, for example sort by surname and if surnames are identical take first name into account. You can do this by decorating with multiple keys with order of importance like so:

$people = array_map(create_function('$a', 'return array($a->getSurname(), $a->getName(), $a);'), $people);

Why it's fast

This way might be faster that using usort because it calls getName() only n-times for sorting array of length n. Comparisons during sort are done using built in comparator so should be fast. In usort method custom comparator is called multiple times (more than n times) during sort and it may slow down things.

Kamil Szot
Pretty ugly . Want to explain how that code works?
Click Upvote
Explanation is "Instead of sorting array of objects you sort array of arrays whose last element is object and first is the key by which you want to sort. After sorting you keep only the object."
Kamil Szot
I tried to elaborate bit more on mechanics of this kind of sorting. If you have further trouble with understanding please let me know by asking specific question.
Kamil Szot