Pass extra parameters to usort callback [duplicate]
Asked Answered
W

6

51

I have the following functions. WordPress functions, but this is really a PHP question. They sort my $term objects according to the artist_lastname property in each object's metadata.

I want to pass a string into $meta in the first function. This would let me reuse this code as I could apply it to various metadata properties.

But I don't understand how I can pass extra parameters to the usort callback. I tried to make a JS style anonymous function but the PHP version on the server is too old (v. 5.2.17) and threw a syntax error.

Any help - or a shove towards the right corner of the manual - gratefully appreciated. Thanks!

function sort_by_term_meta($terms, $meta) 
{
  usort($terms,"term_meta_cmp");
}

function term_meta_cmp( $a, $b ) 
{
    $name_a = get_term_meta($a->term_id, 'artist_lastname', true);
    $name_b = get_term_meta($b->term_id, 'artist_lastname', true);
    return strcmp($name_a, $name_b); 
}

PHP Version: 5.2.17

Wicketkeeper answered 22/11, 2011 at 16:51 Comment(0)
S
25

In PHP, one option for a callback is to pass a two-element array containing an object handle and a method name to call on the object. For example, if $obj was an instance of class MyCallable, and you want to call the method1 method of MyCallable on $obj, then you can pass array($obj, "method1") as a callback.

One solution using this supported callback type is to define a single-use class that essentially acts like a closure type:

function sort_by_term_meta( $terms, $meta ) 
{
    usort($terms, array(new TermMetaCmpClosure($meta), "call"));
}

function term_meta_cmp( $a, $b, $meta )
{
    $name_a = get_term_meta($a->term_id, $meta, true);
    $name_b = get_term_meta($b->term_id, $meta, true);
    return strcmp($name_a, $name_b); 
} 

class TermMetaCmpClosure
{
    private $meta;

    function __construct( $meta ) {
        $this->meta = $meta;
    }

    function call( $a, $b ) {
        return term_meta_cmp($a, $b, $this->meta);
    }
}
Southerly answered 22/11, 2011 at 17:36 Comment(4)
I like this, but I think I like @Kato's better as it's sort of 'chunked out' into a little machine - are there advantages to instantiating a new object each time?Wicketkeeper
@djb: Kato's solution essentially introduces a global variable where the $meta string is held. PHP is single-threaded, so this is not a deal-breaker. However, I think that it is cleaner to encapsulate the $meta string so that code cannot accidentally change the static variable's contents while a sort is being performed.Southerly
ah, now I understand why. I can see its conceptual cleanliness... accepted. thanks.Wicketkeeper
@Daniel - I agree about encapsulation and hadn't considered passing an actual object as the first arg instead of the class name; great solution! That said, I think that the static approach is a bit easier to grok and follow mentally, as noted by djb's initial comments; trade-offs trade-offs ;)Taegu
P
112

I think this question deserves an update. I know the original question was for PHP version 5.2, but I came here looking for a solution and found one for newer versions of PHP and thought this might be useful for other people as well.

For PHP 5.3 and up, you can use the 'use' keyword to introduce local variables into the local scope of an anonymous function. So the following should work:

function sort_by_term_meta(&$terms, $meta) {
    usort($terms, function($a, $b) use ($meta) {
        $name_a = get_term_meta($a->term_id, 'artist_lastname', true);
        $name_b = get_term_meta($b->term_id, 'artist_lastname', true);
        return strcmp($name_a, $name_b);  
    });
}

Some more general code

If you want to sort an array just once and need an extra argument you can use an anonymous function like this:

usort($arrayToSort, function($a, $b) use ($myExtraArgument) {
    //$myExtraArgument is available in this scope
    //perform sorting, return -1, 0, 1
    return strcmp($a, $b);
});

If you need a reusable function to sort an array which needs an extra argument, you can always wrap the anonymous function, like for the original question:

function mySortFunction(&$arrayToSort, $myExtraArgument1, $myExtraArgument2) {
    usort($arrayToSort, function($a, $b) use ($myExtraArgument1, $myExtraArgument2) {
        //$myExtraArgument1 and 2 are available in this scope
        //perform sorting, return -1, 0, 1
        return strcmp($a, $b);
    });
}
Parturition answered 24/3, 2014 at 13:21 Comment(4)
Great! May I suggest to modify your first example to actually use the $meta parameter, so that it would be clearer.Maulmain
Super useful, thanks; realized you can also pass in multiple arguments that way ($extraArgument1, $extraArgument2)Oriel
How would you go about defining the compare function not as a closure but separately (so you can reuse it).Wapentake
usort($terms, function($a, $b) use ($meta) { - of course, thanks!Annabellannabella
S
25

In PHP, one option for a callback is to pass a two-element array containing an object handle and a method name to call on the object. For example, if $obj was an instance of class MyCallable, and you want to call the method1 method of MyCallable on $obj, then you can pass array($obj, "method1") as a callback.

One solution using this supported callback type is to define a single-use class that essentially acts like a closure type:

function sort_by_term_meta( $terms, $meta ) 
{
    usort($terms, array(new TermMetaCmpClosure($meta), "call"));
}

function term_meta_cmp( $a, $b, $meta )
{
    $name_a = get_term_meta($a->term_id, $meta, true);
    $name_b = get_term_meta($b->term_id, $meta, true);
    return strcmp($name_a, $name_b); 
} 

class TermMetaCmpClosure
{
    private $meta;

    function __construct( $meta ) {
        $this->meta = $meta;
    }

    function call( $a, $b ) {
        return term_meta_cmp($a, $b, $this->meta);
    }
}
Southerly answered 22/11, 2011 at 17:36 Comment(4)
I like this, but I think I like @Kato's better as it's sort of 'chunked out' into a little machine - are there advantages to instantiating a new object each time?Wicketkeeper
@djb: Kato's solution essentially introduces a global variable where the $meta string is held. PHP is single-threaded, so this is not a deal-breaker. However, I think that it is cleaner to encapsulate the $meta string so that code cannot accidentally change the static variable's contents while a sort is being performed.Southerly
ah, now I understand why. I can see its conceptual cleanliness... accepted. thanks.Wicketkeeper
@Daniel - I agree about encapsulation and hadn't considered passing an actual object as the first arg instead of the class name; great solution! That said, I think that the static approach is a bit easier to grok and follow mentally, as noted by djb's initial comments; trade-offs trade-offs ;)Taegu
T
8

Assuming you've access to objects and static (PHP 5 or greater), you can create an object and pass the arguments directly there, like so:

<?php
class SortWithMeta {
    private static $meta;

    static function sort(&$terms, $meta) {
       self::$meta = $meta;
       usort($terms, array("SortWithMeta", "cmp_method"));
    }

    static function cmp_method($a, $b) {
       $meta = self::$meta; //access meta data
       // do comparison here
    }

}

// then call it
SortWithMeta::sort($terms, array('hello'));

Assuming you don't have access to objects/static; you could just do a global:

$meta = array('hello'); //define meta in global

function term_meta_cmp($a, $b) {
   global $meta; //access meta data
   // do comparison here
}

usort($terms, 'term_meta_cmp');
Taegu answered 22/11, 2011 at 17:29 Comment(4)
Thanks, this is interesting. I think I prefer this to creating a new object each time? Or is @Daniel's method cleaner?Wicketkeeper
In static method SortWithMeta::sort, I believe that parameter $terms needs to be passed by reference.Southerly
@Daniel - yes, I think it needs to be passed by reference; updating nowTaegu
@Wicketkeeper - Daniel's is technically going to use less resources because $meta can get garbage collected (you could always add self::$meta = null; inside cmp_method() to make them equivalent!), but it's not going to be significant unless $meta contains massive data; go with what is easiest to grok and manipulate for you; I like both ;)Taegu
S
3

Warning This function has been DEPRECATED as of PHP 7.2.0. Relying on this function is highly discouraged.

The docs say that create_function() should work on PHP >= 4.0.1. Does this work?

function term_meta_cmp( $a, $b, $meta )  {
    echo "$a, $b, $meta<hr>"; // Debugging output
}
$terms = array("d","c","b","a");
usort($terms, create_function('$a, $b', 'return term_meta_cmp($a, $b, "some-meta");'));
Sought answered 22/11, 2011 at 17:26 Comment(2)
Well, this does work, but doesn't really solve the problem. You are still passing the meta hardcoded into the comparison function, a variable is whats's needed.Parturition
Great ! this is the only way I found to call a recursive closure without messing up with an index parameter ! Sadly this function is deprecated in PHP 7.2 and should be replaced by a native anonymous function (closure) as it uses eval() which is evilGallbladder
P
1

This won't help you at all with usort() but might be helpful nevertheless. You could sort the array using one of the other sorting functions, array_multisort().

The idea is to build an array of the values that you would be sorting on (the return values from get_term_meta()) and multisort that against your main $terms array.

function sort_by_term_meta(&$terms, $meta) 
{
    $sort_on = array();
    foreach ($terms as $term) {
        $sort_on[] = get_term_meta($term->term_id, $meta, true);
    }
    array_multisort($sort_on, SORT_ASC, SORT_STRING, $terms);
}
Passionless answered 22/11, 2011 at 19:24 Comment(0)
P
0

What is the Simplest Solution to Passing Args to usort()?

I like many of the answers here, but I wanted to have a solution that could be done as simply as possible, but could also be demonstrated! When calling usort, supply extra arguments like this...

usort($sortable, [$arg1, $arg2, ... $argn, compareFunction]);

But make sure to define these arguments before, so, you'll end up with something like...

$arg1 = 'something';
$arg2 = 'something else';
$argn = 'yet another thing';

usort($sortable, [$arg1, $arg2, ... $argn, compareFunction]);

Then $arg1, $arg2, and $argn will be available to the compareFunction().

Demo It Up!

To demonstrate, here is a usort() that only considers the first three letters of elements being compared...

function cmp ($a, $b) {
     return strcmp(substr($a, 0, $num), substr($a, 0, $num));
}

$terms = ['123a', '123z', '123b',];
$num = 3;
$thing = 4;
usort($terms, [$num, $thing, cmp]);

print_r($terms);

Full Working Demo Online

Puerility answered 7/12, 2021 at 19:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.