Repository: hoaproject/Ruler Branch: master Commit: 427d20077d6a Files: 46 Total size: 310.5 KB Directory structure: gitextract_hj2vqy6x/ ├── .State ├── .gitignore ├── .travis.yml ├── Bin/ │ └── Assert.php ├── CHANGELOG.md ├── Context.php ├── Documentation/ │ ├── En/ │ │ └── Index.xyl │ └── Fr/ │ └── Index.xyl ├── DynamicCallable.php ├── Exception/ │ ├── Asserter.php │ ├── Exception.php │ └── Interpreter.php ├── Grammar.pp ├── Model/ │ ├── Bag/ │ │ ├── Bag.php │ │ ├── Context.php │ │ ├── RulerArray.php │ │ └── Scalar.php │ ├── Model.php │ └── Operator.php ├── README.md ├── Ruler.php ├── Test/ │ ├── Integration/ │ │ ├── Documentation.php │ │ └── Model/ │ │ └── Operator.php │ └── Unit/ │ ├── Context.php │ ├── DynamicCallable.php │ ├── Exception/ │ │ ├── Asserter.php │ │ ├── Exception.php │ │ └── Interpreter.php │ ├── Issue.php │ ├── Model/ │ │ ├── Bag/ │ │ │ ├── Bag.php │ │ │ ├── Context.php │ │ │ ├── RulerArray.php │ │ │ └── Scalar.php │ │ ├── Model.php │ │ └── Operator.php │ ├── Ruler.php │ └── Visitor/ │ ├── Asserter.php │ ├── Compiler.php │ ├── Disassembly.php │ └── Interpreter.php ├── Visitor/ │ ├── Asserter.php │ ├── Compiler.php │ ├── Disassembly.php │ └── Interpreter.php ├── bors.toml └── composer.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .State ================================================ finalized ================================================ FILE: .gitignore ================================================ /vendor/ /composer.lock ================================================ FILE: .travis.yml ================================================ language: php branches: only: - staging - trying - master matrix: include: - php: 5.5 - php: 5.6 - php: 7.0 - php: 7.1 env: - ENABLE_XDEBUG=true - php: 7.1 env: - ENABLE_DEVTOOLS=true - php: nightly allow_failures: - php: nightly fast_finish: true os: - linux notifications: irc: "chat.freenode.net#hoaproject" sudo: false env: global: - secure: "AAAAB3NzaC1yc2EAAAADAQABAAAAgQDzZax7/VFMmTnePlw4PQmD7pVaJQDbLaMXfIIuV/h51m0g8dYfWiGytsblv+/tW37b3TKaGVLMP3vL9jGU73V4P64Ytafj0UV3UKSzHR4atrAPjsCjsFMzlIvKLCZf2FmADRspv/pAg1loQWRzXAiZ9pqCSTxx32x20uLAJmLucQ==" cache: directories: - vendor/ before_script: - export PATH="$PATH:$HOME/.composer/vendor/bin" - if [[ ! $ENABLE_XDEBUG ]]; then phpenv config-rm xdebug.ini || echo "ext-xdebug is not available, cannot remove it."; fi script: - composer install - vendor/bin/hoa test:run - if [[ $ENABLE_DEVTOOLS ]]; then composer global require friendsofphp/php-cs-fixer; vendor/bin/hoa devtools:cs --diff --dry-run .; fi ================================================ FILE: Bin/Assert.php ================================================ getOption($v)) { switch ($c) { case '__ambiguous': $context[$v['option']] = $v['value']; break; case 'h': case '?': return $this->usage(); } } $this->parser->listInputs($rule); if (empty($rule)) { return $this->usage(); } return (int) (!$ruler->assert($rule, $context)); } /** * The command usage. * * @return int */ public function usage() { echo 'Usage : ruler:assert rule', "\n", 'Options :', "\n", $this->makeUsageOptionsList([ 'help' => 'This help.' ]), "\n", 'Example : -x=2 -y=6 \'x in [1, 2, 4] and x < y\'.', "\n", 'See $? to see the result (0 for true, > 0 for false).', "\n"; return; } } __halt_compiler(); Assert rules. ================================================ FILE: CHANGELOG.md ================================================ # 2.17.05.16 * Asserter: Can visit array-like dimensions. (Arne Groskurth, 2017-05-16T09:42:29+02:00) # 2.17.04.26 * Grammar: Logical operators are left-associative. (Ivan Enderlin, 2017-03-24T14:39:19+01:00) * CS: Fix copyright. (Ivan Enderlin, 2017-03-13T14:59:05+01:00) * Test: Support PHP 5.x syntax. (Ivan Enderlin, 2017-03-13T14:44:33+01:00) * CI: Set up Travis. (Ivan Enderlin, 2017-03-13T14:16:45+01:00) # 2.17.01.13 * Quality: Happy new year! (Alexis von Glasow, 2017-01-09T21:37:11+01:00) * Test: Add the `Decorrelated` interface. (Ivan Enderlin, 2016-10-25T07:57:09+02:00) # 2.16.10.24 * Documentation: New `README.md` file. (Ivan Enderlin, 2016-10-14T23:10:14+02:00) * Grammar: Chain dimensions on function. (Ivan Enderlin, 2016-10-14T08:37:12+02:00) * Documentation: Update `support` properties. (Ivan Enderlin, 2016-10-11T11:54:40+02:00) * Test: Update according to previous commit. (Ivan Enderlin, 2016-09-09T16:55:21+02:00) * Disassembly: Always add parenthesis around operators. (Ivan Enderlin, 2016-01-16T08:09:28+01:00) * Test: Add test cases for `…uler\Visitor\Asserter`. (Ivan Enderlin, 2016-09-09T08:04:49+02:00) # 2.16.09.07 * Test: Write `…\Ruler\Visitor\Asserter` test suite. (Ivan Enderlin, 2016-09-07T15:06:46+02:00) * Asserter: Fix an exception message. (Ivan Enderlin, 2016-09-07T15:04:19+02:00) * Test: Write `…ler\Visitor\Interpreter` test suite. (Ivan Enderlin, 2016-09-06T08:54:00+02:00) * Quality: Rename an internal variable. (Ivan Enderlin, 2016-09-06T08:02:30+02:00) * Test: Parameterized cases are usually protected. (Ivan Enderlin, 2016-09-06T08:00:43+02:00) * Disassembly: Escape the escaping symbol. (Ivan Enderlin, 2016-09-05T17:23:26+02:00) * Test: Write `…ler\Visitor\Disassembly` test suite. (Ivan Enderlin, 2016-09-05T17:22:16+02:00) * Test: Write `…\Ruler\Visitor\Compiler` test suite. (Ivan Enderlin, 2016-09-05T17:15:41+02:00) * Test: Write `Hoa\Ruler\Ruler` test suite. (Ivan Enderlin, 2016-09-05T08:56:36+02:00) * Ruler: Rename a namespace alias. (Ivan Enderlin, 2016-09-05T08:19:19+02:00) * Ruler: Remove the `interprete` method. (Ivan Enderlin, 2016-09-05T08:14:21+02:00) * Test: Update name for `Issue`. (Ivan Enderlin, 2016-09-05T08:10:18+02:00) * Quality: Run `hoa devtools:cs`. (Ivan Enderlin, 2016-09-05T08:09:13+02:00) * Test: Write `…r\Exception\Interpreter` test suite. (Ivan Enderlin, 2016-09-05T08:08:20+02:00) * Test: Write `…uler\Exception\Asserter` test suite. (Ivan Enderlin, 2016-09-05T08:07:31+02:00) * Test: Write `…ler\Exception\Exception` test suite. (Ivan Enderlin, 2016-09-05T08:05:36+02:00) * Test: Format to standard vocabulary. (Ivan Enderlin, 2016-09-05T08:04:39+02:00) * Test: Rename `CUT` to `SUT`. (Ivan Enderlin, 2016-09-05T08:02:49+02:00) * Test: Move `Documentation` as integration suite. (Ivan Enderlin, 2016-09-05T08:01:25+02:00) * Test: Write `Hoa\Ruler\Model\Model` test suite. (Ivan Enderlin, 2016-09-02T17:40:35+02:00) * Visitor: If the model is empty, compile to `''`. (Ivan Enderlin, 2016-09-02T17:34:27+02:00) * Test: Ensure recursivity applies onto array items. (Ivan Enderlin, 2016-09-02T17:19:51+02:00) * Test: Write `Hoa\Ruler\Model\Operator` test suite. (Ivan Enderlin, 2016-09-02T17:09:31+02:00) * Model: Use the public `getName` method. (Ivan Enderlin, 2016-09-02T17:08:48+02:00) * Model: Move set auto laziness to `setName`. (Ivan Enderlin, 2016-09-02T17:08:02+02:00) * Test: Move `…erator` as unit to integration suite. (Ivan Enderlin, 2016-09-02T07:49:40+02:00) * Documentation: Fix API documentation. (Ivan Enderlin, 2016-09-02T07:47:03+02:00) * Test: Write `…Ruler\Model\Bag\Context` test suite. (Ivan Enderlin, 2016-09-02T07:46:09+02:00) * Quality: Remove an unnecessary namespace alias. (Ivan Enderlin, 2016-08-30T17:03:58+02:00) * Test: Write `…er\Model\Bag\RulerArray` test suite. (Ivan Enderlin, 2016-08-30T17:03:38+02:00) * Test: Write `…\Ruler\Model\Bag\Scalar` test suite. (Ivan Enderlin, 2016-08-29T16:29:37+02:00) * Documentation: Fix API documentation. (Ivan Enderlin, 2016-08-29T16:29:16+02:00) * Test: Write `Hoa\Ruler\Model\Bag` test suite. (Ivan Enderlin, 2016-08-29T15:51:28+02:00) * Test: Use the `::class` constant. (Ivan Enderlin, 2016-08-29T15:49:09+02:00) # 2.16.04.06 * Asserter: Fix a wrong namespace access. (jroenf, 2016-04-06T09:09:43+02:00) # 2.16.03.15 * Composer: `hoa/protocol` is explicitly required. (Ivan Enderlin, 2016-01-18T22:14:18+01:00) * Grammar: Update copyright. (Ivan Enderlin, 2016-01-17T14:22:07+01:00) # 2.16.01.15 * Composer: Remove a useless dependency. (Ivan Enderlin, 2016-01-14T22:42:15+01:00) # 2.16.01.14 * Composer: New stable libraries. (Ivan Enderlin, 2016-01-14T22:13:39+01:00) # 2.16.01.11 * Quality: Drop PHP5.4. (Ivan Enderlin, 2016-01-11T09:15:26+01:00) * Quality: Run devtools:cs. (Ivan Enderlin, 2016-01-09T09:08:44+01:00) * Core: Remove `Hoa\Core`. (Ivan Enderlin, 2016-01-09T08:24:06+01:00) * Consistency: Use `Hoa\Consistency`. (Ivan Enderlin, 2015-12-08T21:50:12+01:00) * Exception: Use `Hoa\Exception`. (Ivan Enderlin, 2015-11-20T13:10:38+01:00) # 1.15.11.09 * Fix CS. (Ivan Enderlin, 2015-09-23T11:31:44+02:00) # 1.15.09.22 * Fix bad evaluation of `null` as an array key in the asserter. (Grummfy, 2015-09-22T15:47:23+02:00) # 1.15.09.08 * Add a `.gitignore` file. (Stéphane HULARD, 2015-08-03T11:45:52+02:00) * Add the `matches` operator. (Kévin Gomez, 2015-07-18T19:44:24+02:00) # 1.15.07.28 * Auto-box the expression to its bag representation. (Ivan Enderlin, 2015-07-13T11:37:00+02:00) * Fix a typo. (Ivan Enderlin, 2015-06-27T16:08:06+02:00) # 1.15.05.29 * Move to PSR-1 and PSR-2. (Ivan Enderlin, 2015-04-20T10:21:13+02:00) # 1.15.04.13 * Add the English documentation. (Ivan Enderlin, 2015-03-19T10:32:47+01:00) * Add the French documentation. (Ivan Enderlin, 2015-03-19T10:29:59+01:00) * Add the `CHANGELOG.md` file. (Ivan Enderlin, 2015-02-16T14:08:39+01:00) * Fix CS and API documentation. (Ivan Enderlin, 2015-02-06T10:37:23+01:00) * Add lazy operator support. (Alexis von Glasow, 2014-12-15T23:42:34+01:00) * Add tests for the dynamic callable. (Ivan Enderlin, 2015-02-05T17:13:12+01:00) # 1.15.02.05 * Sandbox function calls in the context. (Ivan Enderlin, 2015-02-05T16:50:13+01:00) * Add tests for the context. (Ivan Enderlin, 2015-02-05T16:49:30+01:00) # 1.15.02.02 * s/interprete/interpret/ (Ivan Enderlin, 2015-02-02T11:31:29+01:00) * `Ruler::interprete` is an alias to `Ruler::interpret` (simkimsia, 2015-01-16T22:22:18+08:00) * Improve type-hints in `Visitor\Asserter` (Alexis von Glasow, 2015-01-15T13:34:30+01:00) * Happy new year! (Ivan Enderlin, 2015-01-05T14:47:59+01:00) # 1.14.12.10 * Move to PSR-4. (Ivan Enderlin, 2014-12-09T18:45:18+01:00) # 1.14.12.09 * Fix a bug in the `Visitor\Compiler` when function has no argument (Catalin Criste, 2014-12-09T18:25:25+01:00) * Format namespace. (Ivan Enderlin, 2014-12-08T14:04:08+01:00) * Require `hoa/test`. (Alexis von Glasow, 2014-11-26T13:21:41+01:00) * `Hoa\Visitor` has been finalized. (Ivan Enderlin, 2014-11-15T22:28:07+01:00) # 1.14.11.10 * Avoid collisions with user-defined operators… (Ivan Enderlin, 2014-11-10T15:43:04+01:00) # 1.14.11.09 * Split the visitor into several methods (Stéphane PY, 2014-11-07T09:29:55+01:00) * Add tests for the documentation. (Ivan Enderlin, 2014-09-26T09:23:44+02:00) # 1.14.09.25 * Fix Fatal error. (Stéphane PY, 2014-09-25T12:22:18+02:00) * Add `branch-alias` (Stéphane PY, 2014-09-23T16:06:06+02:00) * `Hoa\Core` was missing (Ivan Enderlin, 2014-09-23T15:58:55+02:00) # 1.14.09.23 * First tag :-). (Ivan Enderlin, 2014-09-23T15:41:11+02:00) * Finalized! (Ivan Enderlin, 2014-09-23T15:37:04+02:00) * Remove `from`/`import` and update to PHP5.4. (Ivan Enderlin, 2014-09-23T15:32:36+02:00) * Declare array with […] and not (…). (Ivan Enderlin, 2014-09-23T14:58:18+02:00) # 0.14.09.17 * Drop PHP5.3. (Ivan Enderlin, 2014-09-17T17:13:16+02:00) * Add the installation section. (Ivan Enderlin, 2014-09-17T17:13:05+02:00) (first snapshot) ================================================ FILE: Context.php ================================================ _data = $data; return; } /** * Set a data. * * @param string $id ID. * @param mixed $value Value. * @return void */ public function offsetSet($id, $value) { $this->_data[$id] = $value; return; } /** * Get a data. * * @param string $id ID. * @return mixed * @throws \Hoa\Ruler\Exception */ public function offsetGet($id) { if (false === array_key_exists($id, $this->_data)) { throw new Exception( 'Identifier %s does not exist in the context.', 0, $id ); } $value = $this->_data[$id]; if ($value instanceof DynamicCallable) { return $value($this); } if (true === is_callable($value)) { if (true === is_string($value) && false === in_array(strtolower($value), get_defined_functions()['user'])) { return $value; } $value = $this->_data[$id] = $value($this); } return $value; } /** * Check if a data exists. * * @return bool */ public function offsetExists($id) { return true === array_key_exists($id, $this->_data); } /** * Unset a data. * * @param string $id ID. * @return void */ public function offsetUnset($id) { unset($this->_data[$id]); return; } /** * Get a data as context property * * @param string $name * @return mixed * @throws \Hoa\Ruler\Exception */ public function __get($name) { return $this->offsetGet($name); } /** * Set a data as context property * * @param string $id ID. * @param mixed $value Value. * @return void */ public function __set($id, $value) { $this->offsetSet($id, $value); } } ================================================ FILE: Documentation/En/Index.xyl ================================================

Business rules (like “all the customers that have reached 100€ in one time receive a voucher of 10% on the next purchase”) are most of the time defined outside the application. They are even often written in a different language than the used programming language. The Hoa\Ruler library provides an engine allowing to simply execute business rules while being efficient and very extensible.

Table of contents

Introduction

The business logic is very different from the Computer logic. “All the customers that have reached 100€ in one time receive a voucher of 10% on the next purchase”. This rule allows to access to certain parts of the program if valids. However, it can change at any moment. Most of the time, in a team, it is not the role of the developer to implement this rule. It will probably come from a business rules repository, which have been written by other persons, either manually or thanks to a third-party program. This implies that the language used to express a rule is not the language used to develop the program. An even more obvious example is the use of a rule to filter elements: An element is accepted if “its group is customer or guest and its number of points is greater than 30”. This rule can be written by a user via a command line interface to filter results from a database or logs.

This is important to understand that rules must be written in a dedicated language. Nevertheless, the way we use rules is very vast and unpredictable. This is why it is primordial to have flexible and extensible rules in the syntax. For instance, it should be allowed to add operators and functions: “All the customers from the hotel with a Gold pass will receive a voucher of 10%”. The “Gold pass” can be an operator or a function specific to the current business.

The language the Hoa\Ruler library uses to describe rules respects these constraints of extensibility. The rules will not be close to the human language but they will stay natural when reading. If we take the example of the “its group is customer or guest and its number of points is greater than 30”, it will be written: group in ["customer", "guest"] and points > 30. The group and points elements are variables of the rule. Their values will be defined in a context.

From a more formal point of view, a rule is a predicate, it means that its result is always a boolean: true or false. Because these rules are likely to be manipulated (modified) and executed, the Hoa\Ruler library provides several tools to work efficiently with these constraints, presented in the following sections.

Global workflow

The global workflow of the Hoa\Ruler library follows 3 steps:

  1. Defining a rule,
  2. Defining a context,
  3. Use of an asserter for the execution.

The rule is a string matching a specific syntax, which is described by the grammar of the language defined by the Hoa\Ruler library (detailed hereinafter). This rule contains variables whose values are defined by the context. The context can contain scalar values, arrays or even functions or objects. Finally, the asserter associates the context to the rule in order to execute it and to obtain a result. We remind you about the result which is necessarily a boolean. This is therefore a predicate.

The context is represented by the Hoa\Ruler\Context class. The asserter is represented by the Hoa\Ruler\Visitor\Asserter class. We can use the Hoa\Ruler\Ruler::assert method to ease its usage. Thus:

$ruler = new Hoa\Ruler\Ruler();

// 1. Write a rule.
$rule  = 'group in ["customer", "guest"] and points > 30';

// 2. Create a context.
$context           = new Hoa\Ruler\Context();
$context['group']  = 'customer';
$context['points'] = function () {
    return 42;
};

// 3. Assert!
var_dump(
    $ruler->assert($rule, $context)
);

/**
 * Will output:
 *     bool(true)
 */

The rule is defined in the $rule variable. The context, as far as it is concerned, is defined in the $context variable. It contains 2 variables: group and point, respectively with the 'customer' and 42 (returned by a function) values. Finally, the last step uses the Hoa\Ruler\Ruler::assert method to execute the $rule rule with the $context context (this latter is optional). The result is true because group belongs to the list customer or guest, and point is greater than 30. Change the values in the context or the rule to observe a different result.

The following sections detail the behavior of each step but the classical usage remains as simple as that!

Grammar (through examples)

The grammar of the rules language is described in the hoa://Library/Ruler/Grammar.pp file. This grammar is expressed with the PP language. To get more information, please see the Hoa\Compiler library. We clarify that the language supports Unicode. We are not going to explain the language whilst the grammar provides all the necessary details. Nevertheless, we are going to give several syntax examples.

Language of Hoa\Ruler through examples.
syntax semantics
'foo', "foo", 'f\'oo' strings
true, false, null pre-defined constants
4.2 a real
42 an integer
['foo', true, 4.2, 42] an array (heterogeneous)
sum(1, 2, 3) a call to the sum function with 3 arguments
points a variable
points['x'] an array access
line.pointA an object access (attribute)
line.length() a call to a method
and, or, xor, not logical operators
=, !=, >, &lt;, >=, &lt;= comparison operators
is, in membership operators

Of course, these examples represent atomic parts of the grammar that we can combine. Thus: userA.allows(groups[groupId][userB]) is valid. Just like f(user, points > 7 and points &lt; 42) which is also valid.

In actual fact, functions, comparison operators and membership operators are not defined by the grammar but by the asserter (detailed hereinafter). Hoa\Ruler does not make any difference between an operator and a function. Operators are considered as functions; an operator only being a function with an arity of 1 or 2. Thus, we can write 2 = 2 or =(2, 2), this will strictly produce the same result. Just like the name of functions that is not defined in the grammar, the name of operators is neither defined by the grammar, excepted for the logical operators that have a particular processing (because of the operator precedence). The immediate result is that we can create our own operators or functions. We can imagine aA, √(42) or even userA allows userB being valid expressions.

Context

The context defines values of variables present in rules. These values can be of kind:

  • Constants, like 42 or 'foo' which are scalars, or [1, 1, 2, 3, 5] or an object which are structured types,
  • Computed values, it means returned by a function or a method.

By default, the computed values are computed only once. Indeed, they are stored in a cache for performance reasons. If we would like to recompute them at each read, an encapsulation in an object of kind Hoa\Ruler\DynamicCallable is required.

Before detailing this part, let's present the context. A context is an instance of the Hoa\Ruler\Context class which implements the ArrayAccess interface. Thus, we use the context like an array:

$context        = new Hoa\Ruler\Context();
$context['key'] = 'value';

var_dump(
    isset($context['key']),
    $context['key']
);

/**
 * Will output:
 *     bool(true)
 *     string(5) "value"
 */

To register computed values, we can use a function; thus:

$context['computed'] = function () {
    return 42;
};

var_dump($context['computed']);

/**
 * Will output:
 *     int(42)
 */

We said computed values are stored in a cache on the first read. To illustrate this we will use a function incrementing an integer at each call:

$i              = 0;
$context['int'] = function () use (&amp;$i) {
    return ++$i;
};

var_dump(
    $context['int'],
    $context['int'],
    $context['int'],
    $i
);

/**
 * Will output:
 *     int(1)
 *     int(1)
 *     int(1)
 *     int(1)
 */

The $i variable has been incremented once to go from 0 to 1, and then, has been stored in a cache. Now, if we encapsulate this function in an instance of the Hoa\Ruler\DynamicCallable class, let's observe what happens:

$i              = 0;
$context['int'] = new Hoa\Ruler\DynamicCallable(
    function () use (&amp;$i) {
        return ++$i;
    }
);

var_dump(
    $context['int'],
    $context['int'],
    $context['int'],
    $i
);

/**
 * Will output:
 *     int(1)
 *     int(2)
 *     int(3)
 *     int(3)
 */

The result is no longer stored in a cache.

We can also use a declared function thanks to its name. Attention, it is not possible to call native PHP functions for security reasons. The context has not such a scope. Thus:

function answer()
{
    return 42;
}

$context['the_answer'] = 'answer';

var_dump($context['the_answer']);

/**
 * Will output:
 *     int(42)
 */

Nothing more to know about the context. It is not that complicated!

Asserter

Given a rule and a context, the asserter is responsible to compute the result of a rule, including the values of variables which are in the context.

The rule can have two different forms:

  • a string or
  • an object model.

If it is a string, it will be transformed into an object model automatically by the Hoa\Ruler\Ruler::assert method. This object model implements the interfaces of the Hoa\Visitor library and thus can be visited. This is why the Hoa\Ruler\Visitor\Asserter asserter is a visitor. Finally, the context is defined on the asserter with the Hoa\Ruler\Visitor\Asserter::setContext method. Thus:

$ruler             = new Hoa\Ruler\Ruler();
$rule              = 'points > 30';
$context           = new Hoa\Ruler\Context();
$context['points'] = 42;

// Define an asserter.
$asserter          = new Hoa\Ruler\Visitor\Asserter();

// Set this asserter on the ruler.
$ruler->setAsserter($asserter);

// Assert!
var_dump(
    $ruler->assert($rule, $context)
);

/**
 * Will output:
 *     bool(true)
 */

The Hoa\Ruler\Ruler::assert method will automatically define the context on the asserter.

Add functions

We said the names of the operators and of the functions in the rules are free. Therefore, we supposed the ability to define our own operators and functions. Let's add the logged function that tests if an object of kind User is connected. Here is this object:

class User
{
    const DISCONNECTED = 0;
    const CONNECTED    = 1;
    protected $_status = 1;

    public function getStatus()
    {
        return $this->_status;
    }
}

The implementation of the logged function might be the following:

$logged = function (User $user) {
    return $user::CONNECTED === $user->getStatus();
};

Finally, to declare this function, we will use the Hoa\Ruler\Visitor\Asserter::setOperator method. We can also cite the operatorExists, getOperator and getOperators methods which respectively allow to test if an operator exists, to get a previously declared operator and to get all the declared operators. Thus:

$ruler             = new Hoa\Ruler\Ruler();
$rule              = 'logged(user) and points > 30';
$context           = new Hoa\Ruler\Context();
$context['user']   = new User();
$context['points'] = 42;

// Declare the `logged` function.
$asserter = new Hoa\Ruler\Visitor\Asserter();
$asserter->setOperator('logged', $logged);

$ruler->setAsserter($asserter);

// Assert!
var_dump(
    $ruler->assert($rule, $context)
);

/**
 * Will output:
 *     bool(true)
 */

The Hoa\Ruler\Ruler class only contains methods to work faster and to hide the underlying mechanism (detailed hereinafter). One of them is the getDefaultAsserter static method which returns a unique instance of the Hoa\Ruler\Visitor\Asserter class. We can use this unique instance to define new operators for all the rules. Its usage is very similar to what we saw previously:

$ruler             = new Hoa\Ruler\Ruler();
$rule              = 'logged(user) and points > 30';
$context           = new Hoa\Ruler\Context();
$context['user']   = new User();
$context['points'] = 42;

// Declare the `logged` function.
$ruler->getDefaultAsserter()->setOperator('logged', $logged);

// Assert!
var_dump(
    $ruler->assert($rule, $context)
);

/**
 * Will output:
 *     bool(true)
 */

The Hoa\Ruler\Visitor\Asserter::setOperator method accepts any valid callable.

The and, or, xor, not, =, != and sum etc. operators are defined in the Hoa\Ruler\Visitor\Asserter class. Feel free to read it to get inspired!

Language transformation

The underlying mechanism hidden by the Hoa\Ruler\Ruler class is simple and very modular. The following sections detail the possible transformations, and associated usage, of the language.

First of all, the rule is interpreted by an interpreter to be transformed into an object model. Then, this object model is used by the asserter, or can be transformed into PHP code or transformed into a rule. This object model is the central point of the language, this is its most advanced form.

Interpreter: Language to object model

A rule is a string. To transform it into an object model, we will use the Hoa\Compiler library.

Thanks to the grammar of rules (defined in the hoa://Library/Ruler/Grammar.pp file), we will get an AST: An abstract syntax tree. For instance, for the rule points > 30, its AST is:

$ echo 'points > 30' | hoa compiler:pp hoa://Library/Ruler/Grammar.pp 0 --visitor dump
>  #expression
>  >  #operation
>  >  >  token(identifier, points)
>  >  >  token(identifier, >)
>  >  >  token(integer, 30)

In order to be exploitable, this tree will be transformed into an object model. This transformation is ensured by the Hoa\Ruler\Visitor\Interpreter visitor. Thus, if we should apply it manually:

$compiler    = Hoa\Compiler\Llk::load(
    new Hoa\File\Read('hoa://Library/Ruler/Grammar.pp')
);
$ast         = $compiler->parse('points > 30');
$interpreter = new Hoa\Ruler\Visitor\Interpreter();
$model       = $interpreter->visit($ast);

var_dump(
    get_class($model)
);

/**
 * Will output:
 *     string(21) "Hoa\Ruler\Model\Model"
 */

We learn that the model is represented by classes belonging to the Hoa\Ruler\Model namespace.

All these operations are replaced by the Hoa\Ruler\Ruler::interpret static method.

$model = Hoa\Ruler\Ruler::interpret('points > 30');

We can get the compiler with the Hoa\Ruler\Ruler::getCompiler method.

We will see how this step can be important to get better performances.

Compiler: Object model to PHP

The object model can be created manually by instantiating all the objects of kind Hoa\Ruler\Model\* and by combining them together.

The PHP code required for this operation can be automatically generated thanks to the Hoa\Ruler\Visitor\Compiler class. Thus:

$compiler = new Hoa\Ruler\Visitor\Compiler();
echo $compiler->visit($model);

/**
 * Will output:
 *     $model = new \Hoa\Ruler\Model();
 *     $model->expression =
 *         $model->{'>'}(
 *             $model->variable('points'),
 *             30
 *         );
 */

The generated code is simplified and optimized to be as short as possible whilst staying readable for a human.

We will see how this step can be important to get better performances.

Disassembler: Object model to language

So far, we have seen how to jump from a rule to its object model. The disassembler applies the opposite operation: It transforms an object model into a string.

The generated rule can differ a little bit from the original one in term of syntax (parenthesis, quotes, spacing…) but never in term of semantics. Thus:

$disassembly = new Hoa\Ruler\Visitor\Disassembly();
echo $disassembly->visit($model);

/**
 * Will output:
 *     (points > 30)
 */

Performances

Transforming a rule into an object model is not a low-cost operation. It becomes significant when applied thousand times per minute. Nevertheless, applying an asserter on an object model is a low-cost operation. We will present two ways to avoid the transformation of a rule into an object model.

Serialize the object model

Once the object model is present, we can serialize it with the help of the serialize PHP function. We will get a string representing instances of objects forming the object model. To get back to the object model and being able to apply an asserter, we will use the unserialize PHP function. The result of this serialization can be stored in a database instead of rules. This requires a little bit more space but let's remind that we can transform an object model to its rule easily thanks to the Hoa\Ruler\Visitor\Disassembly class, consequently this information is not lost. Thus:

$ruler             = new Hoa\Ruler\Ruler();
$rule              = 'points > 30';
$context           = new Hoa\Ruler\Context();
$context['points'] = 42;

// Nothing in the database.
if (null === $serialized = $database->get($ruleId)) {
    // We transform the rule into an object model.
    $model = Hoa\Ruler\Ruler::interpret($rule);

    // We serialize and save the object model.
    $database->save($ruleId, serialize($model));
} else {
    // We have a serialization! We unserialize it to get the object model.
    $model = unserialize($serialized);
}

// We can assert by using a model instead of a rule!
var_dump(
    $ruler->assert($model, $context)
);

/**
 * Will output:
 *     bool(true)
 */

This way, the rule is transformed into an object model only once!

Save and execute PHP code

Another way to avoid the transformation of a rule into its object model is to save the PHP code allowing to build the object model thanks to the Hoa\Ruler\Visitor\Compiler class. Once this PHP code stored and executed, we get back our object model.

However, executing such a PHP code will prove to be slightly slower and more difficult to deploy than the previous technique.

Conclusion

The Hoa\Ruler library defines a language of simple business rules inspired by the SQL language for the syntax. The language can be transformed in many ways: To an object model in order to be executed, or from this object model, into PHP code or into the original language. The instantiation of the variables present in the language relies on a context. All these operations are hidden through a simple and clear interface.

The performance aspect has been addressed and two solutions have been proposed. Today it is used by the industry on important projects, Hoa\Ruler is able to support heavy loads if these simple methodologies are applied.

================================================ FILE: Documentation/Fr/Index.xyl ================================================

Les règles métiers (comme « tous les clients qui ont dépensé plus de 100€ en une fois reçoivent une réduction de 10% sur leur prochain achat ») sont la plupart du temps définies à l'extérieur de l'application. Elles sont même souvent exprimées dans un langage différent que le langage de programmation utilisé. La bibliothèque Hoa\Ruler offre un moteur permettant d'exécuter des règles métiers simplement et de manière performante tout en restant très extensible.

Table des matières

Introduction

La logique métier est très différente de la logique Informatique. « Tous les clients qui ont dépensé plus de 100€ en une fois reçoivent une réduction de 10% sur leur prochain achat ». Cette règle permet d'accéder à certaines parties du programme si elle est validée. Toutefois, elle peut changer à n'importe quel moment. Très souvent, dans une équipe, ce ne sera pas au développeur d'implémenter cette règle. Elle proviendra probablement d'un dépôt de règles métiers, qui auront été écrites par d'autres personnes, soit manuellement, soit à l'aide d'un programme tiers. Cela implique que le langage utilisé pour exprimer une règle n'est pas le langage utilisé pour développer le programme. Un exemple encore plus évident avec l'utilisation d'une règle pour filtrer des éléments : un élément est accepté si « son groupe est customer ou guest et son nombre de points est supérieur à 30 ». Cette règle peut très bien être écrite par un utilisateur via une interface en ligne de commande pour filtrer des résultats d'une base de données ou de logs.

Il est important de comprendre que les règles doivent être écrites dans un langage dédié. Néanmoins, l'usage qui est fait des règles est très vaste et imprédictible. C'est pourquoi il est primordiale d'avoir des règles souples et extensibles dans leur syntaxe. Par exemple, il doit être permis d'ajouter des opérateurs ou des fonctions : « tous les clients de l'hôtel avec un pass Gold auront une réduction de 10% ». Le « pass Gold » peut être un opérateur ou une fonction spécifique au métier du programme concerné.

Le langage utilisé par la bibliothèque Hoa\Ruler pour décrire des règles respecte ces contraintes d'extensibilité. Les règles ne seront pas proches du langage humain mais resterons naturelles à lire. Si nous reprenons l'exemple de la règle « son groupe est customer ou guest et son nombre de points est supérieur à 30 », elle s'écrira : group in ["customer", "guest"] and points > 30. Les éléments group et points sont des variables de la règle. Leurs valeurs seront définies par un contexte.

D'un point de vue plus formel, une règle est un prédicat, c'est à dire que son résultat est toujours un booléen : true ou false. Comme ces règles sont destinées à être manipulées (modifiées) et exécutées, la bibliothèque Hoa\Ruler propose plusieurs outils pour travailler efficacement avec ces contraintes, présentés dans les sections suivantes.

Fonctionnement général

Le fonctionnement général de la bibliothèque Hoa\Ruler se déroule en 3 étapes :

  1. définition d'une règle ;
  2. définition d'un contexte ;
  3. usage d'un asserteur pour l'exécution.

La règle est une chaîne de caractères respectant une syntaxe précise, décrite par la grammaire du langage définie par la bibliothèque Hoa\Ruler (détaillée ci-après). Cette règle contient des variables dont les valeurs sont définies par le contexte. Le contexte peut contenir des valeurs scalaires, des tableaux ou même des fonctions et des objets. Enfin, l'asserteur associe le contexte à la règle pour pouvoir l'exécuter et obtenir un résultat. Nous rappelons que ce résultat est nécessairement un booléen. C'est alors un prédicat.

Le contexte est représenté par la classe Hoa\Ruler\Context. L'asserteur est représenté par la classe Hoa\Ruler\Visitor\Asserter. Nous pouvons employer la méthode Hoa\Ruler\Ruler::assert qui facilite son utilisation. Ainsi :

$ruler = new Hoa\Ruler\Ruler();

// 1. Write a rule.
$rule  = 'group in ["customer", "guest"] and points > 30';

// 2. Create a context.
$context           = new Hoa\Ruler\Context();
$context['group']  = 'customer';
$context['points'] = function () {
    return 42;
};

// 3. Assert!
var_dump(
    $ruler->assert($rule, $context)
);

/**
 * Will output:
 *     bool(true)
 */

La règle est définie dans la variable $rule. Le contexte, quant à lui, est défini dans la variable $context. Il contient 2 variables : group et points, respectivement avec les valeurs 'customer' et 42 (retournée par une fonction). Enfin, la dernière étape utilise la méthode Hoa\Ruler\Ruler::assert pour exécuter la règle $rule avec le contexte $context (ce dernier est optionnel). Le résultat est true car group est bien dans la liste customer ou guest, et point est bien supérieur à 30. Changez les valeurs dans le contexte ou la règle pour observer un résultat différent.

Les sections suivantes détaillent le fonctionnement de chaque partie mais l'usage classique reste aussi simple que ça !

Grammaire (par l'exemple)

La grammaire du langage des règles est décrite dans le fichier hoa://Library/Ruler/Grammar.pp. Cette grammaire est exprimée avec le langage PP. Pour plus d'informations, voir la bibliothèque Hoa\Compiler. Nous précisons que le langage supporte Unicode. Nous n'allons pas expliquer le langage alors que la grammaire donne tous les détails nécessaires. En revanche, nous allons donner plusieurs exemples de syntaxe.

Langage de Hoa\Ruler par l'exemple.
syntaxe sémantique
'foo', "foo", 'f\'oo' des chaînes de caractères
true, false, null des constantes pré-définies
4.2 un réel
42 un entier
['foo', true, 4.2, 42] un tableau (hétérogène)
sum(1, 2, 3) un appel de la fonction sum avec 3 arguments
points une variable
points['x'] un accès tableau
line.pointA un accès objet (attribut)
line.length() un appel à une méthode
and, or, xor, not des opérateurs logiques
=, !=, >, &lt;, >=, &lt;= des opérateurs de comparaisons
is, in des opérateurs d'appartenance

Bien sûr, ces exemples représentent des parties atomiques de la grammaire que nous pouvons combiner. Ainsi : userA.allows(groups[groupId][userB]) est valide. De même que f(user, points > 7 and points &lt; 42) est également valide.

En réalité, les fonctions, les opérateurs de comparaisons et les opérateurs d'appartenance ne sont pas définis par la grammaire mais par l'asserteur (détaillé ci-après). Hoa\Ruler ne fait pas la différence entre un opérateur et une fonction. Les opérateurs sont considérés comme des fonctions ; un opérateur n'étant qu'une fonction d'arité 1 ou 2. Ainsi, nous pouvons écrire 2 = 2 ou =(2, 2), cela produira strictement le même résultat. Tout comme le nom des fonctions qui n'est pas défini par la grammaire, le nom des opérateurs n'est lui non plus pas défini par la grammaire, excepté pour les opérateurs logiques qui ont un traitement particuliers (à cause de la précédence des opérateurs). Cela a pour effet de pouvoir créer nos propres opérateurs ou fonctions. Nous pouvons imaginer aA, √(42) ou encore userA allows userB comme étant des expressions valides.

Contexte

Le contexte définit les valeurs des variables présentes dans des règles. Ces valeurs peuvent être :

  • des constantes, comme 42 ou 'foo' qui sont des scalaires, ou [1, 1, 2, 3, 5] ou un objet qui sont des types structurés ;
  • des valeurs calculées, c'est à dire retournée par une fonction ou une méthode.

Par défaut, les valeurs calculées ne le sont qu'une seule fois. En effet, elles sont placées dans un cache pour des raisons de performance. Si nous voulons les recalculer à chaque lecture, il faudra les encapsuler dans un objet de type Hoa\Ruler\DynamicCallable.

Avant de détailler cette partie, présentons le contexte. Un contexte est une instance de la classe Hoa\Ruler\Context qui implémente l'interface ArrayAccess. Ainsi nous utilisons le contexte comme un tableau :

$context        = new Hoa\Ruler\Context();
$context['key'] = 'value';

var_dump(
    isset($context['key']),
    $context['key']
);

/**
 * Will output:
 *     bool(true)
 *     string(5) "value"
 */

Pour y déposer des valeurs calculées, nous pouvons le faire via une fonction ; ainsi :

$context['computed'] = function () {
    return 42;
};

var_dump($context['computed']);

/**
 * Will output:
 *     int(42)
 */

Nous avons dit que les valeurs calculées sont placées en cache dès la première lecture. Pour l'illustrer, nous allons utiliser une fonction qui incrémente un entier à chaque appel :

$i              = 0;
$context['int'] = function () use (&amp;$i) {
    return ++$i;
};

var_dump(
    $context['int'],
    $context['int'],
    $context['int'],
    $i
);

/**
 * Will output:
 *     int(1)
 *     int(1)
 *     int(1)
 *     int(1)
 */

La variable $i a été incrémentée une seule fois pour passer de 0 à 1, puis elle a été placée en cache. Maintenant, si nous encapsulons cette fonction dans une instance de la classe Hoa\Ruler\DynamicCallable, observons ce qu'il se passe :

$i              = 0;
$context['int'] = new Hoa\Ruler\DynamicCallable(
    function () use (&amp;$i) {
        return ++$i;
    }
);

var_dump(
    $context['int'],
    $context['int'],
    $context['int'],
    $i
);

/**
 * Will output:
 *     int(1)
 *     int(2)
 *     int(3)
 *     int(3)
 */

Le résultat n'est plus mis en cache.

Nous pouvons également utiliser une fonction déclarée grâce à son nom. Attention toutefois, il sera impossible d'appeler les fonctions natives de PHP pour des raisons de sécurité. Le contexte n'a pas une telle portée. Ainsi :

function answer()
{
    return 42;
}

$context['the_answer'] = 'answer';

var_dump($context['the_answer']);

/**
 * Will output:
 *     int(42)
 */

C'est tout ce qu'il faut savoir sur le contexte. Ce n'est pas plus compliqué que ça !

Asserteur

Étant donné une règle et un contexte, l'asserteur est chargé de calculer le résultat de cette règle, dont les valeurs des variables sont dans le contexte.

La règle peut avoir deux formes possibles :

  • une chaîne de caractère ou
  • un modèle objet.

Si c'est une chaîne de caractère, elle sera transformée en modèle objet automatiquement par la méthode Hoa\Ruler\Ruler::assert. Ce modèle objet implémente les interfaces de la bibliothèque Hoa\Visitor et peut donc être visité. C'est pourquoi l'asserteur Hoa\Ruler\Visitor\Asserter est un visiteur. Le contexte quant à lui est défini auprès de l'asserteur avec la méthode Hoa\Ruler\Visitor\Asserter::setContext. Ainsi :

$ruler             = new Hoa\Ruler\Ruler();
$rule              = 'points > 30';
$context           = new Hoa\Ruler\Context();
$context['points'] = 42;

// Define an asserter.
$asserter          = new Hoa\Ruler\Visitor\Asserter();

// Set this asserter on the ruler.
$ruler->setAsserter($asserter);

// Assert!
var_dump(
    $ruler->assert($rule, $context)
);

/**
 * Will output:
 *     bool(true)
 */

La méthode Hoa\Ruler\Ruler::assert va automatiquement définir le contexte auprès de l'asserteur.

Ajout de fonctions

Nous avons précisé que les noms des opérateurs et des fonctions dans les règles sont libres. Ainsi, nous avons évoqué la possibilité de définir nos propres opérateurs et fonctions. Ajoutons la fonction logged qui teste si un objet de type User est connecté. Voici cet objet :

class User
{
    const DISCONNECTED = 0;
    const CONNECTED    = 1;
    protected $_status = 1;

    public function getStatus()
    {
        return $this->_status;
    }
}

L'implémentation de la fonction logged serait alors :

$logged = function (User $user) {
    return $user::CONNECTED === $user->getStatus();
};

Enfin, pour déclarer cette fonction, nous allons utiliser la méthode Hoa\Ruler\Visitor\Asserter::setOperator. Nous pouvons aussi citer les méthodes operatorExists, getOperator et getOperators qui permettent respectivement de tester si un opérateur existe, d'obtenir un opérateur précédemment déclaré et d'obtenir tous les opérateurs déclarés. Ainsi :

$ruler             = new Hoa\Ruler\Ruler();
$rule              = 'logged(user) and points > 30';
$context           = new Hoa\Ruler\Context();
$context['user']   = new User();
$context['points'] = 42;

// Declare the `logged` function.
$asserter = new Hoa\Ruler\Visitor\Asserter();
$asserter->setOperator('logged', $logged);

$ruler->setAsserter($asserter);

// Assert!
var_dump(
    $ruler->assert($rule, $context)
);

/**
 * Will output:
 *     bool(true)
 */

La classe Hoa\Ruler\Ruler ne contient que des méthodes pour travailler plus vite et cacher le mécanisme sous-jacent (détaillé ci-après). Elle contient entre autre la méthode statique getDefaultAsserter qui retourne une instance unique de la classe Hoa\Ruler\Visitor\Asserter. Nous pouvons utiliser cette instance unique pour définir des nouveaux opérateurs pour toutes les règles. Son utilisation est très similaire à ce que nous avons vu précédemment :

$ruler             = new Hoa\Ruler\Ruler();
$rule              = 'logged(user) and points > 30';
$context           = new Hoa\Ruler\Context();
$context['user']   = new User();
$context['points'] = 42;

// Declare the `logged` function.
$ruler->getDefaultAsserter()->setOperator('logged', $logged);

// Assert!
var_dump(
    $ruler->assert($rule, $context)
);

/**
 * Will output:
 *     bool(true)
 */

La méthode Hoa\Ruler\Visitor\Asserter::setOperator utilise n'importe quel callable valide.

Les opérateurs and, or, xor, not, =, !=, sum etc. sont définis dans la classe Hoa\Ruler\Visitor\Asserter. N'hésitez pas à vous en inspirer !

Transformation du langage

Le mécanisme sous-jacent caché par la classe Hoa\Ruler\Ruler est simple et très modulaire. Les sections suivantes détaillent les transformations possibles du langage et leur utilisation.

Tout d'abord, la règle est interprétée par un interpréteur pour être transformée en modèle objet. Ensuite, ce modèle objet est utilisé par l'asserteur, ou peut être transformé en code PHP ou transformé en une règle. Le modèle objet est le point central du langage, c'est sa forme la plus avancée.

Interpréteur : langage vers modèle objet

Une règle est une chaîne de caractères. Pour la transformer en modèle objet, nous utilisons la bibliothèque Hoa\Compiler.

Grâce à la grammaire des règles (définie dans le fichier hoa://Library/Ruler/Grammar.pp), nous allons obtenir un AST : un arbre abstrait. Par exemple, pour la règle points > 30, son AST est :

$ echo 'points > 30' | hoa compiler:pp hoa://Library/Ruler/Grammar.pp 0 --visitor dump
>  #expression
>  >  #operation
>  >  >  token(identifier, points)
>  >  >  token(identifier, >)
>  >  >  token(integer, 30)

Pour que cet arbre soit exploitable plus facilement, il va être transformé en modèle objet. Cette transformation est assurée par la classe Hoa\Ruler\Visitor\Interpreter. Ainsi, si nous devions le faire manuellement :

$compiler    = Hoa\Compiler\Llk::load(
    new Hoa\File\Read('hoa://Library/Ruler/Grammar.pp')
);
$ast         = $compiler->parse('points > 30');
$interpreter = new Hoa\Ruler\Visitor\Interpreter();
$model       = $interpreter->visit($ast);

var_dump(
    get_class($model)
);

/**
 * Will output:
 *     string(21) "Hoa\Ruler\Model\Model"
 */

Nous apprenons alors que le modèle est représenté par les classes appartenant à l'espace de nom Hoa\Ruler\Model.

Toutes ces opérations sont remplacées par la méthode statique Hoa\Ruler\Ruler::interpret :

$model = Hoa\Ruler\Ruler::interpret('points > 30');

Nous pouvons obtenir le compilateur avec la méthode Hoa\Ruler\Ruler::getCompiler.

Nous verrons comment cette étape peut être importante pour améliorer les performances.

Compilateur : modèle objet vers PHP

Le modèle objet peut être créé manuellement en instanciant tous les objets de type Hoa\Ruler\Model\* et en les combinant ensemble.

Le code PHP nécessaire à cette opération peut être automatiquement généré grâce à la classe Hoa\Ruler\Visitor\Compiler. Ainsi :

$compiler = new Hoa\Ruler\Visitor\Compiler();
echo $compiler->visit($model);

/**
 * Will output:
 *     $model = new \Hoa\Ruler\Model();
 *     $model->expression =
 *         $model->{'>'}(
 *             $model->variable('points'),
 *             30
 *         );
 */

Le code généré est simplifié et optimisé pour être le plus court possible tout en restant lisible par un être humain.

Nous verrons comment cette étape peut être importante pour améliorer les performances.

Désassembleur : modèle objet vers langage

Jusqu'à maintenant, nous avons vu comment passer d'une règle vers son modèle objet. Le désassembleur applique l'opération inverse : il transforme un modèle objet vers une chaîne de caractères.

La règle générée peut différer un peu de la règle originale syntaxiquement (parenthèses, guillemets, espacements…) mais jamais sémantiquement. Ainsi :

$disassembly = new Hoa\Ruler\Visitor\Disassembly();
echo $disassembly->visit($model);

/**
 * Will output:
 *     (points > 30)
 */

Performances

Transformer une règle vers un modèle objet a un coût. Ce coût devient significatif si l'opération est appliquée des milliers de fois à la minute. En revanche, appliquer un asserteur sur un modèle objet n'est pas coûteux. Nous allons donc présenter deux façons d'éviter la transformation d'une règle vers un modèle objet.

Sérialiser le modèle objet

Une fois le modèle objet obtenu, nous pouvons le sérialiser à l'aide de la fonction PHP serialize. Nous allons obtenir une chaîne de caractères représentant les instances des objets constituant le modèle objet. Pour obtenir à nouveau le modèle objet et être capable d'y appliquer un asserteur, nous utiliserons la fonction PHP unserialize. Le résultat de la sérialisation peut être stocké en base de données à la place des règles. Cela nécessite un peu plus de place mais rappelons que nous pouvons transformer le modèle objet vers sa règle facilement grâce à la classe Hoa\Ruler\Visitor\Disassembly, cette information n'est donc pas perdue. Ainsi :

$ruler             = new Hoa\Ruler\Ruler();
$rule              = 'points > 30';
$context           = new Hoa\Ruler\Context();
$context['points'] = 42;

// Nothing in the database.
if (null === $serialized = $database->get($ruleId)) {
    // We transform the rule into an object model.
    $model = Hoa\Ruler\Ruler::interpret($rule);

    // We serialize and save the object model.
    $database->save($ruleId, serialize($model));
} else {
    // We have a serialization! We unserialize it to get the object model.
    $model = unserialize($serialized);
}

// We can assert by using a model instead of a rule!
var_dump(
    $ruler->assert($model, $context)
);

/**
 * Will output:
 *     bool(true)
 */

De cette manière, la règle n'est transformée en modèle objet qu'une seule fois !

Enregistrer et exécute du code PHP

Une autre façon d'éviter la transformation d'une règle en modèle objet est d'enregistrer le code PHP permettant de construire le modèle objet grâce à la classe Hoa\Ruler\Visitor\Compiler. Une fois ce code PHP enregistré et exécuté, nous retrouverons notre modèle objet.

Toutefois, exécuter un tel code PHP s'avèrera légèrement plus lent et plus difficile à mettre en œuvre que la technique précédente.

Conclusion

La bibliothèque Hoa\Ruler définit un langage de règles métiers simple et inspiré du langage SQL dans sa syntaxe. Le langage peut être transformé de plusieurs façons : vers un modèle objet pour qu'il puisse être exécuté, ou à partir de ce modèle, vers du code PHP ou vers le langage d'origine. L'instanciation des variables dans ce langage se fait à travers un contexte. Toutes ces opérations sont cachées à travers une interface simple et claire.

La question des performances a été abordée et deux solutions sont proposées. Aujourd'hui utilisé dans l'industrie sur de gros projets, Hoa\Ruler est capable de supporter de lourdes charges si ces pratiques simples sont mises en œuvre.

================================================ FILE: DynamicCallable.php ================================================ logical_operation_primary() #operation )? operand: ::parenthesis_:: logical_operation_primary() ::_parenthesis:: | value() value: ::not:: logical_operation_primary() #not | | | | | | | array_declaration() | chain() chain: ( variable() | function_call() ) ( ( array_access() | object_access() ) #variable_access )* variable: #array_access: ::bracket_:: value() ::_bracket:: object_access: ::dot:: ( #attribute_access | function_call() #method_access ) #array_declaration: ::bracket_:: value() ( ::comma:: value() )* ::_bracket:: #function_call: ::parenthesis_:: ( logical_operation_primary() ( ::comma:: logical_operation_primary() )* )? ::_parenthesis:: ================================================ FILE: Model/Bag/Bag.php ================================================ visit($this, $handle, $eldnah); } } /** * Flex entity. */ Consistency::flexEntity('Hoa\Ruler\Model\Bag\Bag'); ================================================ FILE: Model/Bag/Context.php ================================================ _id = $id; return; } /** * Call an index (variable[indexA][indexB][indexC]). * * @param mixed $index Index (a bag, a scalar or an array). * @return \Hoa\Ruler\Model\Bag\Context */ public function index($index) { if (is_scalar($index) || null === $index) { $index = new Scalar($index); } elseif (is_array($index)) { $index = new RulerArray($index); } $this->_dimensions[] = [ static::ACCESS_TYPE => static::ARRAY_ACCESS, static::ACCESS_VALUE => $index ]; return $this; } /** * Call an attribute (variable.attrA.attrB). * * @param string $attribute Attribute name. * @return \Hoa\Ruler\Model\Bag\Context */ public function attribute($attribute) { $this->_dimensions[] = [ static::ACCESS_TYPE => static::ATTRIBUTE_ACCESS, static::ACCESS_VALUE => $attribute ]; return $this; } /** * Call a method (variable.foo().bar().baz()). * * @param \Hoa\Ruler\Model\Operator $method Method to call. * @return \Hoa\Ruler\Model\Bag\Context */ public function call(Ruler\Model\Operator $method) { $this->_dimensions[] = [ static::ACCESS_TYPE => static::METHOD_ACCESS, static::ACCESS_VALUE => $method ]; return $this; } /** * Get all dimensions. * * @return array */ public function getDimensions() { return $this->_dimensions; } /** * Get ID. * * @return string */ public function getId() { return $this->_id; } } ================================================ FILE: Model/Bag/RulerArray.php ================================================ _array = $data; return; } /** * Get array. * * @return array */ public function getArray() { return $this->_array; } } ================================================ FILE: Model/Bag/Scalar.php ================================================ _value = $value; return; } /** * Get content of the bag. * * @return scalar */ public function getValue() { return $this->_value; } } ================================================ FILE: Model/Model.php ================================================ $name = $value; } if (is_scalar($value)) { $value = new Bag\Scalar($value); } elseif (is_array($value)) { $value = new Bag\RulerArray($value); } $this->_root = $value; return; } /** * Get the expression. * * @return \Hoa\Ruler\Model\Operator */ public function getExpression() { return $this->_root; } /** * Declare a function. * * @param string $name Name. * @param mixed … * @return \Hoa\Ruler\Model\Operator */ public function func() { $arguments = func_get_args(); $name = array_shift($arguments); return $this->_operator($name, $arguments, true); } /** * Declare an operation. * * @param string $name Name. * @param array $arguments Arguments. * @return \Hoa\Ruler\Model\Operator */ public function operation($name, array $arguments) { return $this->_operator($name, $arguments, false); } /** * Create an operator object. * * @param string $name Name. * @param array $arguments Arguments. * @param bool $isFunction Whether it is a function or not. * @return \Hoa\Ruler\Model\Operator */ public function _operator($name, array $arguments, $isFunction) { return new Operator(mb_strtolower($name), $arguments, $isFunction); } /** * Declare an operation. * * @param string $name Name. * @param array $arguments Arguments. * @return \Hoa\Ruler\Model\Operator */ public function __call($name, array $arguments) { return $this->operation($name, $arguments); } /** * Declare a variable. * * @parma string $id ID. * @return \Hoa\Ruler\Model\Bag\Context */ public function variable($id) { return new Bag\Context($id); } /** * Accept a visitor. * * @param \Hoa\Visitor\Visit $visitor Visitor. * @param mixed &$handle Handle (reference). * @param mixed $eldnah Handle (no reference). * @return mixed */ public function accept( Visitor\Visit $visitor, &$handle = null, $eldnah = null ) { return $visitor->visit($this, $handle, $eldnah); } /** * Transform the object as a string. * * @return string */ public function __toString() { if (null === static::$_compiler) { static::$_compiler = new Ruler\Visitor\Compiler(); } return static::$_compiler->visit($this); } } /** * Flex entity. */ Consistency::flexEntity('Hoa\Ruler\Model\Model'); ================================================ FILE: Model/Operator.php ================================================ setName($name); $this->setArguments($arguments); $this->setFunction($isFunction); return; } /** * Set name. * * @param string $name Name. * @return string */ protected function setName($name) { $old = $this->_name; $this->_name = $name; $this->setLaziness('and' === $name || 'or' === $name); return $old; } /** * Get name. * * @return string */ public function getName() { return $this->_name; } /** * Set arguments. * * @param array $arguments Arguments. * @return array */ protected function setArguments(array $arguments) { foreach ($arguments as &$argument) { if (is_scalar($argument) || null === $argument) { $argument = new Bag\Scalar($argument); } elseif (is_array($argument)) { $argument = new Bag\RulerArray($argument); } } $old = $this->_arguments; $this->_arguments = $arguments; return $old; } /** * Get arguments. * * @return array */ public function getArguments() { return $this->_arguments; } /** * Set whether the operator is a function or not. * * @param bool $isFunction Is a function or not. * @return bool */ protected function setFunction($isFunction) { $old = $this->_function; $this->_function = $isFunction; return $old; } /** * Check if the operator is a function or not. * * @return bool */ public function isFunction() { return $this->_function; } /** * Set whether the operator is lazy or not. * * @param bool $isLazy Is a lazy operator or not. * @return bool */ protected function setLaziness($isLazy) { $old = $this->_laziness; $this->_laziness = $isLazy; return $old; } /** * Check if the operator is lazy or not. * * @return bool */ public function isLazy() { return $this->_laziness; } /** * Check whether we should break the lazy evaluation or not. * * @param mixed $value Value to check. * @return bool */ public function shouldBreakLazyEvaluation($value) { switch ($this->getName()) { case 'and': if (false === $value) { return self::LAZY_BREAK; } break; case 'or': if (true === $value) { return self::LAZY_BREAK; } break; } return self::LAZY_CONTINUE; } /** * Check if the operator is a token of the grammar or not. * * @param string $operator Operator. * @return bool */ public static function isToken($operator) { static $_tokens = ['not', 'and', 'or', 'xor']; return true === in_array($operator, $_tokens); } /** * Accept a visitor. * * @param \Hoa\Visitor\Visit $visitor Visitor. * @param mixed &$handle Handle (reference). * @param mixed $eldnah Handle (no reference). * @return mixed */ public function accept( Visitor\Visit $visitor, &$handle = null, $eldnah = null ) { return $visitor->visit($this, $handle, $eldnah); } } ================================================ FILE: README.md ================================================

Hoa

---

Build status Code coverage Packagist License

Hoa is a modular, extensible and structured set of PHP libraries.
Moreover, Hoa aims at being a bridge between industrial and research worlds.

# Hoa\Ruler [![Help on IRC](https://img.shields.io/badge/help-%23hoaproject-ff0066.svg)](https://webchat.freenode.net/?channels=#hoaproject) [![Help on Gitter](https://img.shields.io/badge/help-gitter-ff0066.svg)](https://gitter.im/hoaproject/central) [![Documentation](https://img.shields.io/badge/documentation-hack_book-ff0066.svg)](https://central.hoa-project.net/Documentation/Library/Ruler) [![Board](https://img.shields.io/badge/organisation-board-ff0066.svg)](https://waffle.io/hoaproject/ruler) This library allows to manipulate a rule engine. Rules can be written by using a dedicated language, very close to SQL. Therefore, they can be written by a user and saved in a database. Such rules are useful, for example, for commercial solutions that need to manipulate promotion or special offer rules written by a user. To quote [Wikipedia](https://en.wikipedia.org/wiki/Business_rules_engine): > A business rules engine is a software system that executes one or more > business rules in a runtime production environment. The rules might come from > legal regulation (“An employee can be fired for any reason or no reason but > not for an illegal reason”), company policy (“All customers that spend more > than $100 at one time will receive a 10% discount”), or other sources. A > business rule system enables these company policies and other operational > decisions to be defined, tested, executed and maintained separately from > application code. [Learn more](https://central.hoa-project.net/Documentation/Library/Ruler). ## Installation With [Composer](https://getcomposer.org/), to include this library into your dependencies, you need to require [`hoa/ruler`](https://packagist.org/packages/hoa/ruler): ```sh $ composer require hoa/ruler '~2.0' ``` For more installation procedures, please read [the Source page](https://hoa-project.net/Source.html). ## Testing Before running the test suites, the development dependencies must be installed: ```sh $ composer install ``` Then, to run all the test suites: ```sh $ vendor/bin/hoa test:run ``` For more information, please read the [contributor guide](https://hoa-project.net/Literature/Contributor/Guide.html). ## Quick usage As a quick overview, we propose to see a very simple example that manipulates a simple rule with a simple context. After, we will add a new operator in the rule. And finally, we will see how to save a rule in a database. ### Three steps So first, we create a context with two variables: `group` and `points`, and we then assert a rule. A context holds values to concretize a rule. A value can also be the result of a callable. Thus: ```php $ruler = new Hoa\Ruler\Ruler(); // 1. Write a rule. $rule = 'group in ["customer", "guest"] and points > 30'; // 2. Create a context. $context = new Hoa\Ruler\Context(); $context['group'] = 'customer'; $context['points'] = function () { return 42; }; // 3. Assert! var_dump( $ruler->assert($rule, $context) ); /** * Will output: * bool(true) */ ``` In the next example, we have a `User` object and a context that is populated dynamically (when the `user` variable is concretized, two new variables, `group` and `points` are created). Moreover, we will create a new operator/function called `logged`. There is no difference between an operator and a function except that an operator has two operands (so arguments). ### Adding operators and functions For now, we have the following operators/functions by default: `and`, `or`, `xor`, `not`, `=` (`is` as an alias), `!=`, `>`, `>=`, `<`, `<=`, `in` and `sum`. We can add our own by different way. The simplest and volatile one is given in the following example. Thus: ```php // The User object. class User { const DISCONNECTED = 0; const CONNECTED = 1; public $group = 'customer'; public $points = 42; protected $_status = 1; public function getStatus() { return $this->_status; } } $ruler = new Hoa\Ruler\Ruler(); // New rule. $rule = 'logged(user) and group in ["customer", "guest"] and points > 30'; // New context. $context = new Hoa\Ruler\Context(); $context['user'] = function () use ($context) { $user = new User(); $context['group'] = $user->group; $context['points'] = $user->points; return $user; }; // We add the logged() operator. $ruler->getDefaultAsserter()->setOperator('logged', function (User $user) { return $user::CONNECTED === $user->getStatus(); }); // Finally, we assert the rule. var_dump( $ruler->assert($rule, $context) ); /** * Will output: * bool(true) */ ``` Also, if a variable in the context is an array, we can access to its values from a rule with the same syntax as PHP. For example, if the `a` variable is an array, we can write `a[0]` to access to the value associated to the `0` key. It works as an hashmap (PHP array implementation), so we can have strings & co. as keys. In the same way, if a variable is an object, we can call a method on it. For example, if the `a` variable is an array where the value associated to the first key is an object with a `foo` method, we can write: `a[0].foo(b)` where `b` is another variable in the context. Also, we can access to the public attributes of an object. Obviously, we can mixe array and object accesses. Please, take a look at the grammar (`hoa://Library/Ruler/Grammar.pp`) to see all the possible constructions. ### Saving a rule Now, we have two options to save the rule, for example, in a database. Either we save the rule as a string directly, or we will save the serialization of the rule which will avoid further interpretations. In the next example, we see how to serialize and unserialize a rule by using the `Hoa\Ruler\Ruler::interpret` static method: ```php $database->save( serialize( Hoa\Ruler\Ruler::interpret( 'logged(user) and group in ["customer", "guest"] and points > 30' ) ) ); ``` And for next executions: ```php $rule = unserialize($database->read()); var_dump( $ruler->assert($rule, $context) ); ``` When a rule is interpreted, its object model is created. We serialize and unserialize this model. To see the PHP code needed to create such a model, we can print the model itself (as an example). Thus: ```php echo Hoa\Ruler\Ruler::interpret( 'logged(user) and group in ["customer", "guest"] and points > 30' ); /** * Will output: * $model = new \Hoa\Ruler\Model(); * $model->expression = * $model->and( * $model->func( * 'logged', * $model->variable('user') * ), * $model->and( * $model->in( * $model->variable('group'), * [ * 'customer', * 'guest' * ] * ), * $model->{'>'}( * $model->variable('points'), * 30 * ) * ) * ); */ ``` Have fun! ## Documentation The [hack book of `Hoa\Ruler`](https://central.hoa-project.net/Documentation/Library/Ruler) contains detailed information about how to use this library and how it works. To generate the documentation locally, execute the following commands: ```sh $ composer require --dev hoa/devtools $ vendor/bin/hoa devtools:documentation --open ``` More documentation can be found on the project's website: [hoa-project.net](https://hoa-project.net/). ## Getting help There are mainly two ways to get help: * On the [`#hoaproject`](https://webchat.freenode.net/?channels=#hoaproject) IRC channel, * On the forum at [users.hoa-project.net](https://users.hoa-project.net). ## Contribution Do you want to contribute? Thanks! A detailed [contributor guide](https://hoa-project.net/Literature/Contributor/Guide.html) explains everything you need to know. ## License Hoa is under the New BSD License (BSD-3-Clause). Please, see [`LICENSE`](https://hoa-project.net/LICENSE) for details. ## Related projects The following projects are using this library: * [RulerZ](https://github.com/K-Phoen/rulerz), Powerful implementation of the Specification pattern in PHP, * [ownCloud](https://owncloud.org/), A safe home for all your data, * [PhpMetrics](http://www.phpmetrics.org/), Static analysis tool for PHP, * [`hiqdev/php-billing`](https://github.com/hiqdev/php-billing), a billing library in PHP * [`atoum/ruler-extension`](https://github.com/atoum/ruler-extension), This extension allows to filter test results in [atoum](http://atoum.org/). ================================================ FILE: Ruler.php ================================================ getAsserter($context)->visit($rule); } /** * Short interpreter. * * @param string $rule Rule. * @return \Hoa\Ruler\Model * @throws \Hoa\Ruler\Exception */ public static function interpret($rule) { return static::getInterpreter()->visit( static::getCompiler()->parse($rule) ); } /** * Get interpreter. * * @return \Hoa\Ruler\Visitor\Interpreter */ public static function getInterpreter() { if (null === static::$_interpreter) { static::$_interpreter = new Visitor\Interpreter(); } return static::$_interpreter; } /** * Set current asserter. * * @param \Hoa\Visitor\Visit $visitor Visitor. * @return \Hoa\Visitor\Visit */ public function setAsserter(HoaVisitor\Visit $visitor) { $old = $this->_asserter; $this->_asserter = $visitor; return $old; } /** * Get asserter. * * @param \Hoa\Ruler\Context $context Context. * @return \Hoa\Visitor\Visit */ public function getAsserter(Context $context = null) { if (null === $asserter = $this->_asserter) { return static::getDefaultAsserter($context); } if (null !== $context) { $asserter->setContext($context); } return $asserter; } /** * Get default asserter. * * @param \Hoa\Ruler\Context $context Context. * @return \Hoa\Ruler\Visitor\Asserter */ public static function getDefaultAsserter(Context $context = null) { if (null === static::$_defaultAsserter) { static::$_defaultAsserter = new Visitor\Asserter($context); } if (null !== $context) { static::$_defaultAsserter->setContext($context); } return static::$_defaultAsserter; } /** * Get compiler. * * @return \Hoa\Compiler\Llk\Parser */ public static function getCompiler() { if (null === static::$_compiler) { static::$_compiler = Compiler\Llk::load( new File\Read('hoa://Library/Ruler/Grammar.pp') ); } return static::$_compiler; } } /** * Flex entity. */ Consistency::flexEntity('Hoa\Ruler\Ruler'); ================================================ FILE: Test/Integration/Documentation.php ================================================ given( $ruler = new LUT(), $rule = 'group in ["customer", "guest"] and points > 30' ); $this->next_case_classical($ruler, $rule); } public function next_case_classical($ruler, $rule) { $this ->given( $context = new LUT\Context(), $context['group'] = $this->sample( $this->realdom->regex('/customer|guest/') ), $context['points'] = function () { return 42; } ) ->when($result = $ruler->assert($rule, $context)) ->then ->boolean($result) ->isTrue() ->given($context['points'] = 29) ->when($result = $ruler->assert($rule, $context)) ->then ->boolean($result) ->isFalse(); } public function case_new_operators() { $this ->given( $user = new \Mock\StdClass(), $user->group = 'customer', $user->points = 42, $user->status = true, $ruler = new LUT(), $rule = 'logged(user) and group in ["customer", "guest"] and points > 30', $context = new LUT\Context(), $context['user'] = function () use ($user, $context) { $context['group'] = $user->group; $context['points'] = $user->points; return $user; } ) ->when( $ruler->getDefaultAsserter()->setOperator('logged', function ($user) { return $user->status; }), $result = $ruler->assert($rule, $context) ) ->then ->boolean($result) ->isTrue() ->given($user->status = false) ->when($result = $ruler->assert($rule, $context)) ->then ->boolean($result) ->isFalse(); } public function case_interpret() { $this ->given( $model = LUT::interpret('group in ["customer", "guest"] and points > 30') ) ->when($ledom = unserialize(serialize($model))) ->then ->object($model) ->isEqualTo($ledom); $this->next_case_classical(new LUT(), $model); } public function case_compile() { $expectedResult = <<<'RESULT' $model = new \Hoa\Ruler\Model(); $model->expression = $model->and( $model->func( 'logged', $model->variable('user') ), $model->and( $model->in( $model->variable('group'), [ 'customer', 'guest' ] ), $model->{'>'}( $model->variable('points'), 30 ) ) ); RESULT; $this ->when($result = LUT::interpret( 'logged(user) and group in ["customer", "guest"] and points > 30' ) . '') ->then ->string($result) ->isEqualTo($expectedResult); } } ================================================ FILE: Test/Integration/Model/Operator.php ================================================ given( $ruler = new LUT(), $fExecuted = false, $gExecuted = false, $asserter = $ruler->getDefaultAsserter(), $asserter->setOperator( 'f', function ($a = false) use (&$fExecuted) { $fExecuted = true; return $a; } ), $asserter->setOperator( 'g', function ($b = false) use (&$gExecuted) { $gExecuted = true; return $b; } ), $rule = 'f(false) and g(true)' ) ->when($result = $ruler->assert($rule, new LUT\Context())) ->then ->boolean($result) ->isFalse() ->boolean($fExecuted) ->isTrue() ->boolean($gExecuted) ->isFalse() ->given( $fExecuted = false, $gExecuted = false, $rule = 'f(true) and g(true)' ) ->when($result = $ruler->assert($rule, new LUT\Context())) ->then ->boolean($result) ->isTrue() ->boolean($fExecuted) ->isTrue() ->boolean($gExecuted) ->isTrue() ->given( $fExecuted = false, $gExecuted = false, $rule = 'f(true) and g(false)' ) ->when($result = $ruler->assert($rule, new LUT\Context())) ->then ->boolean($result) ->isFalse() ->boolean($fExecuted) ->isTrue() ->boolean($gExecuted) ->isTrue() ->given( $fExecuted = false, $gExecuted = false, $rule = 'f(false) and g(false)' ) ->when($result = $ruler->assert($rule, new LUT\Context())) ->then ->boolean($result) ->isFalse() ->boolean($fExecuted) ->isTrue() ->boolean($gExecuted) ->isFalse(); } public function case_lazy_or() { $this ->given( $ruler = new LUT(), $fExecuted = false, $gExecuted = false, $asserter = $ruler->getDefaultAsserter(), $asserter->setOperator( 'f', function ($a) use (&$fExecuted) { $fExecuted = true; return $a; } ), $asserter->setOperator( 'g', function ($b) use (&$gExecuted) { $gExecuted = true; return $b; } ), $rule = 'f(false) or g(true)' ) ->when($result = $ruler->assert($rule, new LUT\Context())) ->then ->boolean($result) ->isTrue() ->boolean($fExecuted) ->isTrue() ->boolean($gExecuted) ->isTrue() ->given( $fExecuted = false, $gExecuted = false, $rule = 'f(true) or g(true)' ) ->when($result = $ruler->assert($rule, new LUT\Context())) ->then ->boolean($result) ->isTrue() ->boolean($fExecuted) ->isTrue() ->boolean($gExecuted) ->isFalse() ->given( $fExecuted = false, $gExecuted = false, $rule = 'f(true) or g(false)' ) ->when($result = $ruler->assert($rule, new LUT\Context())) ->then ->boolean($result) ->isTrue() ->boolean($fExecuted) ->isTrue() ->boolean($gExecuted) ->isFalse() ->given( $fExecuted = false, $gExecuted = false, $rule = 'f(false) or g(false)' ) ->when($result = $ruler->assert($rule, new LUT\Context())) ->then ->boolean($result) ->isFalse() ->boolean($fExecuted) ->isTrue() ->boolean($gExecuted) ->isTrue(); } } ================================================ FILE: Test/Unit/Context.php ================================================ when($context = new CUT()) ->then ->object($context) ->isInstanceOf('ArrayAccess'); } public function case_exists_set_get() { $this ->given( $key = 'foo', $value = 'bar', $context = new CUT() ) ->then ->boolean(isset($context[$key])) ->isFalse() ->when($context[$key] = $value) ->then ->boolean(isset($context[$key])) ->isTrue() ->string($context[$key]) ->isEqualTo($value); } public function case_exception_when_getting_unspecified_key() { $this ->given($context = new CUT()) ->exception(function () use ($context) { $context['foo']; }) ->isInstanceOf('Hoa\Ruler\Exception'); } public function case_unset() { $this ->given( $key = 'foo', $value = 'bar', $context = new CUT() ) ->then ->boolean(isset($context[$key])) ->isFalse() ->when($context[$key] = $value) ->then ->boolean(isset($context[$key])) ->isTrue() ->when(function () use ($context, $key) { unset($context[$key]); }) ->then ->boolean(isset($context[$key])) ->isFalse(); } public function case_callable_closure() { $this ->given( $context = new CUT(), $context['foo'] = function () { return fakeCallable(); } ) ->when($result = $context['foo']) ->then ->boolean($result) ->isTrue(); } public function case_callable_user_function() { $this ->given( $context = new CUT(), $context['foo'] = __NAMESPACE__ . '\fakeCallable' ) ->when($result = $context['foo']) ->then ->boolean($result) ->isTrue(); } public function case_callable_internal_function() { $this ->given( $context = new CUT(), $context['foo'] = 'var_dump' ) ->when($result = $context['foo']) ->then ->string($result) ->isEqualTo('var_dump'); } public function case_callable_method() { $this ->given( $context = new CUT(), $context['foo'] = [$this, 'fakeCallable'] ) ->when($result = $context['foo']) ->then ->boolean($result) ->isTrue(); } public function case_callable_xcallable() { $this ->given( $context = new CUT(), $context['foo'] = xcallable($this, 'fakeCallable') ) ->when($result = $context['foo']) ->then ->boolean($result) ->isTrue(); } public function case_callable_cache() { $this ->given( $context = new CUT(), $context['foo'] = function () { static $i = 0; return $i++; } ) ->when($result = $context['foo']) ->then ->integer($result) ->isEqualTo(0) ->when($result = $context['foo']) ->then ->integer($result) ->isEqualTo(0); } public function case_callable_no_cache() { $this ->given( $context = new CUT(), $context['foo'] = new LUT\DynamicCallable(function () { static $i = 0; return $i++; }) ) ->when($result = $context['foo']) ->then ->integer($result) ->isEqualTo(0) ->when($result = $context['foo']) ->then ->integer($result) ->isEqualTo(1); } public function case_callable_argument() { $this ->given( $self = $this, $context = new CUT(), $context['foo'] = function () use ($self, $context) { $arguments = func_get_args(); $self ->integer(count($arguments)) ->isEqualTo(1) ->object($arguments[0]) ->isIdenticalTo($context); } ) ->when($result = $context['foo']); } public function case_access_as_properties() { $this ->given( $self = $this, $context = new CUT(), $context['foo'] = 42 ) ->when($result = $context->foo) ->then ->integer($result) ->isEqualTo(42) ->when($context->bar = 24) ->then ->integer($context['bar']) ->isEqualTo(24); } public function fakeCallable() { return fakeCallable(); } } function fakeCallable() { return true; } ================================================ FILE: Test/Unit/DynamicCallable.php ================================================ when($result = new SUT(function () {})) ->then ->object($result) ->isInstanceOf(Consistency\Xcallable::class); } } ================================================ FILE: Test/Unit/Exception/Asserter.php ================================================ when($result = new SUT('foo')) ->then ->object($result) ->isInstanceOf(LUT\Exception::class); } } ================================================ FILE: Test/Unit/Exception/Exception.php ================================================ when($result = new SUT('foo')) ->then ->object($result) ->isInstanceOf(HoaException::class); } } ================================================ FILE: Test/Unit/Exception/Interpreter.php ================================================ when($result = new SUT('foo')) ->then ->object($result) ->isInstanceOf(LUT\Exception::class); } } ================================================ FILE: Test/Unit/Issue.php ================================================ given( $ruler = new LUT(), $rule = 'variable', $context = new LUT\Context(), $context['variable'] = 'file' ) ->when(function () use ($ruler, $rule, $context) { $ruler->assert($rule, $context); }) ->error() ->notExists(); } public function case_github_70() { $this ->given( $ruler = new LUT(), $rule = 'variable["foo"] is null', $context = new LUT\Context(), $context['variable'] = ['foo' => null] ) ->when($result = $ruler->assert($rule, $context)) ->then ->boolean($result) ->isTrue(); } public function case_github_100_1() { $this ->given( $ruler = new LUT(), $rule = '(false and true) or true' ) ->when($result = $ruler->assert($rule)) ->then ->boolean($result) ->isTrue(); } public function case_github_100_2() { $this ->given( $ruler = new LUT(), $rule = 'false and true or true' ) ->when($result = $ruler->assert($rule)) ->then ->boolean($result) ->isTrue(); } public function case_github_100_3() { $this ->given( $ruler = new LUT(), $rule = 'true or true and false' ) ->when($result = $ruler->assert($rule)) ->then ->boolean($result) ->isTrue(); } } ================================================ FILE: Test/Unit/Model/Bag/Bag.php ================================================ when($result = new SUT()) ->then ->object($result) ->isInstanceOf(Visitor\Element::class); } } ================================================ FILE: Test/Unit/Model/Bag/Context.php ================================================ when($result = new SUT('foobar')) ->then ->object($result) ->isInstanceOf(LUT\Model\Bag::class); } public function case_constructor() { $this ->given($id = 'foobar') ->when($result = new SUT($id)) ->then ->string($result->getId()) ->isEqualTo($id) ->array($result->getDimensions()) ->isEmpty(); } public function case_scalar_index_from_root() { return $this->_case_index_from_root( 'baz', new LUT\Model\Bag\Scalar('baz') ); } public function case_array_index_from_root() { return $this->_case_index_from_root( ['baz'], new LUT\Model\Bag\RulerArray(['baz']) ); } public function case_bag_index_from_root() { return $this->_case_index_from_root( new LUT\Model\Bag\Scalar('baz'), new LUT\Model\Bag\Scalar('baz') ); } protected function _case_index_from_root($index, $expectedIndex) { $this ->given( $id = 'foobar', $context = new SUT($id) ) ->when($result = $context->index($index)) ->then ->object($result) ->isIdenticalTo($context) ->array($result->getDimensions()) ->isEqualTo([ [ SUT::ACCESS_TYPE => SUT::ARRAY_ACCESS, SUT::ACCESS_VALUE => $expectedIndex ] ]); } public function case_scalar_index() { return $this->_case_index( 'baz', new LUT\Model\Bag\Scalar('baz') ); } public function case_array_index() { return $this->_case_index( ['baz'], new LUT\Model\Bag\RulerArray(['baz']) ); } public function case_bag_index() { return $this->_case_index( new LUT\Model\Bag\Scalar('baz'), new LUT\Model\Bag\Scalar('baz') ); } protected function _case_index($index, $expectedIndex) { $this ->given( $id = 'foobar', $context = new SUT($id), $context->index(new LUT\Model\Bag\Scalar('qux')) ) ->when($result = $context->index($index)) ->then ->object($result) ->isIdenticalTo($context) ->array($result->getDimensions()) ->isEqualTo([ [ SUT::ACCESS_TYPE => SUT::ARRAY_ACCESS, SUT::ACCESS_VALUE => new LUT\Model\Bag\Scalar('qux') ], [ SUT::ACCESS_TYPE => SUT::ARRAY_ACCESS, SUT::ACCESS_VALUE => $expectedIndex ] ]); } public function case_attribute() { $this ->given( $id = 'foobar', $attribute = 'bazqux', $context = new SUT($id) ) ->when($result = $context->attribute($attribute)) ->then ->object($result) ->isIdenticalTo($context) ->array($result->getDimensions()) ->isEqualTo([ [ SUT::ACCESS_TYPE => SUT::ATTRIBUTE_ACCESS, SUT::ACCESS_VALUE => $attribute ] ]); } public function case_call() { $this ->given( $id = 'foobar', $method = new LUT\Model\Operator('f'), $context = new SUT($id) ) ->when($result = $context->call($method)) ->then ->object($result) ->isIdenticalTo($context) ->array($result->getDimensions()) ->isEqualTo([ [ SUT::ACCESS_TYPE => SUT::METHOD_ACCESS, SUT::ACCESS_VALUE => $method ] ]); } public function case_multiple_dimension_types() { $this ->given( $id = 'foobar', $index = 'baz', $attribute = 'qux', $method = new LUT\Model\Operator('f'), $context = new SUT($id), $context->attribute($attribute), $context->index($index), $context->call($method) ) ->when($result = $context->getDimensions()) ->then ->array($result) ->isEqualTo([ [ SUT::ACCESS_TYPE => SUT::ATTRIBUTE_ACCESS, SUT::ACCESS_VALUE => $attribute ], [ SUT::ACCESS_TYPE => SUT::ARRAY_ACCESS, SUT::ACCESS_VALUE => new LUT\Model\Bag\Scalar($index) ], [ SUT::ACCESS_TYPE => SUT::METHOD_ACCESS, SUT::ACCESS_VALUE => $method ] ]); } } ================================================ FILE: Test/Unit/Model/Bag/RulerArray.php ================================================ when($result = new SUT(['foobar'])) ->then ->object($result) ->isInstanceOf(LUT\Model\Bag::class); } public function case_constructor() { $this ->given($data = ['foo', ['bar'], new LUT\Model\Bag\Scalar('baz')]) ->when($result = new SUT($data)) ->then ->let($array = $result->getArray()) ->array($array) ->hasSize(count($data)) ->isEqualTo([ new LUT\Model\Bag\Scalar('foo'), new SUT([new LUT\Model\Bag\Scalar('bar')]), new LUT\Model\Bag\Scalar('baz') ]); } } ================================================ FILE: Test/Unit/Model/Bag/Scalar.php ================================================ when($result = new SUT('foobar')) ->then ->object($result) ->isInstanceOf(LUT\Model\Bag::class); } public function case_constructor() { $this ->given($scalar = 'foobar') ->when($result = new SUT($scalar)) ->then ->string($result->getValue()) ->isEqualTo($scalar); } } ================================================ FILE: Test/Unit/Model/Model.php ================================================ when($result = new SUT()) ->then ->object($result) ->isInstanceOf(Visitor\Element::class); } public function case_set_default() { $this ->given($model = new SUT()) ->when($result = $model->foo = 'bar') ->then ->string($result) ->isEqualTo('bar') ->string($model->foo) ->isEqualTo('bar'); } public function case_set_expression() { $this ->given($model = new SUT()) ->when($result = $model->expression = 'foo') ->then ->string($result) ->isEqualTo('foo') ->boolean(isset($model->expression)) ->isFalse(); } public function case_get_expression_is_a_scalar() { $this ->given( $model = new SUT(), $model->expression = 'foo' ) ->when($result = $model->getExpression()) ->then ->object($result) ->isEqualTo(new LUT\Model\Bag\Scalar('foo')); } public function case_get_expression_is_an_array() { $this ->given( $model = new SUT(), $model->expression = ['foo'] ) ->when($result = $model->getExpression()) ->then ->object($result) ->isEqualTo(new LUT\Model\Bag\RulerArray(['foo'])); } public function case_get_expression() { $this ->given( $model = new SUT(), $model->expression = new LUT\Model\Operator('f') ) ->when($result = $model->getExpression()) ->then ->object($result) ->isEqualTo(new LUT\Model\Operator('f')); } public function case_func() { $this ->given($model = new SUT()) ->when($result = $model->func('f', 'x', 42)) ->then ->object($result) ->isInstanceOf(LUT\Model\Operator::class) ->string($result->getName()) ->isEqualTo('f') ->array($result->getArguments()) ->isEqualTo([ new LUT\Model\Bag\Scalar('x'), new LUT\Model\Bag\Scalar(42) ]) ->boolean($result->isFunction()) ->isTrue() ->boolean($result->isLazy()) ->isFalse(); } public function case_operation() { $this ->given($model = new SUT()) ->when($result = $model->operation('f', ['x', 42])) ->then ->object($result) ->isInstanceOf(LUT\Model\Operator::class) ->string($result->getName()) ->isEqualTo('f') ->array($result->getArguments()) ->isEqualTo([ new LUT\Model\Bag\Scalar('x'), new LUT\Model\Bag\Scalar(42) ]) ->boolean($result->isFunction()) ->isFalse() ->boolean($result->isLazy()) ->isFalse(); } public function case_operator() { $this ->given($model = new SUT()) ->when($result = $model->_operator('f', ['x', 42], true)) ->then ->object($result) ->isInstanceOf(LUT\Model\Operator::class) ->string($result->getName()) ->isEqualTo('f') ->array($result->getArguments()) ->isEqualTo([ new LUT\Model\Bag\Scalar('x'), new LUT\Model\Bag\Scalar(42) ]) ->boolean($result->isFunction()) ->isTrue() ->boolean($result->isLazy()) ->isFalse(); } public function case_call() { $this ->given($model = new SUT()) ->when($result = $model->f('x', 42)) ->then ->object($result) ->isInstanceOf(LUT\Model\Operator::class) ->string($result->getName()) ->isEqualTo('f') ->array($result->getArguments()) ->isEqualTo([ new LUT\Model\Bag\Scalar('x'), new LUT\Model\Bag\Scalar(42) ]) ->boolean($result->isFunction()) ->isFalse() ->boolean($result->isLazy()) ->isFalse(); } public function case_variable() { $this ->given($model = new SUT()) ->when($result = $model->variable('x')) ->then ->object($result) ->isEqualTo(new LUT\Model\Bag\Context('x')); } public function case_to_string() { $this ->given( $model = new SUT(), $model->expression = $model->f('x', 42) ) ->when($result = $model->__toString()) ->then ->string($result) ->isEqualTo( '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->f(' . "\n" . ' \'x\',' . "\n" . ' 42' . "\n" . ' );' ); } } ================================================ FILE: Test/Unit/Model/Operator.php ================================================ given($name = 'foo') ->when($result = new SUT($name)) ->then ->object($result) ->isInstanceOf(Visitor\Element::class); } public function case_is_a_context() { $this ->given($name = 'foo') ->when($result = new SUT($name)) ->then ->object($result) ->isInstanceOf(LUT\Model\Bag\Context::class); } public function case_constructor() { $this ->given($name = 'foo') ->when($result = new SUT($name)) ->then ->string($result->getName()) ->isEqualTo($name) ->array($result->getArguments()) ->isEmpty() ->boolean($result->isFunction()) ->isTrue() ->boolean($result->isLazy()) ->isFalse(); } public function case_constructor_with_arguments() { $this ->given( $name = 'foo', $arguments = [new LUT\Model\Bag\Scalar(42)] ) ->when($result = new SUT($name, $arguments)) ->then ->string($result->getName()) ->isEqualTo($name) ->array($result->getArguments()) ->isEqualTo($arguments) ->boolean($result->isFunction()) ->isTrue() ->boolean($result->isLazy()) ->isFalse(); } public function case_constructor_with_arguments_and_function_flag() { $this ->given( $name = 'foo', $arguments = [new LUT\Model\Bag\Scalar(42)], $isFunction = false ) ->when($result = new SUT($name, $arguments, $isFunction)) ->then ->string($result->getName()) ->isEqualTo($name) ->array($result->getArguments()) ->isEqualTo($arguments) ->boolean($result->isFunction()) ->isFalse() ->boolean($result->isLazy()) ->isFalse(); } public function case_set_name() { $this ->given( $oldName = 'foo', $name = 'bar', $operator = new SUT('foo') ) ->when($result = $this->invoke($operator)->setName($name)) ->then ->string($result) ->isEqualTo($oldName) ->boolean($operator->isLazy()) ->isFalse(); } public function case_set_name_with_the_and_operator_for_auto_laziness() { return $this->_case_set_name_with_auto_laziness('and'); } public function case_set_name_with_the_or_operator_for_auto_laziness() { return $this->_case_set_name_with_auto_laziness('or'); } protected function _case_set_name_with_auto_laziness($name) { $this ->given($operator = new SUT('foo')) ->when($result = $this->invoke($operator)->setName($name)) ->then ->string($result) ->isEqualTo('foo') ->boolean($operator->isLazy()) ->isTrue(); } public function case_get_name() { $this ->given( $name = 'bar', $operator = new SUT('foo'), $this->invoke($operator)->setName($name) ) ->when($result = $operator->getName()) ->then ->string($result) ->isEqualTo($name); } public function case_set_arguments() { $this ->given( $operator = new SUT('foo'), $arguments = ['foo', [42], new LUT\Model\Bag\Scalar('baz')] ) ->when($result = $this->invoke($operator)->setArguments($arguments)) ->then ->array($result) ->isEmpty(); } public function case_set_arguments_not_additive() { $this ->given( $operator = new SUT('foo'), $argumentsA = [new LUT\Model\Bag\Scalar('foo')], $argumentsB = [new LUT\Model\Bag\Scalar('bar')], $this->invoke($operator)->setArguments($argumentsA) ) ->when($result = $this->invoke($operator)->setArguments($argumentsB)) ->then ->array($result) ->isEqualTo($argumentsA) ->array($operator->getArguments()) ->isEqualTo($argumentsB); } public function case_get_arguments() { $this ->given( $operator = new SUT('foo'), $arguments = ['foo', [42], new LUT\Model\Bag\Scalar('baz')], $this->invoke($operator)->setArguments($arguments) ) ->when($result = $operator->getArguments()) ->then ->array($result) ->isEqualTo([ new LUT\Model\Bag\Scalar('foo'), new LUT\Model\Bag\RulerArray([42]), new LUT\Model\Bag\Scalar('baz') ]); } public function case_set_function() { $this ->given($operator = new SUT('foo')) ->when($result = $this->invoke($operator)->setFunction(false)) ->then ->boolean($result) ->isTrue(); } public function case_is_function() { $this ->given( $operator = new SUT('foo'), $this->invoke($operator)->setFunction(true) ) ->when($result = $operator->isFunction()) ->then ->boolean($result) ->isTrue(); } public function case_is_not_function() { $this ->given( $operator = new SUT('foo'), $this->invoke($operator)->setFunction(false) ) ->when($result = $operator->isFunction()) ->then ->boolean($result) ->isFalse(); } public function case_set_laziness() { $this ->given($operator = new SUT('foo')) ->when($result = $this->invoke($operator)->setLaziness(false)) ->then ->boolean($result) ->isFalse(); } public function case_is_lazy() { $this ->given( $operator = new SUT('foo'), $this->invoke($operator)->setLaziness(true) ) ->when($result = $operator->isLazy()) ->then ->boolean($result) ->isTrue(); } public function case_is_not_lazy() { $this ->given( $operator = new SUT('foo'), $this->invoke($operator)->setLaziness(false) ) ->when($result = $operator->isLazy()) ->then ->boolean($result) ->isFalse(); } public function case_should_break_lazy_evaluation_with_and_operator() { return $this->_case_should_break_lazy_evaluation_with_x_operator( 'and', false, SUT::LAZY_BREAK ); } public function case_should_not_break_lazy_evaluation_with_and_operator() { return $this->_case_should_break_lazy_evaluation_with_x_operator( 'and', true, SUT::LAZY_CONTINUE ); } public function case_should_break_lazy_evaluation_with_or_operator() { return $this->_case_should_break_lazy_evaluation_with_x_operator( 'or', true, SUT::LAZY_BREAK ); } public function case_should_not_break_lazy_evaluation_with_or_operator() { return $this->_case_should_break_lazy_evaluation_with_x_operator( 'or', false, SUT::LAZY_CONTINUE ); } public function case_should_not_break_lazy_evaluation_with_any_operator() { return $this->_case_should_break_lazy_evaluation_with_x_operator( 'foo', 42, SUT::LAZY_CONTINUE ); } protected function _case_should_break_lazy_evaluation_with_x_operator($name, $value, $expect) { $this ->given($operator = new SUT($name)) ->when($result = $operator->shouldBreakLazyEvaluation($value)) ->then ->boolean($result) ->isEqualTo($expect); } public function case_is_token() { $this ->when(function () { foreach (['not', 'and', 'or', 'xor'] as $token) { $this ->when($result = SUT::isToken($token)) ->then ->boolean($result) ->isTrue(); } }); } public function case_is_not_token() { $this ->when($result = SUT::isToken('foo')) ->then ->boolean($result) ->isFalse(); } } ================================================ FILE: Test/Unit/Ruler.php ================================================ given( $rule = '7 < 42', $ruler = new SUT() ) ->when($result = $ruler->assert($rule)) ->then ->boolean($result) ->isTrue(); } public function case_assert_with_a_context() { $this ->given( $rule = 'x < 42', $ruler = new SUT(), $context = new LUT\Context(), $context['x'] = 7 ) ->when($result = $ruler->assert($rule, $context)) ->then ->boolean($result) ->isTrue(); } public function case_assert_with_rule_as_a_model() { $this ->given( $rule = SUT::interpret('x < 42'), $ruler = new SUT(), $context = new LUT\Context(), $context['x'] = 7 ) ->when($result = $ruler->assert($rule, $context)) ->then ->boolean($result) ->isTrue(); } public function case_interpret() { $this ->when($result = SUT::interpret('x < 42')) ->then ->object($result) ->isInstanceOf(LUT\Model::class); } public function case_get_interpreter() { $this ->when($result = SUT::getInterpreter()) ->then ->object($result) ->isInstanceOf(LUT\Visitor\Interpreter::class); } public function case_set_asserter() { $this ->given( $ruler = new SUT(), $asserter = new LUT\Visitor\Asserter() ) ->when($result = $ruler->setAsserter($asserter)) ->then ->variable($result) ->isNull(); } public function case_get_asserter() { $this ->given( $asserter = new LUT\Visitor\Asserter(), $ruler = new SUT(), $context = new LUT\Context(), $ruler->setAsserter($asserter), $asserter->setContext($context) ) ->when($result = $ruler->getAsserter()) ->then ->object($result) ->isIdenticalTo($asserter) ->object($result->getContext()) ->isIdenticalTo($context); } public function case_get_asserter_with_a_specific_context() { $this ->given( $asserter = new LUT\Visitor\Asserter(), $contextA = new LUT\Context(), $contextB = new LUT\Context(), $ruler = new SUT(), $ruler->setAsserter($asserter), $asserter->setContext($contextA) ) ->when($result = $ruler->getAsserter($contextB)) ->then ->object($result) ->isIdenticalTo($asserter) ->object($result->getContext()) ->isIdenticalTo($contextB); } public function case_get_asserter_the_default_one() { $this ->given($ruler = new SUT()) ->when($result = $ruler->getAsserter()) ->then ->object($result) ->isInstanceOf(LUT\Visitor\Asserter::class) ->variable($result->getContext()) ->isNull() ->object($ruler->getAsserter()) ->isIdenticalTo($result); } public function case_get_asserter_the_default_one_with_a_specific_context() { $this ->given( $ruler = new SUT(), $context = new LUT\Context() ) ->when($result = $ruler->getAsserter($context)) ->then ->object($result) ->isInstanceOf(LUT\Visitor\Asserter::class) ->object($result->getContext()) ->isIdenticalTo($context) ->object($ruler->getAsserter($context)) ->isIdenticalTo($result); } public function case_get_default_asserter() { $this ->when($result = SUT::getDefaultAsserter()) ->then ->object($result) ->isInstanceOf(LUT\Visitor\Asserter::class) ->variable($result->getContext()) ->isNull() ->object(SUT::getDefaultAsserter()) ->isIdenticalTo($result); } public function case_get_default_asserter_with_a_specific_context() { $this ->given($context = new LUT\Context()) ->when($result = SUT::getDefaultAsserter($context)) ->then ->object($result) ->isInstanceOf(LUT\Visitor\Asserter::class) ->object($result->getContext()) ->isIdenticalTo($context) ->object(SUT::getDefaultAsserter($context)) ->isIdenticalTo($result); } public function case_get_compiler() { $this ->when($result = SUT::getCompiler()) ->then ->object($result) ->isInstanceOf(Compiler\Llk\Parser::class) ->object(SUT::getCompiler()) ->isIdenticalTo($result); } } ================================================ FILE: Test/Unit/Visitor/Asserter.php ================================================ when($result = new SUT()) ->then ->object($result) ->isInstanceOf(Visitor\Visit::class); } public function case_constructor() { $this ->when($result = new SUT()) ->then ->array($result->getOperators()) ->hasSize(14) ->hasKey('and') ->hasKey('or') ->hasKey('xor') ->hasKey('not') ->hasKey('=') ->hasKey('is') ->hasKey('!=') ->hasKey('>') ->hasKey('>=') ->hasKey('<') ->hasKey('<=') ->hasKey('in') ->hasKey('sum') ->hasKey('matches') ->variable($result->getContext()) ->isNull(); } public function case_constructor_with_a_context() { $this ->given($context = new LUT\Context()) ->when($result = new SUT($context)) ->then ->object($result->getContext()) ->isIdenticalTo($context); } public function case_operator_and_arity_0() { return $this->_case_boolean_operator('and', [], false); } public function case_operator_and_true() { return $this->_case_boolean_operator('and', [true], false); } public function case_operator_and_false() { return $this->_case_boolean_operator('and', [false], false); } public function case_operator_and_true_true() { return $this->_case_boolean_operator('and', [true, true], true); } public function case_operator_and_true_false() { return $this->_case_boolean_operator('and', [true, false], false); } public function case_operator_and_false_true() { return $this->_case_boolean_operator('and', [false, true], false); } public function case_operator_and_false_false() { return $this->_case_boolean_operator('and', [false, false], false); } public function case_operator_or_arity_0() { return $this->_case_boolean_operator('or', [], false); } public function case_operator_or_true() { return $this->_case_boolean_operator('or', [true], true); } public function case_operator_or_false() { return $this->_case_boolean_operator('or', [false], false); } public function case_operator_or_true_true() { return $this->_case_boolean_operator('or', [true, true], true); } public function case_operator_or_true_false() { return $this->_case_boolean_operator('or', [true, false], true); } public function case_operator_or_false_true() { return $this->_case_boolean_operator('or', [false, true], true); } public function case_operator_or_false_false() { return $this->_case_boolean_operator('or', [false, false], false); } public function case_operator_xor_true_true() { return $this->_case_boolean_operator('xor', [true, true], false); } public function case_operator_xor_true_false() { return $this->_case_boolean_operator('xor', [true, false], true); } public function case_operator_xor_false_true() { return $this->_case_boolean_operator('xor', [false, true], true); } public function case_operator_xor_false_false() { return $this->_case_boolean_operator('xor', [false, false], false); } public function case_operator_not_true() { return $this->_case_boolean_operator('not', [true], false); } public function case_operator_not_false() { return $this->_case_boolean_operator('not', [false], true); } public function case_operator_equal_7_42() { return $this->_case_boolean_operator('=', [7, 42], false); } public function case_operator_equal_7_7() { return $this->_case_boolean_operator('=', [7, 7], true); } public function case_operator_equal_7_7_casting() { return $this->_case_boolean_operator('=', [7, '7'], true); } public function case_operator_is_7_42() { return $this->_case_boolean_operator('is', [7, 42], false); } public function case_operator_is_7_7() { return $this->_case_boolean_operator('is', [7, 7], true); } public function case_operator_is_7_7_casting() { return $this->_case_boolean_operator('is', [7, '7'], true); } public function case_operator_not_equal_7_42() { return $this->_case_boolean_operator('!=', [7, 42], true); } public function case_operator_not_equal_7_7() { return $this->_case_boolean_operator('!=', [7, 7], false); } public function case_operator_not_equal_7_7_casting() { return $this->_case_boolean_operator('!=', [7, '7'], false); } public function case_operator_greater_than_7_42() { return $this->_case_boolean_operator('>', [7, 42], false); } public function case_operator_greater_than_42_7() { return $this->_case_boolean_operator('>', [42, 7], true); } public function case_operator_greater_than_7_7() { return $this->_case_boolean_operator('>', [7, 7], false); } public function case_operator_greater_than_or_equal_to_7_42() { return $this->_case_boolean_operator('>=', [7, 42], false); } public function case_operator_greater_than_or_equal_to_42_7() { return $this->_case_boolean_operator('>=', [42, 7], true); } public function case_operator_greater_than_or_equal_to_7_7() { return $this->_case_boolean_operator('>=', [7, 7], true); } public function case_operator_lower_than_7_42() { return $this->_case_boolean_operator('<', [7, 42], true); } public function case_operator_lower_than_42_7() { return $this->_case_boolean_operator('<', [42, 7], false); } public function case_operator_lower_than_7_7() { return $this->_case_boolean_operator('<', [7, 7], false); } public function case_operator_lower_than_or_equal_to_7_42() { return $this->_case_boolean_operator('<=', [7, 42], true); } public function case_operator_lower_than_or_equal_to_42_7() { return $this->_case_boolean_operator('<=', [42, 7], false); } public function case_operator_lower_than_or_equal_to_7_7() { return $this->_case_boolean_operator('<=', [7, 7], true); } public function case_operator_in_empty_array() { return $this->_case_boolean_operator('in', [7, []], false); } public function case_operator_in() { return $this->_case_boolean_operator('in', [7, [1, 3, 5, 7, 9]], true); } public function case_operator_in_falsy() { return $this->_case_boolean_operator('in', [42, [1, 3, 5, 7, 9]], false); } protected function _case_boolean_operator($operator, array $parameters, $expected) { $this ->given( $asserter = new SUT(), $operator = $asserter->getOperator($operator) ) ->when($result = call_user_func_array($operator, $parameters)) ->then ->boolean($result) ->isEqualTo($expected); } public function case_operator_sum_arity_0() { return $this->_case_operator_sum([], 0); } public function case_operator_sum_arity_1() { return $this->_case_operator_sum([7], 7); } public function case_operator_sum() { return $this->_case_operator_sum([1, 2, 3, 4, 5, 6, 7, 8, 9], 45); } protected function _case_operator_sum(array $parameters, $expected) { $this ->given( $asserter = new SUT(), $operator = $asserter->getOperator('sum') ) ->when($result = call_user_func_array($operator, $parameters)) ->then ->integer($result) ->isEqualTo($expected); } public function case_operator_matches() { return $this->_case_boolean_operator('matches', ['foo', '\w+'], true); } public function case_operator_matches_falsy() { return $this->_case_boolean_operator('matches', ['foo', '\d+'], false); } public function case_operator_matches_escaped_delimiter() { return $this->_case_boolean_operator('matches', ['`foo`', '`\w+`'], true); } public function case_set_context() { $this ->given( $context = new LUT\Context(), $asserter = new SUT() ) ->when($result = $asserter->setContext($context)) ->then ->variable($result) ->isNull(); } public function case_get_context() { $this ->given( $context = new LUT\Context(), $asserter = new SUT(), $asserter->setContext($context) ) ->when($result = $asserter->getContext()) ->then ->object($result) ->isIdenticalTo($context); } public function case_set_operator() { $this ->given( $asserter = new SUT(), $oldOperators = $asserter->getOperators(), $operator = function () {} ) ->when($result = $asserter->setOperator('_foo_', $operator)) ->then ->object($result) ->isIdenticalTo($asserter) ->integer(count($asserter->getOperators())) ->isEqualTo(count($oldOperators) + 1) ->boolean($asserter->operatorExists('_foo_')) ->isTrue() ->object($asserter->getOperator('_foo_')) ->isEqualTo(xcallable($operator)); } public function case_set_operator_overwrite() { $this ->given( $asserter = new SUT(), $asserter->setOperator('_foo_', function () {}), $oldOperators = $asserter->getOperators(), $operator = function () {} ) ->when($result = $asserter->setOperator('_foo_', $operator)) ->then ->object($result) ->isIdenticalTo($asserter) ->integer(count($asserter->getOperators())) ->isEqualTo(count($oldOperators)) ->boolean($asserter->operatorExists('_foo_')) ->isTrue() ->object($asserter->getOperator('_foo_')) ->isEqualTo(xcallable($operator)); } public function case_operator_exists() { $this ->given( $asserter = new SUT(), $asserter->setOperator('_foo_', function () {}) ) ->when($result = $asserter->operatorExists('_foo_')) ->then ->boolean($result) ->isTrue(); } public function case_operator_does_not_exist() { $this ->given($asserter = new SUT()) ->when($result = $asserter->operatorExists('_foo_')) ->then ->boolean($result) ->isFalse(); } public function case_get_operator() { $this ->given( $asserter = new SUT(), $operator = function () {}, $asserter->setOperator('_foo_', $operator) ) ->when($result = $asserter->getOperator('_foo_')) ->then ->object($result) ->isEqualTo(xcallable($operator)); } public function case_get_undefined_operator() { $this ->given($asserter = new SUT()) ->when($result = $asserter->getOperator('_foo_')) ->then ->variable($result) ->isNull(); } public function case_get_operators() { $this ->given($asserter = new SUT()) ->when($result = $asserter->getOperators()) ->then ->array($result) ->hasSize(14); } public function case_visit_model() { $this ->given( $model = new LUT\Model(), $model->expression = 42, $asserter = new SUT() ) ->when($result = $asserter->visitModel($model)) ->then ->boolean($result) ->isTrue() ->boolean($asserter->visit($model)) ->isIdenticalTo($result); } public function case_visit_operator() { $this ->given( $operator = new LUT\Model\Operator('sum', [7, 35]), $asserter = new SUT() ) ->when($result = $this->invoke($asserter)->visitOperator($operator)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($operator)) ->isIdenticalTo($result); } public function case_visit_operator_does_not_exist() { $this ->given( $operator = new LUT\Model\Operator('_foo_', [7, 35]), $asserter = new SUT() ) ->exception(function () use ($asserter, $operator) { $this->invoke($asserter)->visitOperator($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Operator _foo_ does not exist.') ->exception(function () use ($asserter, $operator) { $asserter->visit($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Operator _foo_ does not exist.'); } public function case_visit_operator_array_dimension_1() { $this ->given( $operator = new LUT\Model\Operator('c', [7]), $operator->index('x'), $asserter = new SUT(), $asserter->setOperator( 'c', function ($x) { return ['x' => $x * 6]; } ) ) ->when($result = $this->invoke($asserter)->visitOperator($operator)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($operator)) ->isIdenticalTo($result); } public function case_visit_operator_array_dimension_2() { $this ->given( $operator = new LUT\Model\Operator('c', [7]), $operator->index('x'), $operator->index('y'), $asserter = new SUT(), $asserter->setOperator( 'c', function ($x) { return ['x' => ['y' => $x * 6]]; } ) ) ->when($result = $this->invoke($asserter)->visitOperator($operator)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($operator)) ->isIdenticalTo($result); } public function case_visit_operator_array_like_dimension_1() { $this ->given( $operator = new LUT\Model\Operator('c', [7]), $operator->index('x'), $asserter = new SUT(), $asserter->setOperator( 'c', function ($x) { return new ArrayObject(['x' => $x * 6]); } ) ) ->when($result = $this->invoke($asserter)->visitOperator($operator)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($operator)) ->isIdenticalTo($result); } public function case_visit_operator_array_like_dimension_2() { $this ->given( $operator = new LUT\Model\Operator('c', [7]), $operator->index('x'), $operator->index('y'), $asserter = new SUT(), $asserter->setOperator( 'c', function ($x) { return new ArrayObject(['x' => new ArrayObject(['y' => $x * 6])]); } ) ) ->when($result = $this->invoke($asserter)->visitOperator($operator)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($operator)) ->isIdenticalTo($result); } public function case_visit_operator_array_dimension_1_undefined_index() { $this ->given( $operator = new LUT\Model\Operator('c', [7]), $operator->index('z'), $asserter = new SUT(), $asserter->setOperator( 'c', function ($x) { return ['x' => 42]; } ) ) ->exception(function () use ($asserter, $operator) { $this->invoke($asserter)->visitOperator($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to access to an undefined index: z (dimension number 1 of c()).') ->exception(function () use ($asserter, $operator) { $asserter->visit($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to access to an undefined index: z (dimension number 1 of c()).'); } public function case_visit_operator_array_dimension_1_not_an_array() { $this ->given( $operator = new LUT\Model\Operator('c', [7]), $operator->index('y'), $asserter = new SUT(), $asserter->setOperator( 'c', function ($x) { return 42; } ) ) ->exception(function () use ($asserter, $operator) { $this->invoke($asserter)->visitOperator($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to access to an undefined index: y (dimension number 1 of c()), because it is not an array.') ->exception(function () use ($asserter, $operator) { $asserter->visit($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to access to an undefined index: y (dimension number 1 of c()), because it is not an array.'); } public function case_visit_operator_array_like_dimension_1_undefined_index() { $this ->given( $operator = new LUT\Model\Operator('c', [7]), $operator->index('z'), $asserter = new SUT(), $asserter->setOperator( 'c', function ($x) { return new ArrayObject(['x' => 42]); } ) ) ->exception(function () use ($asserter, $operator) { $this->invoke($asserter)->visitOperator($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to access to an undefined index: z (dimension number 1 of c()).') ->exception(function () use ($asserter, $operator) { $asserter->visit($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to access to an undefined index: z (dimension number 1 of c()).'); } public function case_visit_operator_attribute_dimension_1() { $this ->given( $operator = new LUT\Model\Operator('c', [7]), $operator->attribute('x'), $asserter = new SUT(), $asserter->setOperator( 'c', function ($x) { return (object) ['x' => $x * 6]; } ) ) ->when($result = $this->invoke($asserter)->visitOperator($operator)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($operator)) ->isIdenticalTo($result); } public function case_visit_operator_attribute_dimension_2() { $this ->given( $operator = new LUT\Model\Operator('c', [7]), $operator->attribute('x'), $operator->attribute('y'), $asserter = new SUT(), $asserter->setOperator( 'c', function ($x) { return (object) ['x' => (object) ['y' => $x * 6]]; } ) ) ->when($result = $this->invoke($asserter)->visitOperator($operator)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($operator)) ->isIdenticalTo($result); } public function case_visit_operator_attribute_dimension_1_undefined_name() { $this ->given( $operator = new LUT\Model\Operator('c', [7]), $operator->attribute('y'), $asserter = new SUT(), $asserter->setOperator( 'c', function ($x) { return (object) ['x' => $x * 6]; } ) ) ->exception(function () use ($asserter, $operator) { $this->invoke($asserter)->visitOperator($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to read an undefined attribute: y (dimension number 1 of c()).') ->exception(function () use ($asserter, $operator) { $asserter->visit($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to read an undefined attribute: y (dimension number 1 of c()).'); } public function case_visit_operator_attribute_dimension_1_not_an_object() { $this ->given( $operator = new LUT\Model\Operator('c', [7]), $operator->attribute('x'), $asserter = new SUT(), $asserter->setOperator( 'c', function ($x) { return $x * 6; } ) ) ->exception(function () use ($asserter, $operator) { $this->invoke($asserter)->visitOperator($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to read an undefined attribute: x (dimension number 1 of c()), because it is not an object.') ->exception(function () use ($asserter, $operator) { $asserter->visit($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to read an undefined attribute: x (dimension number 1 of c()), because it is not an object.'); } public function case_visit_operator_method_dimension_1() { $this ->given( $operator = new LUT\Model\Operator('c'), $operator->call(new LUT\Model\Operator('f', [7, 35])), $context = new LUT\Context(['x' => new C()]), $asserter = new SUT($context), $asserter->setOperator( 'c', function () { return new C(); } ) ) ->when($result = $this->invoke($asserter)->visitOperator($operator)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($operator)) ->isIdenticalTo($result); } public function case_visit_operator_method_dimension_2() { $this ->given( $operator = new LUT\Model\Operator('c'), $operator->call(new LUT\Model\Operator('newMe')), $operator->call(new LUT\Model\Operator('f', [7, 35])), $asserter = new SUT(), $asserter->setOperator( 'c', function () { return new C(); } ) ) ->when($result = $this->invoke($asserter)->visitOperator($operator)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($operator)) ->isIdenticalTo($result); } public function case_visit_operator_method_dimension_1_undefined_method() { $this ->given( $operator = new LUT\Model\Operator('c'), $operator->call(new LUT\Model\Operator('h')), $asserter = new SUT(), $asserter->setOperator( 'c', function () { return new C(); } ) ) ->exception(function () use ($asserter, $operator) { $this->invoke($asserter)->visitOperator($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to call an undefined method: h (dimension number 1 of c()).') ->exception(function () use ($asserter, $operator) { $asserter->visit($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to call an undefined method: h (dimension number 1 of c()).'); } public function case_visit_operator_method_dimension_1_not_an_object() { $this ->given( $operator = new LUT\Model\Operator('c'), $operator->call(new LUT\Model\Operator('f', [7, 35])), $asserter = new SUT(), $asserter->setOperator( 'c', function () { return 42; } ) ) ->exception(function () use ($asserter, $operator) { $this->invoke($asserter)->visitOperator($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to call an undefined method: f (dimension number 1 of c()), because it is not an object.') ->exception(function () use ($asserter, $operator) { $asserter->visit($operator); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to call an undefined method: f (dimension number 1 of c()), because it is not an object.'); } public function case_visit_operator_mixed_dimensions() { $this ->given( $operator = new LUT\Model\Operator('c'), $operator->index('x'), $operator->attribute('y'), $operator->call(new LUT\Model\Operator('f', [7, 35])), $asserter = new SUT(), $asserter->setOperator( 'c', function () { return ['x' => (object) ['y' => new C()]]; } ) ) ->when($result = $this->invoke($asserter)->visitOperator($operator)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($operator)) ->isIdenticalTo($result); } public function case_visit_scalar_null() { return $this->_case_visit_scalar(null); } public function case_visit_scalar_boolean() { return $this->_case_visit_scalar(true); } public function case_visit_scalar_integer() { return $this->_case_visit_scalar(7); } public function case_visit_scalar_float() { return $this->_case_visit_scalar(4.2); } public function case_visit_scalar_string() { return $this->_case_visit_scalar('foo'); } protected function _case_visit_scalar($scalar) { $this ->given( $bag = new LUT\Model\Bag\Scalar($scalar), $asserter = new SUT() ) ->when($result = $this->invoke($asserter)->visitScalar($bag)) ->then ->variable($result) ->isIdenticalTo($scalar) ->variable($asserter->visit($bag)) ->isIdenticalTo($result); } public function case_visit_array() { $this ->given( $bag = new LUT\Model\Bag\RulerArray(['foo']), $asserter = new SUT() ) ->when($result = $this->invoke($asserter)->visitArray($bag)) ->then ->array($result) ->isEqualTo(['foo']) ->variable($asserter->visit($bag)) ->isIdenticalTo($result); } public function case_visit_undefined_context() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $asserter = new SUT() ) ->exception(function () use ($asserter, $bag) { $this->invoke($asserter)->visitContext($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Assert needs a context to work properly.') ->exception(function () use ($asserter, $bag) { $asserter->visit($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Assert needs a context to work properly.'); } public function case_visit_context_undefined_reference() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $context = new LUT\Context(), $asserter = new SUT($context) ) ->exception(function () use ($asserter, $bag) { $this->invoke($asserter)->visitContext($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Context reference x does not exist.') ->exception(function () use ($asserter, $bag) { $asserter->visit($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Context reference x does not exist.'); } public function case_visit_context() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $context = new LUT\Context(['x' => 42]), $asserter = new SUT($context) ) ->when($result = $this->invoke($asserter)->visitContext($bag)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($bag)) ->isIdenticalTo($result); } public function case_visit_context_array_dimension_1() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $bag->index('y'), $context = new LUT\Context(['x' => ['y' => 42]]), $asserter = new SUT($context) ) ->when($result = $this->invoke($asserter)->visitContext($bag)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($bag)) ->isIdenticalTo($result); } public function case_visit_context_array_dimension_2() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $bag->index('y'), $bag->index('z'), $context = new LUT\Context(['x' => ['y' => ['z' => 42]]]), $asserter = new SUT($context) ) ->when($result = $this->invoke($asserter)->visitContext($bag)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($bag)) ->isIdenticalTo($result); } public function case_visit_context_array_dimension_1_undefined_index() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $bag->index('z'), $context = new LUT\Context(['x' => ['y' => 42]]), $asserter = new SUT($context) ) ->exception(function () use ($asserter, $bag) { $this->invoke($asserter)->visitContext($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to access to an undefined index: z (dimension number 1 of x).') ->exception(function () use ($asserter, $bag) { $asserter->visit($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to access to an undefined index: z (dimension number 1 of x).'); } public function case_visit_context_array_dimension_1_not_an_array() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $bag->index('y'), $context = new LUT\Context(['x' => 42]), $asserter = new SUT($context) ) ->exception(function () use ($asserter, $bag) { $this->invoke($asserter)->visitContext($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to access to an undefined index: y (dimension number 1 of x), because it is not an array.') ->exception(function () use ($asserter, $bag) { $asserter->visit($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to access to an undefined index: y (dimension number 1 of x), because it is not an array.'); } public function case_visit_context_attribute_dimension_1() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $bag->attribute('y'), $context = new LUT\Context(['x' => (object) ['y' => 42]]), $asserter = new SUT($context) ) ->when($result = $this->invoke($asserter)->visitContext($bag)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($bag)) ->isIdenticalTo($result); } public function case_visit_context_attribute_dimension_2() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $bag->attribute('y'), $bag->attribute('z'), $context = new LUT\Context(['x' => (object) ['y' => (object) ['z' => 42]]]), $asserter = new SUT($context) ) ->when($result = $this->invoke($asserter)->visitContext($bag)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($bag)) ->isIdenticalTo($result); } public function case_visit_context_attribute_dimension_1_undefined_name() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $bag->attribute('z'), $context = new LUT\Context(['x' => (object) ['y' => 42]]), $asserter = new SUT($context) ) ->exception(function () use ($asserter, $bag) { $this->invoke($asserter)->visitContext($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to read an undefined attribute: z (dimension number 1 of x).') ->exception(function () use ($asserter, $bag) { $asserter->visit($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to read an undefined attribute: z (dimension number 1 of x).'); } public function case_visit_context_attribute_dimension_1_not_an_object() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $bag->attribute('y'), $context = new LUT\Context(['x' => 42]), $asserter = new SUT($context) ) ->exception(function () use ($asserter, $bag) { $this->invoke($asserter)->visitContext($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to read an undefined attribute: y (dimension number 1 of x), because it is not an object.') ->exception(function () use ($asserter, $bag) { $asserter->visit($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to read an undefined attribute: y (dimension number 1 of x), because it is not an object.'); } public function case_visit_context_method_dimension_1() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $bag->call(new LUT\Model\Operator('f', [7, 35])), $context = new LUT\Context(['x' => new C()]), $asserter = new SUT($context) ) ->when($result = $this->invoke($asserter)->visitContext($bag)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($bag)) ->isIdenticalTo($result); } public function case_visit_context_method_dimension_2() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $bag->call(new LUT\Model\Operator('newMe')), $bag->call(new LUT\Model\Operator('f', [7, 35])), $context = new LUT\Context(['x' => new C()]), $asserter = new SUT($context) ) ->when($result = $this->invoke($asserter)->visitContext($bag)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($bag)) ->isIdenticalTo($result); } public function case_visit_context_method_dimension_1_undefined_method() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $bag->call(new LUT\Model\Operator('h')), $context = new LUT\Context(['x' => new C()]), $asserter = new SUT($context) ) ->exception(function () use ($asserter, $bag) { $this->invoke($asserter)->visitContext($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to call an undefined method: h (dimension number 1 of x).') ->exception(function () use ($asserter, $bag) { $asserter->visit($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to call an undefined method: h (dimension number 1 of x).'); } public function case_visit_context_method_dimension_1_not_an_object() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $bag->call(new LUT\Model\Operator('f', [7, 35])), $context = new LUT\Context(['x' => 42]), $asserter = new SUT($context) ) ->exception(function () use ($asserter, $bag) { $this->invoke($asserter)->visitContext($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to call an undefined method: f (dimension number 1 of x), because it is not an object.') ->exception(function () use ($asserter, $bag) { $asserter->visit($bag); }) ->isInstanceOf(LUT\Exception\Asserter::class) ->hasMessage('Try to call an undefined method: f (dimension number 1 of x), because it is not an object.'); } public function case_visit_context_mixed_dimensions() { $this ->given( $bag = new LUT\Model\Bag\Context('x'), $bag->index('y'), $bag->attribute('z'), $bag->call(new LUT\Model\Operator('f', [7, 35])), $context = new LUT\Context(['x' => ['y' => (object) ['z' => new C()]]]), $asserter = new SUT($context) ) ->when($result = $this->invoke($asserter)->visitContext($bag)) ->then ->integer($result) ->isEqualTo(42) ->integer($asserter->visit($bag)) ->isIdenticalTo($result); } } class C { public function f($x, $y) { return $x + $y; } public function newMe() { return new self(); } } ================================================ FILE: Test/Unit/Visitor/Compiler.php ================================================ when($result = new SUT()) ->then ->object($result) ->isInstanceOf(Visitor\Visit::class); } public function case_model() { return $this->_case( 'true', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' true;' ); } public function case_operator() { return $this->_case( '7 < 42', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->{\'<\'}(' . "\n" . ' 7,' . "\n" . ' 42' . "\n" . ' );' ); } public function case_operator_is_an_identifier() { return $this->_case( 'true and false', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->and(' . "\n" . ' true,' . "\n" . ' false' . "\n" . ' );' ); } public function case_function() { return $this->_case( 'f(7, 42)', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->func(' . "\n" . ' \'f\',' . "\n" . ' 7,' . "\n" . ' 42' . "\n" . ' );' ); } public function case_function_of_arity_1() { return $this->_case( 'f(7)', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->func(' . "\n" . ' \'f\',' . "\n" . ' 7' . "\n" . ' );' ); } public function case_function_with_array_dimension() { return $this->_case( 'x(7)[42]', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->func(' . "\n" . ' \'x\',' . "\n" . ' 7' . "\n" . ' )' . "\n" . ' ->index(' . "\n" . ' 42' . "\n" . ' );' ); } public function case_function_with_attribute_dimension() { return $this->_case( 'x(7).y', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->func(' . "\n" . ' \'x\',' . "\n" . ' 7' . "\n" . ' )' . "\n" . ' ->attribute(\'y\');' ); } public function case_function_with_call_dimension() { return $this->_case( 'x(7).y(42)', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->func(' . "\n" . ' \'x\',' . "\n" . ' 7' . "\n" . ' )' . "\n" . ' ->call(' . "\n" . ' $model->func(' . "\n" . ' \'y\',' . "\n" . ' 42' . "\n" . ' )' . "\n" . ' );' ); } public function case_function_with_many_dimensions() { return $this->_case( 'x(7).y(42).z[153]', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->func(' . "\n" . ' \'x\',' . "\n" . ' 7' . "\n" . ' )' . "\n" . ' ->call(' . "\n" . ' $model->func(' . "\n" . ' \'y\',' . "\n" . ' 42' . "\n" . ' )' . "\n" . ' )' . "\n" . ' ->attribute(\'z\')' . "\n" . ' ->index(' . "\n" . ' 153' . "\n" . ' );' ); } public function case_scalar_true() { return $this->_case( 'true', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' true;' ); } public function case_scalar_false() { return $this->_case( 'false', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' false;' ); } public function case_scalar_null() { return $this->_case( 'null and true', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->and(' . "\n" . ' null,' . "\n" . ' true' . "\n" . ' );' ); } public function case_scalar_numeric() { return $this->_case( '7', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' 7;' ); } public function case_scalar_string() { return $this->_case( "'Hello, World!'", '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' \'Hello, World!\';' ); } public function case_scalar_escaped_string() { return $this->_case( "'He\llo, \'World\'!'", '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' \'He\llo, \\\'World\\\'!\';' ); } public function case_array() { return $this->_case( '[7, true, \'foo\']', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' [' . "\n" . ' 7,' . "\n" . ' true,' . "\n" . ' \'foo\'' . "\n" . ' ];' ); } public function case_context() { return $this->_case( 'x', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->variable(\'x\');' ); } public function case_context_with_array_dimension() { return $this->_case( 'x[7]', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->variable(\'x\')' . "\n" . ' ->index(' . "\n" . ' 7' . "\n" . ' );' ); } public function case_context_with_array_dimensions() { return $this->_case( 'x[7][42]', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->variable(\'x\')' . "\n" . ' ->index(' . "\n" . ' 7' . "\n" . ' )' . "\n" . ' ->index(' . "\n" . ' 42' . "\n" . ' );' ); } public function case_context_with_attribute_dimension() { return $this->_case( 'x.y', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->variable(\'x\')' . "\n" . ' ->attribute(\'y\');' ); } public function case_context_with_attribute_dimensions() { return $this->_case( 'x.y.z', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->variable(\'x\')' . "\n" . ' ->attribute(\'y\')' . "\n" . ' ->attribute(\'z\');' ); } public function case_context_with_call_dimension() { return $this->_case( 'x.y(7)', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->variable(\'x\')' . "\n" . ' ->call(' . "\n" . ' $model->func(' . "\n" . ' \'y\',' . "\n" . ' 7' . "\n" . ' )' . "\n" . ' );' ); } public function case_context_with_call_dimensions() { return $this->_case( 'x.y(7).z(42)', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->variable(\'x\')' . "\n" . ' ->call(' . "\n" . ' $model->func(' . "\n" . ' \'y\',' . "\n" . ' 7' . "\n" . ' )' . "\n" . ' )' . "\n" . ' ->call(' . "\n" . ' $model->func(' . "\n" . ' \'z\',' . "\n" . ' 42' . "\n" . ' )' . "\n" . ' );' ); } public function case_context_with_many_dimensions() { return $this->_case( 'a.b(7).c[42].d.e(153).f', '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . ' $model->variable(\'a\')' . "\n" . ' ->call(' . "\n" . ' $model->func(' . "\n" . ' \'b\',' . "\n" . ' 7' . "\n" . ' )' . "\n" . ' )' . "\n" . ' ->attribute(\'c\')' . "\n" . ' ->index(' . "\n" . ' 42' . "\n" . ' )' . "\n" . ' ->attribute(\'d\')' . "\n" . ' ->call(' . "\n" . ' $model->func(' . "\n" . ' \'e\',' . "\n" . ' 153' . "\n" . ' )' . "\n" . ' )' . "\n" . ' ->attribute(\'f\');' ); } protected function _case($rule, $compiled) { $this ->given($compiler = new SUT()) ->when($result = $compiler->visit(LUT::interpret($rule))) ->then ->string($result) ->isEqualTo($compiled); } } ================================================ FILE: Test/Unit/Visitor/Disassembly.php ================================================ when($result = new SUT()) ->then ->object($result) ->isInstanceOf(Visitor\Visit::class); } public function case_model() { return $this->_case( 'true', 'true' ); } public function case_operator() { return $this->_case( '7 < 42', '(7 < 42)' ); } public function case_operator_is_an_identifier() { return $this->_case( 'true and false', '(true and false)' ); } public function case_function() { return $this->_case( 'f(7, 42)', 'f(7, 42)' ); } public function case_function_of_arity_1() { return $this->_case( 'f(7)', 'f(7)' ); } public function case_function_with_array_dimensions() { return $this->_case( 'x(7)[42]', 'x(7)[42]' ); } public function case_function_with_attribute_dimensions() { return $this->_case( 'x(7).y', 'x(7).y' ); } public function case_function_with_call_dimensions() { return $this->_case( 'x(7).y(42)', 'x(7).y(42)' ); } public function case_function_with_many_dimensions() { return $this->_case( 'x(7).y(42).z[153]', 'x(7).y(42).z[153]' ); } public function case_scalar_true() { return $this->_case( 'true', 'true' ); } public function case_scalar_false() { return $this->_case( 'false', 'false' ); } public function case_scalar_null() { return $this->_case( 'null and true', '(null and true)' ); } public function case_scalar_numeric() { return $this->_case( '7', '7' ); } public function case_scalar_string() { return $this->_case( "'Hello, World!'", "'Hello, World!'" ); } public function case_scalar_escaped_string() { return $this->_case( "'He\llo, \'World\'!'", "'He\llo, \'World\'!'" ); } public function case_array() { return $this->_case( '[7, true, \'foo\']', '[7, true, \'foo\']' ); } public function case_context() { return $this->_case( 'x', 'x' ); } public function case_context_with_array_dimension() { return $this->_case( 'x[7]', 'x[7]' ); } public function case_context_with_attribute_dimension() { return $this->_case( 'x.y', 'x.y' ); } public function case_context_with_call_dimension() { return $this->_case( 'x.y(7)', 'x.y(7)' ); } public function case_context_with_many_dimensions() { return $this->_case( 'x.y(7).z[42]', 'x.y(7).z[42]' ); } protected function _case($rule, $disassembled) { $this ->given($compiler = new SUT()) ->when($result = $compiler->visit(LUT::interpret($rule))) ->then ->string($result) ->isEqualTo($disassembled); } } ================================================ FILE: Test/Unit/Visitor/Interpreter.php ================================================ when($result = new SUT()) ->then ->object($result) ->isInstanceOf(Visitor\Visit::class); } public function case_model() { return $this->_case( 'true', function () { $model = new LUT\Model(); $model->expression = true; return $model; } ); } public function case_operator() { return $this->_case( '7 < 42', function () { $model = new LUT\Model(); $model->expression = $model->{'<'}( 7, 42 ); return $model; } ); } public function case_operator_is_an_identifier() { return $this->_case( 'true and false', function () { $model = new LUT\Model(); $model->expression = $model->and( true, false ); return $model; } ); } public function case_operator_and() { return $this->_case( 'true and false', function () { $model = new LUT\Model(); $model->expression = $model->and( true, false ); return $model; } ); } public function case_operator_or() { return $this->_case( 'true or false', function () { $model = new LUT\Model(); $model->expression = $model->or( true, false ); return $model; } ); } public function case_operator_xor() { return $this->_case( 'true xor false', function () { $model = new LUT\Model(); $model->expression = $model->xor( true, false ); return $model; } ); } public function case_operator_not() { return $this->_case( 'not true', function () { $model = new LUT\Model(); $model->expression = $model->not( true ); return $model; } ); } public function case_function() { return $this->_case( 'f(7, 42)', function () { $model = new LUT\Model(); $model->expression = $model->func( 'f', 7, 42 ); return $model; } ); } public function case_function_of_arity_1() { return $this->_case( 'f(7)', function () { $model = new LUT\Model(); $model->expression = $model->func( 'f', 7 ); return $model; } ); } public function case_function_with_array_dimension() { return $this->_case( 'x(7)[42]', function () { $model = new LUT\Model(); $model->expression = $model->func( 'x', 7 ) ->index( 42 ); return $model; } ); } public function case_function_with_attribute_dimension() { return $this->_case( 'x(7).y', function () { $model = new LUT\Model(); $model->expression = $model->func( 'x', 7 ) ->attribute('y'); return $model; } ); } public function case_function_with_call_dimension() { return $this->_case( 'x(7).y(42)', function () { $model = new LUT\Model(); $model->expression = $model->func( 'x', 7 ) ->call( $model->func( 'y', 42 ) ); return $model; } ); } public function case_function_with_many_dimensions() { return $this->_case( 'x(7).y(42).z[153]', function () { $model = new LUT\Model(); $model->expression = $model->func( 'x', 7 ) ->call( $model->func( 'y', 42 ) ) ->attribute('z') ->index( 153 ); return $model; } ); } public function case_scalar_true() { return $this->_case( 'true', function () { $model = new LUT\Model(); $model->expression = true; return $model; } ); } public function case_scalar_false() { return $this->_case( 'false', function () { $model = new LUT\Model(); $model->expression = false; return $model; } ); } public function case_scalar_null() { return $this->_case( 'null and true', function () { $model = new LUT\Model(); $model->expression = $model->and( null, true ); return $model; } ); } public function case_scalar_float() { return $this->_case( '4.2', function () { $model = new LUT\Model(); $model->expression = 4.2; return $model; } ); } public function case_scalar_integer() { return $this->_case( '7', function () { $model = new LUT\Model(); $model->expression = 7; return $model; } ); } public function case_scalar_string() { return $this->_case( "'Hello, World!'", function () { $model = new LUT\Model(); $model->expression = 'Hello, World!'; return $model; } ); } public function case_scalar_escaped_string() { return $this->_case( "'He\llo, \'World\'!'", function () { $model = new LUT\Model(); $model->expression = 'He\llo, \'World\'!'; return $model; } ); } public function case_array() { return $this->_case( '[7, true, \'foo\']', function () { $model = new LUT\Model(); $model->expression = [ 7, true, 'foo' ]; return $model; } ); } public function case_context() { return $this->_case( 'x', function () { $model = new LUT\Model(); $model->expression = $model->variable('x'); return $model; } ); } public function case_context_with_array_dimension() { return $this->_case( 'x[7]', function () { $model = new LUT\Model(); $model->expression = $model->variable('x') ->index( 7 ); return $model; } ); } public function case_context_with_array_dimensions() { return $this->_case( 'x[7][42]', function () { $model = new LUT\Model(); $model->expression = $model->variable('x') ->index( 7 ) ->index( 42 ); return $model; } ); } public function case_context_with_attribute_dimension() { return $this->_case( 'x.y', function () { $model = new LUT\Model(); $model->expression = $model->variable('x') ->attribute('y'); return $model; } ); } public function case_context_with_attribute_dimensions() { return $this->_case( 'x.y.z', function () { $model = new LUT\Model(); $model->expression = $model->variable('x') ->attribute('y') ->attribute('z'); return $model; } ); } public function case_context_with_many_dimensions() { return $this->_case( 'a.b(7).c[42].d.e(153).f', function () { $model = new LUT\Model(); $model->expression = $model->variable('a') ->call( $model->func( 'b', 7 ) ) ->attribute('c') ->index( 42 ) ->attribute('d') ->call( $model->func( 'e', 153 ) ) ->attribute('f'); return $model; } ); } protected function _case($rule, \Closure $expected) { $this ->given( $interpreter = new SUT(), $ast = LUT::getCompiler()->parse($rule) ) ->when($result = $interpreter->visit($ast)) ->then ->object($result) ->isEqualTo($expected()); } } ================================================ FILE: Visitor/Asserter.php ================================================ setContext($context); } $this->setOperator('and', function ($a = false, $b = false) { return $a && $b; }); $this->setOperator('or', function ($a = false, $b = false) { return $a || $b; }); $this->setOperator('xor', function ($a, $b) { return (bool) ($a ^ $b); }); $this->setOperator('not', function ($a) { return !$a; }); $this->setOperator('=', function ($a, $b) { return $a == $b; }); $this->setOperator('is', $this->getOperator('=')); $this->setOperator('!=', function ($a, $b) { return $a != $b; }); $this->setOperator('>', function ($a, $b) { return $a > $b; }); $this->setOperator('>=', function ($a, $b) { return $a >= $b; }); $this->setOperator('<', function ($a, $b) { return $a < $b; }); $this->setOperator('<=', function ($a, $b) { return $a <= $b; }); $this->setOperator('in', function ($a, array $b) { return in_array($a, $b); }); $this->setOperator('sum', function () { return array_sum(func_get_args()); }); $this->setOperator('matches', function ($subject, $pattern) { $escapedPattern = preg_replace('/(?visitModel($element, $handle, $eldnah); } if ($element instanceof Ruler\Model\Operator) { return $this->visitOperator($element, $handle, $eldnah); } if ($element instanceof Ruler\Model\Bag\Scalar) { return $this->visitScalar($element, $handle, $eldnah); } if ($element instanceof Ruler\Model\Bag\RulerArray) { return $this->visitArray($element, $handle, $eldnah); } if ($element instanceof Ruler\Model\Bag\Context) { return $this->visitContext($element, $handle, $eldnah); } } /** * Visit a model * * @param \Hoa\Visitor\Element $element Element to visit. * @param mixed &$handle Handle (reference). * @param mixed $eldnah Handle (not reference). * @return mixed */ public function visitModel(Ruler\Model $element, &$handle = null, $eldnah = null) { return (bool) $element->getExpression()->accept($this, $handle, $eldnah); } /** * Visit an operator * * @param \Hoa\Visitor\Element $element Element to visit. * @param mixed &$handle Handle (reference). * @param mixed $eldnah Handle (not reference). * @return mixed */ protected function visitOperator(Ruler\Model\Operator $element, &$handle = null, $eldnah = null) { $name = $element->getName(); $arguments = []; foreach ($element->getArguments() as $argument) { $value = $argument->accept($this, $handle, $eldnah); $arguments[] = $value; if ($element::LAZY_BREAK === $element->shouldBreakLazyEvaluation($value)) { break; } } if (false === $this->operatorExists($name)) { throw new Ruler\Exception\Asserter( 'Operator %s does not exist.', 0, $name ); } $value = $this->getOperator($name)->distributeArguments($arguments); return $this->visitDimensions( $element, $name . '()', $value, $handle, $eldnah ); } /** * Visit a scalar * * @param \Hoa\Visitor\Element $element Element to visit. * @param mixed &$handle Handle (reference). * @param mixed $eldnah Handle (not reference). * @return mixed */ protected function visitScalar(Ruler\Model\Bag\Scalar $element, &$handle = null, $eldnah = null) { return $element->getValue(); } /** * Visit an array * * @param \Hoa\Visitor\Element $element Element to visit. * @param mixed &$handle Handle (reference). * @param mixed $eldnah Handle (not reference). * @return array */ protected function visitArray(Ruler\Model\Bag\RulerArray $element, &$handle = null, $eldnah = null) { $out = []; foreach ($element->getArray() as $key => $data) { $out[$key] = $data->accept($this, $handle, $eldnah); } return $out; } /** * Visit a context * * @param \Hoa\Visitor\Element $element Element to visit. * @param mixed &$handle Handle (reference). * @param mixed $eldnah Handle (not reference). * @return mixed */ protected function visitContext(Ruler\Model\Bag\Context $element, &$handle = null, $eldnah = null) { $context = $this->getContext(); if (null === $context) { throw new Ruler\Exception\Asserter( 'Assert needs a context to work properly.', 1 ); } $id = $element->getId(); if (!isset($context[$id])) { throw new Ruler\Exception\Asserter( 'Context reference %s does not exist.', 2, $id ); } return $this->visitDimensions( $element, $id, $context[$id], $handle, $eldnah ); } /** * Visit dimensions of a context. * * @param \Hoa\Visitor\Element $element Element to visit. * @param string $elementId Element name. * @param mixed $subject Current root to apply the dimensions. * @param mixed &$handle Handle (reference). * @param mixed $eldnah Handle (not reference). * @return mixed */ protected function visitDimensions( Ruler\Model\Bag\Context $element, $elementId, $subject, &$handle = null, $eldnah = null ) { $pointer = $subject; foreach ($element->getDimensions() as $dimensionNumber => $dimension) { ++$dimensionNumber; switch ($dimension[Ruler\Model\Bag\Context::ACCESS_TYPE]) { case Ruler\Model\Bag\Context::ARRAY_ACCESS: $this->visitArrayDimension( $pointer, $dimension, $dimensionNumber, $elementId, $handle, $eldnah ); break; case Ruler\Model\Bag\Context::ATTRIBUTE_ACCESS: $this->visitAttributeDimension( $pointer, $dimension, $dimensionNumber, $elementId, $handle, $eldnah ); break; case Ruler\Model\Bag\Context::METHOD_ACCESS: $this->visitMethodDimension( $pointer, $dimension, $dimensionNumber, $elementId, $handle, $eldnah ); break; } } return $pointer; } /** * Visit an array dimension. * * @param mixed &$contextPointer Pointer to the current context. * @param array $dimension Dimension bucket. * @param int $dimensionNumber Dimension number. * @param string $elementId Element name. * @param mixed &$handle Handle (reference). * @param mixed $eldnah Handle (not reference). * @return void */ protected function visitArrayDimension( &$contextPointer, array $dimension, $dimensionNumber, $elementId, &$handle = null, $eldnah = null ) { $value = $dimension[Ruler\Model\Bag\Context::ACCESS_VALUE]; $key = $value->accept($this, $handle, $eldnah); $isArray = is_array($contextPointer); $isArrayLike = !$isArray && $contextPointer instanceof ArrayAccess; if (false === $isArray && false === $isArrayLike) { throw new Ruler\Exception\Asserter( 'Try to access to an undefined index: %s ' . '(dimension number %d of %s), because it is ' . 'not an array.', 3, [$key, $dimensionNumber, $elementId] ); } if ((true === $isArray && false === array_key_exists($key, $contextPointer)) || (true === $isArrayLike && false === $contextPointer->offsetExists($key))) { throw new Ruler\Exception\Asserter( 'Try to access to an undefined index: %s ' . '(dimension number %d of %s).', 4, [$key, $dimensionNumber, $elementId] ); } $contextPointer = $contextPointer[$key]; return; } /** * Visit an attribute dimension. * * @param mixed &$contextPointer Pointer to the current context. * @param array $dimension Dimension bucket. * @param int $dimensionNumber Dimension number. * @param string $elementId Element name. * @param mixed &$handle Handle (reference). * @param mixed $eldnah Handle (not reference). * @return void */ protected function visitAttributeDimension( &$contextPointer, array $dimension, $dimensionNumber, $elementId, &$handle = null, $eldnah = null) { $attribute = $dimension[Ruler\Model\Bag\Context::ACCESS_VALUE]; if (!is_object($contextPointer)) { throw new Ruler\Exception\Asserter( 'Try to read an undefined attribute: %s ' . '(dimension number %d of %s), because it is ' . 'not an object.', 5, [$attribute, $dimensionNumber, $elementId] ); } if (!property_exists($contextPointer, $attribute)) { throw new Ruler\Exception\Asserter( 'Try to read an undefined attribute: %s ' . '(dimension number %d of %s).', 6, [$attribute, $dimensionNumber, $elementId] ); } $contextPointer = $contextPointer->$attribute; return; } /** * Visit a method dimension. * * @param mixed &$contextPointer Pointer to the current context. * @param array $dimension Dimension bucket. * @param int $dimensionNumber Dimension number. * @param string $elementId Element name. * @param mixed &$handle Handle (reference). * @param mixed $eldnah Handle (not reference). * @return void */ protected function visitMethodDimension( &$contextPointer, array $dimension, $dimensionNumber, $elementId, &$handle = null, $eldnah = null ) { $value = $dimension[Ruler\Model\Bag\Context::ACCESS_VALUE]; $method = $value->getName(); if (!is_object($contextPointer)) { throw new Ruler\Exception\Asserter( 'Try to call an undefined method: %s ' . '(dimension number %d of %s), because it is ' . 'not an object.', 7, [$method, $dimensionNumber, $elementId] ); } if (!method_exists($contextPointer, $method)) { throw new Ruler\Exception\Asserter( 'Try to call an undefined method: %s ' . '(dimension number %d of %s).', 8, [$method, $dimensionNumber, $elementId] ); } $arguments = []; foreach ($value->getArguments() as $argument) { $arguments[] = $argument->accept($this, $handle, $eldnah); } $contextPointer = call_user_func_array( [$contextPointer, $method], $arguments ); return; } /** * Set context. * * @param \Hoa\Ruler\Context $context Context. * @return \Hoa\Ruler\Context */ public function setContext(Ruler\Context $context) { $old = $this->_context; $this->_context = $context; return $old; } /** * Get context. * * @return \Hoa\Ruler\Context */ public function getContext() { return $this->_context; } /** * Set an operator. * * @param string $operator Operator. * @param callable $callable Callable. * @return Ruler\Visitor\Asserter */ public function setOperator($operator, $callable) { $this->_operators[$operator] = $callable; return $this; } /** * Check if an operator exists. * * @param string $operator Operator. * @return bool */ public function operatorExists($operator) { return true === array_key_exists($operator, $this->_operators); } /** * Get an operator. * * @param string $operator Operator. * @return string */ public function getOperator($operator) { if (false === $this->operatorExists($operator)) { return null; } $handle = &$this->_operators[$operator]; if (!($handle instanceof Consistency\Xcallable)) { $handle = xcallable($handle); } return $this->_operators[$operator]; } /** * Get all operators. * * @return array */ public function getOperators() { foreach ($this->_operators as &$operator) { if (!($operator instanceof Consistency\Xcallable)) { $operator = xcallable($operator); } } return $this->_operators; } } ================================================ FILE: Visitor/Compiler.php ================================================ _indentation); if ($element instanceof Ruler\Model) { $expression = $element->getExpression(); if (null === $expression) { $out = ''; } else { $this->_indentation = 1; $out = '$model = new \Hoa\Ruler\Model();' . "\n" . '$model->expression =' . "\n" . $expression->accept($this, $handle, $eldnah) . ';'; } } elseif ($element instanceof Ruler\Model\Operator) { $out = $_ . '$model->'; $name = $element->getName(); $_handle = []; if (false === $element->isFunction()) { if (true === Consistency::isIdentifier($name)) { $out .= $name; } else { $out .= '{\'' . $name . '\'}'; } $out .= '(' . "\n"; } else { $out .= 'func(' . "\n" . $_ . ' '; $_handle[] = '\'' . $name . '\''; } ++$this->_indentation; foreach ($element->getArguments() as $argument) { $_handle[] = $argument->accept($this, $handle, $eldnah); } $out .= implode(',' . "\n", $_handle) . "\n" . $_ . ')' . $this->visitContext($element, $handle, $eldnah, $_); --$this->_indentation; } elseif ($element instanceof Ruler\Model\Bag\Scalar) { $value = $element->getValue(); $out = $_; if (true === $value) { $out .= 'true'; } elseif (false === $value) { $out .= 'false'; } elseif (null === $value) { $out .= 'null'; } elseif (is_numeric($value)) { $out .= (string) $value; } else { $out .= '\'' . str_replace(['\'', '\\\\'], ['\\\'', '\\'], $value) . '\''; } } elseif ($element instanceof Ruler\Model\Bag\RulerArray) { $values = []; ++$this->_indentation; foreach ($element->getArray() as $value) { $values[] = $value->accept($this, $handle, $eldnah); } --$this->_indentation; $out = $_ . '[' . "\n" . implode(',' . "\n", $values) . "\n" . $_ . ']'; } elseif ($element instanceof Ruler\Model\Bag\Context) { ++$this->_indentation; $out = $_ . '$model->variable(\'' . $element->getId() . '\')' . $this->visitContext($element, $handle, $eldnah, $_); --$this->_indentation; } return $out; } /** * Visit a context. * * @param \Hoa\Ruler\Model\Bag\Context $context Context. * @param mixed &$handle Handle (reference). * @param mixed $eldnah Handle (not reference). * @param string $_ Indentation. * @return mixed */ protected function visitContext(Ruler\Model\Bag\Context $context, &$handle, $eldnah, $_) { $out = null; foreach ($context->getDimensions() as $dimension) { ++$this->_indentation; $value = $dimension[Ruler\Model\Bag\Context::ACCESS_VALUE]; $out .= "\n" . $_ . ' ->'; switch ($dimension[Ruler\Model\Bag\Context::ACCESS_TYPE]) { case Ruler\Model\Bag\Context::ARRAY_ACCESS: $out .= 'index(' . "\n" . $value->accept($this, $handle, $eldnah) . "\n" . $_ . ' )'; break; case Ruler\Model\Bag\Context::ATTRIBUTE_ACCESS: $out .= 'attribute(\'' . $value . '\')'; break; case Ruler\Model\Bag\Context::METHOD_ACCESS: $out .= 'call(' . "\n" . $value->accept($this, $handle, $eldnah) . "\n" . $_ . ' )'; break; } --$this->_indentation; } return $out; } } ================================================ FILE: Visitor/Disassembly.php ================================================ getExpression()->accept($this, $handle, $eldnah); } elseif ($element instanceof Ruler\Model\Operator) { $name = $element->getName(); $arguments = []; foreach ($element->getArguments() as $argument) { $arguments[] = $argument->accept($this, $handle, $eldnah); } if (true === $element->isFunction()) { $out .= $name . '(' . implode(', ', $arguments) . ')'; } else { if (!isset($arguments[1])) { $_out = $name . ' ' . $arguments[0]; } else { $_out = '(' . $arguments[0] . ' ' . $name . ' ' . $arguments[1] . ')'; } $out .= $_out; } $out .= $this->visitContext($element, $handle, $eldnah); } elseif ($element instanceof Ruler\Model\Bag\Scalar) { $value = $element->getValue(); if (true === $value) { $out .= 'true'; } elseif (false === $value) { $out .= 'false'; } elseif (null === $value) { $out .= 'null'; } elseif (is_numeric($value)) { $out .= (string) $value; } else { $out .= '\'' . str_replace(['\\', '\''], ['\\', '\\\''], $value) . '\''; } } elseif ($element instanceof Ruler\Model\Bag\RulerArray) { $values = []; foreach ($element->getArray() as $value) { $values[] = $value->accept($this, $handle, $eldnah); } $out .= '[' . implode(', ', $values) . ']'; } elseif ($element instanceof Ruler\Model\Bag\Context) { $out .= $element->getId() . $this->visitContext($element, $handle, $eldnah); } return $out; } /** * Visit a context. * * @param \Hoa\Ruler\Model\Bag\Context $context Context. * @param mixed &$handle Handle (reference). * @param mixed $eldnah Handle (not reference). * @return mixed */ protected function visitContext(Ruler\Model\Bag\Context $context, &$handle, $eldnah) { $out = null; foreach ($context->getDimensions() as $dimension) { $value = $dimension[Ruler\Model\Bag\Context::ACCESS_VALUE]; switch ($dimension[Ruler\Model\Bag\Context::ACCESS_TYPE]) { case Ruler\Model\Bag\Context::ARRAY_ACCESS: $out .= '[' . $value->accept($this, $handle, $eldnah) . ']'; break; case Ruler\Model\Bag\Context::ATTRIBUTE_ACCESS: $out .= '.' . $value; break; case Ruler\Model\Bag\Context::METHOD_ACCESS: $out .= '.' . $value->accept($this, $handle, $eldnah); break; } } return $out; } } ================================================ FILE: Visitor/Interpreter.php ================================================ getId(); $variable = false !== $eldnah; switch ($id) { case '#expression': $this->_root = new Ruler\Model(); $this->_root->expression = $element->getChild(0)->accept( $this, $handle, $eldnah ); return $this->_root; case '#operation': $children = $element->getChildren(); $left = $children[0]->accept($this, $handle, $eldnah); $right = $children[2]->accept($this, $handle, $eldnah); $name = $children[1]->accept($this, $handle, false); return $this->_root->_operator( $name, [$left, $right], false ); case '#variable_access': $children = $element->getChildren(); $name = $children[0]->accept($this, $handle, $eldnah); array_shift($children); foreach ($children as $child) { $_child = $child->accept($this, $handle, $eldnah); switch ($child->getId()) { case '#array_access': $name->index($_child); break; case '#attribute_access': $name->attribute($_child); break; case '#method_access': $name->call($_child); break; } } return $name; case '#array_access': return $element->getChild(0)->accept($this, $handle, $eldnah); case '#attribute_access': return $element->getChild(0)->accept($this, $handle, false); case '#method_access': return $element->getChild(0)->accept($this, $handle, $eldnah); case '#array_declaration': $out = []; foreach ($element->getChildren() as $child) { $out[] = $child->accept($this, $handle, $eldnah); } return $out; case '#function_call': $children = $element->getChildren(); $name = $children[0]->accept($this, $handle, false); array_shift($children); $arguments = []; foreach ($children as $child) { $arguments[] = $child->accept($this, $handle, $eldnah); } return $this->_root->_operator( $name, $arguments, true ); case '#and': case '#or': case '#xor': $name = substr($id, 1); $children = $element->getChildren(); $left = $children[0]->accept($this, $handle, $eldnah); $right = $children[1]->accept($this, $handle, $eldnah); return $this->_root->operation( $name, [$left, $right] ); case '#not': return $this->_root->operation( 'not', [$element->getChild(0)->accept($this, $handle, $eldnah)] ); case 'token': $token = $element->getValueToken(); $value = $element->getValueValue(); switch ($token) { case 'identifier': return true === $variable ? $this->_root->variable($value) : $value; case 'true': return true; case 'false': return false; case 'null': return null; case 'float': return floatval($value); case 'integer': return intval($value); case 'string': return str_replace( '\\' . $value[0], $value[0], substr($value, 1, -1) ); default: throw new Ruler\Exception\Interpreter( 'Token %s is unknown.', 0, $token ); } break; default: throw new Ruler\Exception\Interpreter( 'Element %s is unknown.', 1, $id ); } return; } /** * Get root. * * @return \Hoa\Ruler\Model */ public function getRoot() { return $this->_root; } } ================================================ FILE: bors.toml ================================================ status = [ "continuous-integration/travis-ci/push" ] timeout_sec = 1800 ================================================ FILE: composer.json ================================================ { "name" : "hoa/ruler", "description": "The Hoa\\Ruler library.", "type" : "library", "keywords" : ["library", "ruler"], "homepage" : "https://hoa-project.net/", "license" : "BSD-3-Clause", "authors" : [ { "name" : "Ivan Enderlin", "email": "ivan.enderlin@hoa-project.net" }, { "name" : "Hoa community", "homepage": "https://hoa-project.net/" } ], "support": { "email" : "support@hoa-project.net", "irc" : "irc://chat.freenode.net/hoaproject", "forum" : "https://users.hoa-project.net/", "docs" : "https://central.hoa-project.net/Documentation/Library/Ruler", "source": "https://central.hoa-project.net/Resource/Library/Ruler" }, "require": { "php" : ">=5.5.0", "hoa/compiler" : "~3.0", "hoa/consistency": "~1.0", "hoa/exception" : "~1.0", "hoa/file" : "~1.0", "hoa/protocol" : "~1.0", "hoa/visitor" : "~2.0" }, "require-dev": { "hoa/test": "~2.0" }, "autoload": { "psr-4": { "Hoa\\Ruler\\": "." } }, "extra": { "branch-alias": { "dev-master": "2.x-dev" } } }