1
\$\begingroup\$

I wrote an implementation of php FilterIterator objects that allows me to apply them in a "cascade" way, so I can finally obtain an array of objects which is the result of all those filters.

It works fine, but - being new to SPL objects - I'm wondering if there is a better way to achieve the same results with FilterIterators et similia.

My implementation

I have a Factory class holding an array of objects in a static property:

class Factory {

    /**
     * @var Reservation[]
     */
    public static $reservations;

    public function __construct(){
        self::$reservations = $this->get_all();
    }

    private function get_all(){
        // Queries the database...
        return $results; // array of Reservation objects
    }

    // other unrelated things...
}

As I need the ability to filter such array by some criteria on-demand, I defined a bunch of FilterIterator sub-classes. The following is byOrder, others (byService, byProvider etc.) are defined in a similar way:

class byOrder extends FilterIterator {

    protected $order;

    public function __construct(Iterator $iterator, $order){
        parent::__construct($iterator);
        $this->order = $order;
    }

    public function accept(){

        /** @var $current Reservation */
        $current = $this->current();

        return $current->order() === $this->order;
    }
}

In order to apply just one filter to the array (I need it to stay as array) wherever in my application I can do:

// somewhere else...
$factory = new Factory();
// later on...
$reservations = new ArrayObject(Factory::$reservations);
$iterator = $reservations->getIterator();
$filteredReservation = iterator_to_array(new byOrder($iterator, 'ORDER_ID'));

To be able to apply multiple filters in a "cascade" way, I created the following helper class:

class Select {

    /**
     * @var ArrayIterator
     */
    private $reservations;

    public function __construct(array $reservations)
    {
        $obj = new ArrayObject($reservations);
        $this->reservations = $obj->getIterator();
    }

    public function get(){
        return iterator_to_array($this->reservations);
    }

    public function byOrder($order){
        $this->reservations = new byOrder($this->reservations, $order);

        return $this;
    }

    public function byService($service){
        $this->reservations = new byService($this->reservations, $service);

        return $this;
    }

    // ... and so on
}

It is instantiated on-demand through a method inside the Factory class:

class Factory {

    // All the previously defined content, plus:
    public function select() {
        return new Select(self::$reservations);
    }
}

So I'm able to apply multiple cascading filters like this:

// somewhere else...
$factory = new Factory();
// later on...
$filtered = $factory->select()
    ->byOrder('ORDER_ID')
    ->byService('SERVICE_NAME')
    ->get();
\$\endgroup\$
2
\$\begingroup\$

Edit

One suggestion is to make your helper class more dynamic. As it currently stands it is heavily tied to each filter class so you could not reuse it in another project. For example, imagine a new project that deals with selling cars. Your Select helper class only knows about orders, services, providers. You'd have to rewrite it to know about cars, prices, colours etc.

To get around this, you can define a filter property which holds a list of filters. Then, when calling get(), loop over the filters and apply each one. The benefit here is that the helper class is now unaware of any filter implementations and can be used in any project.

<?php

/*************************************************/
/******************** FILTERS ********************/
/*************************************************/
class Order extends FilterIterator
{
    private $_value = null;

    public function __construct(Iterator $iterator, $value)
    {
        $this->_value = $value;
        parent::__construct($iterator);
    }

    public function accept()
    {
        return $this->current()->order === $this->_value;
    }
}

class Future extends FilterIterator
{
    public function __construct(Iterator $iterator)
    {
        parent::__construct($iterator);
    }

    public function accept()
    {
        return $this->current()->date > new DateTime();
    }
}

class Date extends FilterIterator
{
    private $_value = null;

    public function __construct(Iterator $iterator, $value)
    {
        $this->_value = $value;
        parent::__construct($iterator);
    }

    public function accept()
    {
        return $this->current()->date === $this->_value;
    }
}

/*************************************************/
/*************** COLLECTION HELPER ***************/
/*************************************************/
class Helper
{
    private $_items = [];
    private $_filters = [];

    public function __construct(array $items)
    {
        $this->_items = (new ArrayObject($items))->getIterator();
    }

    public function filter($filter, $value = null)
    {
        $this->_filters[] = [$filter => $value];

        return $this;
    }

    public function get()
    {
        foreach ($this->_filters as $filter) {
            foreach ($filter as $name => $value) {

                if ($value) {
                    $this->_items = new $name($this->_items, $value);
                } else {
                    $this->_items = new $name($this->_items);
                }
            }
        }

        return iterator_to_array($this->_items);
    }
}

/*************************************************/
/****************** TEST CASES *******************/
/*************************************************/
class Reservation
{
    public $order = null;
    public $date = null;
    public $status = null;

    public function __construct($order, $date, $status)
    {
        $this->order = $order;
        $this->date = $date;
        $this->status = $status;
    }
}

$reservations = new Helper([
    new Reservation(123, new DateTime('+1Day'), 'Active'),
    new Reservation(512, new DateTime('+2Month'), 'Active'),
    new Reservation(456, new DateTime('-6Hour'), 'Pending'),
    new Reservation(789, new DateTime('-17Day'), 'Cancelled'),
    new Reservation(264, new DateTime('+1Hour'), 'Pending'),
    new Reservation(151, new DateTime('+1Year'), 'Active'),
]);

$filtered = $reservations
    ->filter('Future')
    ->filter('Order', 151)
    ->get();

var_dump($filtered);

Final note - I played around with the implementation for a while and found FilterIterators to be rather cumbersome because I had to write a single filter class for each permutation of a property. For example, in a reservation system you'd need the following filters: ByOrder, ByOrderNotEqualTo, ByDate, ByFuture, ByPast, By6MonthsFromNow, ByStatus, ByStatusNotEqualTo, ByCustomerId, etc.

One solution is to add a where method to your helper class which would accept a comparison operator. Again, I point you to Laravel's implementation and a document highlighting its usage. Food for thought.

EndEdit

If you're looking for a simple method, take a look at Laravel's implementation of their Collection::filter which calls Arr::filter. It provides a clean, chainable way to pass a callback to array_filter.

Below, is a basic demonstration on how to use it. I've implemented it using ArrayObject but you can use any iterable class. The filter calls will be slightly longer to type out than your implementation since you must define a callback function each time, but it makes the source much cleaner since you don't have to define a separate filter class for every property.

http://sandbox.onlinephpfunctions.com/code/cea564648d52c702e112d9775d1afe76f41fc810

<?php

class Collection extends ArrayObject
{
    public function filter(callable $callback = null)
    {
        if ($callback) {
            // This will return a new Collection object with the filtered results.
            // Late static binding -  You **could use either `static`
            // or `self` here since both will resolve to Collection.
            // Going with `static` in case you want to extend this class.
            // See - https://stackoverflow.com/q/5197300/296555
            return new static (array_filter($this->getArrayCopy(), $callback, ARRAY_FILTER_USE_BOTH));
        }

        return new static (array_filter($this->getArrayCopy()));
    }
}

class Reservation
{
    public $order = null;
    public $date = null;
    public $status = null;

    public function __construct($order, $date, $status)
    {
        $this->order = $order;
        $this->date = $date;
        $this->status = $status;
    }
}

$reservations = new Collection([
    new Reservation(123, new DateTime('+1Day'), 'Active'),
    new Reservation(512, new DateTime('+2Month'), 'Active'),
    new Reservation(456, new DateTime('-6Hour'), 'Pending'),
    new Reservation(789, new DateTime('-17Day'), 'Cancelled'),
    new Reservation(264, new DateTime('+1Hour'), 'Pending'),
    new Reservation(151, new DateTime('+1Year'), 'Active'),
]);

$filtered = $reservations 
    // Active reservations
    ->filter(function ($value, $key) {
        return $value->status === 'Active';
    })
    // In the future
    ->filter(function ($value, $key) {
        return $value->date > new DateTime();
    });

var_dump($filtered);

Finally, here's a decent package that took this idea a step further and implements most array functions so you can chain them.

New contributor
waterloomatt is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$
  • \$\begingroup\$ This is a nice and compact solution, although it doesn't use FilterIterators which I wanted to explore for the reason (which I should have probably explicited) of their flexibility and reusability. From a quick-and-dirty test, it also looks like my implementation uses less memory than array_filter chaining at least when a high number of items and a certain number of filters are applied: here versus here \$\endgroup\$ – Jolly Roger Jan 11 at 19:33
  • \$\begingroup\$ To answer your original question, you need that helper class to return $this to make your filters chainble. If you want to remove that helper class, you'd inject the filter into the next filter. Ex. new ByOrder(new ByService(new ByProvider(...))) but I'm not a fan of this approach - goo.gl/LcyqfU. As for the memory usage, you're right about array_filter; it makes a copy of the array for each filter. Be careful using this approach on very large datasets. For me though, I value the simplicity, readability and reusability of the Collection class over the higher memory usage. \$\endgroup\$ – waterloomatt 2 days ago

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.