2
\$\begingroup\$

I made an interface for constructing an abstract query against a data set.

For example if I have an array of objects, and I want to filter it and return only the objects that match certain criteria, I would need to go foreach and type my conditions in, or use array_filter. What if I could declare my intentions as a data structure and then have my code understand this structure and give me what I want?

<?php

class Criteria {

    protected $glue = 'AND';
    protected $rules = [];

    /**
     * Get the glue of this criteria.
     * @return string The glue.
     */
    public function glue() {
        return $this->glue;
    }

    /**
     * Get the rules of this criteria.
     * @return array The structured rules defined by this criteria.
     */
    public function rules() {
        return $this->rules;
    }

    public function __construct($glue = null) {
        if ($glue !== null) {
            $this->glue = strtoupper($glue);
        }
    }

    public function equals($property, $value) {
        $this->rules[] = ['eq' => [$property, $value]];
        return $this;
    }

    public function notequals($property, $value) {
        $this->rules[] = ['noteq' => [$property, $value]];
        return $this;
    }

    public function greater($property, $value) {
        $this->rules[] = ['gt' => [$property, $value]];
        return $this;
    }

    public function less($property, $value) {
        $this->rules[] = ['lt' => [$property, $value]];
        return $this;
    }

    public function in($property, array $values) {
        $this->rules[] = ['in' => [$property, $values]];
        return $this;
    }

    public function notin($property, array $values) {
        $this->rules[] = ['notin' => [$property, $values]];
        return $this;
    }

    public function between($property, $start, $end) {
        $this->rules[] = ['btw' => [$property, [$start, $end]]];
        return $this;
    }

    public function notbetween($property, $start, $end) {
        $this->rules[] = ['notbtw' => [$property, [$start, $end]]];
        return $this;
    }

    public function like($property, $value) {
        $this->rules[] = ['like' => [$property, $value]];
        return $this;
    }

    public function notlike($property, $value) {
        $this->rules[] = ['notlike' => [$property, $value]];
        return $this;
    }

    public function group($glue) {
        $criteria = new static($glue);

        $this->rules[] = $criteria;

        return $criteria;
    }

}

Let's get practical so it would be easier to understand, suppose I have a list of people.

<?php

$people = [
    ['name' => 'John', 'age' => 20],
    ['name' => 'Joe', 'age' => 25],
    ['name' => 'Jill', 'age' => 30],
    ['name' => 'Jane', 'age' => 35],
];

Now I want to get all people above the age of 25, I will construct my criteria.

<?php

$criteria = new Criteria();
$criteria->greater('age', 25);

dump($criteria);

/**
Criteria Object
(
    [glue:protected] => AND
    [rules:protected] => Array
        (
            [0] => Array
                (
                    [gt] => Array
                        (
                            [0] => age
                            [1] => 25
                        )

                )

        )

)
*/

At this point I have my criteria declared but it alone has not much of a practical use, it is simply a declaration of intentions, it doesn't do anything. Here I introduce the criteria interpreter.

class PHPCriteriaInterpreter {

    protected $phpcode;

    public function __construct(Criteria $criteria) {
        $this->phpcode = $this->interpret($criteria);
    }

    protected function interpret(Criteria $criteria): string {
        $conditions = [];

        foreach ($criteria->rules() as $rule) {
            if ($rule instanceof Criteria) {
                $conditions[] = '(' . $this->interpret($rule) . ')';
            } else {
                foreach ($rule as $key => $data) {
                    $conditions[] = "\$this->{$key}(\$data['{$data[0]}'], " . var_export($data[1], true) . ')';
                }
            }
        }

        switch ($criteria->glue()) {
            case 'OR':
                $glue = ' || ';
                break;

            default:
                $glue = ' && ';
                break;
        }

        return implode($glue, $conditions);
    }

    public function validate($data) {
        return eval('return ' . $this->phpcode . ';');
    }

    public function eq($data, $value) {
        return $data == $value;
    }

    public function noteq($data, $value) {
        return $data != $value;
    }

    public function gt($data, $value) {
        return $data > $value;
    }

    public function lt($data, $value) {
        return $data < $value;
    }

    public function in($data, $list) {
        return in_array($data, $list);
    }

    public function notin($data, $list) {
        return !$this->in($data, $list);
    }

    public function btw($data, $values) {
        return $data > $values[0] && $data < $values[1];
    }

    public function notbtw($data, $values) {
        return !$this->btw($data, $values);
    }

    public function like($data, $value) {
        return mb_strpos($data, $value) !== false;
    }

    public function notlike($data, $value) {
        return !$this->like($data, $value);
    }

}

So now I can write my foreach using the interpreter.

<?php

$interpreter = new PHPCriteriaInterpreter($criteria);
$filtered = [];

foreach ($people as $row) {
    if ($interpreter->validate($row)) {
        $filtered[] = $row;
    }
}

dump($filtered);
/**
Array
(
    [0] => Array
        (
            [name] => Jill
            [age] => 30
        )

    [1] => Array
        (
            [name] => Jane
            [age] => 35
        )

)
*/

Why go through all this trouble, you might ask. The hidden power of this is, that I can now make an SQLCriteriaInterpreter which can translate those rules into SQL syntax and through other layers of abstraction I can completely ignore what kind of persistence is used and I could query the persistence layer through an uniform interface.

\$\endgroup\$
2
\$\begingroup\$

I like the general idea of the criteria class as a general means for defining rules that could then be interpreted by various rules engine implementations.

I find myself not liking the specific rules engine implementation that you have provided, primarily because of it's reliance on eval(). I would think that your rules engine implementations might be made specific to the data store/provider against which you are working - ArrayInterpreter, StdClassInterpreter, PdoInterpreter, MongoDbInterpreter, etc. which all implement a common interpretation interface.

It seems odd to me that you would need to manually loop through a data source outside of the interpreter. Why would you not just pass the full data set to the interpreter to get the final result returned? Without this, you are really no better off that using array_filter, and in fact you limit the types of data you can work with those data sources that allow for array style iteration.

Why are the gt, eq, etc. methods in the interpreter public methods? What value do these have to an outside caller? These methods sure would be nice to use as callable functions with array_filter() in an ArrayInterpreter() class that can accept an entire array as input.

I like that you start to write doc blocks. Be consistent and write these throughout.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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