How to Use Value Comparators

Caution

This page is not updated for Rollerworks v2.0 yet.

A powerful feature of RollerworksSearch is the ability to optimize search conditions and perform basic validation of user input.

But in order to do this the system needs to understand which values are equal or lower/higher to other values. Especially when you are working with objects.

Note

Fields with range support enabled must have a Value Comparator in order to work properly. A missing Comparator will mark every range in the field invalid!

Assuming you have a field that handles invoice numbers as an InvoiceNumber and you configured that the field supports ranges.

InvoiceNumber value class

First create the InvoiceNumber class that holds an invoice number.

This technique is known as a ‘value class’, the InvoiceNumber is immutable meaning its internal values can’t be changed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// src/Acme/Invoice/InvoiceNumber.php

namespace Acme\Invoice;

final class InvoiceNumber
{
    private $year;
    private $number;

    private function __construct(int $year, int  $number)
    {
        $this->year = $year;
        $this->number = $number;
    }

    public static function createFromString(string $input): self
    {
        if (!preg_match('/^(?P<year>\d{4})-(?P<number>\d+)$/s', $input, $matches)) {
            throw new \InvalidArgumentException('This not a valid invoice number.');
        }

        return new InvoiceNumber((int) $matches['year'], (int) ltrim($matches['number'], '0'));
    }

    public function equals(InvoiceNumber $input): bool
    {
        return $input == $this;
    }

    public function isHigher(InvoiceNumber $input): bool
    {
        if ($this->year > $input->year) {
            return true;
        }

        if ($input->year === $this->year && $this->number > $input->number) {
            return true;
        }

        return false;
    }

    public function isLower(InvoiceNumber $input): bool
    {
        if ($this->year < $input->year) {
            return true;
        }

        if ($input->year === $this->year && $this->number < $input->number) {
            return true;
        }

        return false;
    }

    public function __toString(): string
    {
        // Return the invoice number with leading zero
        return sprintf('%d-%04d', $this->year, $this->number);
    }
}

Tip

See How to Use Data Transformers on how to transform a user input to an InvoiceNumber.

Creating the Comparator

Create an InvoiceNumberComparator class - this class will be responsible for comparing values for equality and lower/higher InvoiceNumber objects:

// src/Acme/Invoice/Search/ValueComparator/InvoiceNumberComparator.php

namespace Acme\Invoice\Search\ValueComparator;

use Acme\Invoice\InvoiceNumber;
use Rollerworks\Component\Search\ValueComparator;

final class InvoiceNumberComparator implements ValueComparator
{
    public function isHigher($higher, $lower, array $options): bool
    {
        return $higher->isHigher($lower);
    }

    public function isLower($lower, $higher, array $options): bool
    {
        return $lower->isLower($higher);
    }

    public function isEqual($value, $nextValue, array $options): bool
    {
        return $value->equals($nextValue);
    }
}

Tip

A comparison method will only receive values that are returned by the field’s data transformer.

You don’t have to check if the input is what you expect, but if the input is invalid you need look at the configured data transformers.

Note

When isLower() and isHigher() are not supported, then both methods should be return false.

Using the Comparator

Now that you have the Comparator built, you need to add it to your invoice field type:

// src/Acme/Invoice/Search/Type/InvoiceNumberType.php

namespace Acme\Invoice\Search\Type;

use Acme\Invoice\Search\DataTransformer\InvoiceNumberTransformer;
use Rollerworks\Component\Search\AbstractFieldType;
use Rollerworks\Component\Search\Exception\InvalidConfigurationException;
use Rollerworks\Component\Search\FieldConfigInterface;
use Rollerworks\Component\Search\Value\{Compare, Range};

final class InvoiceNumberType extends AbstractFieldType
{
    private $valueComparator;

    public function __construct()
    {
        $this->valueComparator = new InvoiceNumberComparator();
    }

    public function buildType(FieldConfigInterface $config, array $options)
    {
        $config->setValueComparator($this->valueComparator);
        $config->setValueTypeSupport(Compare::class, true);
        $config->setValueTypeSupport(Range::class, true);

        $config->addViewTransformer(new InvoiceNumberTransformer());
    }
}

Cool, you’re done! Input processors can now validate the bounds of ranges and optimizers can optimize the generated search condition.

Optimizing incremented values

Now that your type supports comparing values, you can extend the Comparator with the ability to calculate increments.

Calculating increments helps with optimizing single incremented values. For example: 1, 2, 3, 4, 5 can be converted to a 1 ~ 5 range which will simplify the search condition and speed-up the search operation.

Note

Optimizing incremented values is done by the :class:Rollerworks\\Component\\Search\\ConditionOptimizer\\ValuesToRange optimizer. So make sure its enabled.

Instead of implementing the ValueComparator interface, you implement the ValueIncrementer interface (which extends the ValueComparator interface) and add the getIncrementedValue method for calculating increments:

// src/Acme/Invoice/Search/ValueComparison/InvoiceNumberComparison.php

namespace Acme\Invoice\Search\ValueComparison;

use Acme\Invoice\InvoiceNumber;
use Rollerworks\Component\Search\ValueIncrementerInterface;

class InvoiceNumberComparison implements ValueIncrementerInterface
{
    public function isHigher($higher, $lower, array $options): bool
    {
        return $higher->isHigher($lower);
    }

    public function isLower($lower, $higher, array $options): bool
    {
        return $lower->isLower($higher);
    }

    public function isEqual($value, $nextValue, array $options): bool
    {
        return $value->equals($nextValue);
    }

    public function getIncrementedValue($value, array $options, int $increments = 1)
    {
        return new InvoiceNumber($value->getYear(), $value->getNumber() + $increments);
    }
}

Note

Technically it’s possible to optimize 2015-099, 2015-100, 2015-000" to 2015-099 ~ 2015-0001, but only when we know if “2015-100” is the last invoice for the year 2015.

Cool, you’re done! The new InvoiceNumberComparison can be set as your field’s ValueComparator.