The Refinery
library is used to unify the way input is
processed by the ILIAS project.
Table of Contents
This library contains various implementations and interfaces to establish a way to secure input and transform values in a secure way.
The initial concept for this library can be found here.
These library also consists of sub-libraries, that can be used for transformations and validation. Checkout the chapter about these additional libraries.
global $DIC;
$refinery = $DIC->refinery();
$transformation = $refinery->in()->series(
array(
new ILIAS\Refinery\To\Transformation\IntegerTransformation(),
new ILIAS\Refinery\To\Transformation\IntegerTransformation()
)
);
$result = $transformation->transform(5);
$data = $refinery->data('alphanumeric')->transform(array($result));
echo $data->getData();
The output will be a integer
value: 5
The factory of the refinery interface can create an implementations of very different groups. These groups can be used for several validations and transformations.
A concrete implementation of the Refinery\Factory
interface is Refinery\Factory\BasicFactory
.
This implementation will create new instances of the
different groups.
The Refinery\Factory\BasicFactory
can also be accessed
via the ILIAS Dependency Injection Container(DIC)
.
global $DIC;
$refinery = $DIC->refinery();
$transformation = $refinery->to()->string();
// ...
Checkout the examples to see how these library can be used.
Info: These examples are just for a show case. These examples are non-operable from the console, because of the missing initialization of ILIAS
The different groups are used to validate and/or transform the input given to the certain transformation.
Because of the usage of the Transformation
interface
these groups can interact with each other and
with other implementation interfaces.
E.g. transformation from the to
group can be used in
the in
group and vice versa.
The to
group consists of combined validations and transformations
for native data types that establish a baseline for further constraints
and more complex transformations.
A concrete implementation for the Refinery\To\Group
interface
is the Refinery\To\BasicGroup
.
To learn more about transformations checkout the README about Transformations.
The transformations of this group are very strict, which means that there are several type checks before the transformation is executed.
global $DIC;
$refinery = $DIC->refinery();
$transformation = $refinery->to()->int();
$result = $transformation->transform(3.5); // Will throw exception because, values is not an integer value
$result = $transformation->transform('hello'); // Will throw exception because, values is not an integer value
$result = $transformation->transform(3); // $result = 3
In this example the ILIAS\Refinery\To\IntegerTransformation
of the to
group is
used.
The ILIAS\Refinery\To\IntegerTransformation
of this group is very strict,
so only elements of the integer
type are allowed.
Every non-matching value will throw an exception.
To avoid exception handling the applyTo
method can be used instead.
Find out more about the applyTo
method of instances of the Transformation
interface in the
README about Transformations.
As seen in the example of the previous chapter
there are transformations which cover the native data
types of PHP (int
, string
, float
and boolean
).
string()
- Returns an object that allows to transform a value to a string value.int()
- Returns an object that allows to transform a value to a integer value.float()
- Returns an object that allows to transform a value to a float value.bool()
- Returns an object that allows to transform a value to a boolean value.
Beside the native transformations there also
transformation to create structures like list
, dictonary
,
record
and tuple
.
listOf()
- Returns an object that allows to transform an value in a given array with the given transformation object. The transformation will be executed on every element of the array.dictOf()
- Returns an object that allows to transform an value in a given array with the given transformation object. The transformation will be executed on every element of the array.tupleOf()
- Returns an object that allows to transform the values of an array with the given array of transformations objects. The length of the array of transformations MUST be identical to the array of values to transform. The keys of the transformation array will be the same as the key from the value array e.g. Transformation on position 2 will transform value on position 2 of the value array.recordOf()
- Returns an object that allows to transform the values of an associative array with the given associative array of transformations objects. The length of the array of transformations MUST be identical to the array of values to transform. The keys of the transformation array will be the same as the key from the value array e.g. Transformation with the key "hello" will transform value with the key "hello" of the value array.toNew()
- Returns either an transformation object to create objects of an existing class, with variations of constructor parameters OR returns an transformation object to execute a certain method with variation of parameters on the objects.data()
- Returns a data factory to create a certain data type
The in
group is a group with a dict of Transformations
as parameters that define the content at the indices.
A concrete implementation for the Refinery\In\Group
interface
is the Refinery\In\BasicGroup
.
There are currently two different strategies supported by this group, that are accessible by the methods:
The transformation series
takes an array of transformations and
performs them one after another on the result of the previous transformation.
global $DIC;
$refinery = $DIC->refinery();
$transformation = $refinery->in()->series(
array(
new ILIAS\Refinery\To\Transformation\IntegerTransformation(),
new ILIAS\Refinery\To\Transformation\StringTransformation()
)
);
$result = $transformation->transform(5.5);
// $result => '5'
The result will be the end result of the transformations that were executed
in the strict order added in the series
method.
In this case it is a string
with the value '5'.
The transformation parallel
takes an array of transformations and
performs each on the input value to form a tuple of the results.
global $DIC;
$refinery = $DIC->refinery();
$transformation = $refinery->in()->parallel(
array(
new ILIAS\Refinery\To\Transformation\IntegerTransformation(),
new ILIAS\Refinery\To\Transformation\IntegerTransformation()
)
);
$result = $transformation->transform(5);
// $result => array(5, 5)
The result will be an array of results of each transformation.
In this case this is an array with an integer
and a string
value.
The Custom
group contains of Transformations
and Constraints
that can be used to create individual transformations and constraints.
The Logical
group contains of Constraints
that can be used to create
different logical operation that can be used on concrete Constraints
-
Null
group contains of constraints that can be used to identify the
null
value via a Constraint
.
Numeric
group consists of a constraints that can be used to identify a
numeric value via a Constraint
.
Password
consists of a contains that can be used to create constraints
for validating password.
String
consist of transformations and constraints which can be applied
to string inputs.
Sometimes the default transformations of this library are not enough, so a custom transformation is needed.
As every other transformation it must implement the
ILIAS\Refinery\To\Transformation
interface.
By default these transformation need an implementation for the
methods transformation
and applyTo
.
Because these methods are always containing the same basic process
(with different results types and exception handling),
this library contains traits to ease the creation of new transformation.
The traits that can be used are:
An example shows how the traits can be used.
class BooleanTransformation implements ILIAS\Refinery\Transformation
{
use ILIAS\Refinery\DeriveApplyToFromTransform;
use ILIAS\Refinery\DeriveInvokeFromTransform;
/**
* @inheritdoc
*/
public function transform($from)
{
if (false === is_bool($from)) {
throw new ILIAS\Refinery\ConstraintViolationException(
'The value MUST be of type boolean',
'not_boolean'
);
}
return (bool) $from;
}
}
In the above example we use the trait DeriveApplyToFromTransform
and only define the transform
method.
Please be aware that the error handling can vary by using these traits. Checkout the following chapters for more information.
This trait is used define applyTo
on its own.
Just the transform
method needs to be created in the new transformation class.
Exceptions thrown inside the transformation
method will be
caught and added to new
error result object (Result\Error
).
The origin exception can be accessed through this error object.
This trait is used define transform
on its own.
Just the applyTo
method needs to be created in the new transformation class.
Exceptions thrown inside the applyTo
method will be
not be caught.
On return of an error result object (Result\Error
)
the transform
method will throw an exception.
- If the content of the error object is an exception the exception will be thrown.
- If the content of the error object is an string this string will be added
to a an
Exception
which will be thrown.
These library consists of several sub-libraries, which have their own descriptions.
A transformation is a function from one type or structure of data to another. It MUST NOT perform any sideeffects, i.e. it must be morally impossible to observe how often the transformation was actually performed. It MUST NOT touch the provided value, i.e. it is allowed to create new values but not to modify existing values. This would be an observable sideeffect.
The actual usage of this interface is quite boring, but we could typehint on
Transformation
to announce we indeed want some function having the aforementioned
properties. Typehinting on Transformation
will be useful when code talks about
structures containing data in some sense, e.g. lists or trees, where the code is
involved with containing structure but not with the contained data. This would be
a classic case for generics in languages that support them. PHP unfortunately is
a language that does not support generics.
The use case that actually led to the proposal of this library is the forms abstraction in the UI framework, where the abstraction deals with forms, extraction of data from them and validation of data in them. The concept of transformation is required, not matter if we typehint on them or not. Other facilities in PHP do not allow a more accurate typehinting, due to lack of generics.
Having common transformations ready in a factory, connected with the promise
given by the developer that the Transformation
indeed respects the intended
properties, should be useful in other scenarios as well, especially at the
boundaries of the system, where data needs to be re- and destructured to fit
interfaces to other systems or even users.
global $DIC;
$f = $DIC->refinery();
// Adding labels to an array to name the elements.
$add_abc_label = $f->container()->addLabels(["a", "b", "c"]);
$labeled = $add_abc_label->transform([1,2,3]);
assert($labeled === ["a" => 1, "b" => 2, "c" => 3]);
// Split a string at some delimiter.
$split_string_at_dot = $f->string()->splitString(".");
$split = $split_string_at_dot->transform("a.b.c");
assert($split === ["a", "b", "c"]);
// Use a closure for the transformation.
$int_to_string = $f->custom()->transformation(function ($v) {
if (!is_int($v)) {
throw new \InvalidArgumentException("Expected int, got ".gettype($v));
}
return "$v";
});
$str = $int_to_string->transform(5);
assert($str === "5");
assert($str !== 5);
A validation checks some supplied value for compliance with some constraints. Validations MUST NOT modify the supplied value.
Having an interface to Validations allows to typehint on them and allows them to be combined in structured ways. Understanding validation as a separate service in the system with objects performing the validations makes it possible to build a set of common validations used throughout the system. Having a known set of validations makes it possible to perform at least some of the validations on client side someday.
global $DIC;
$f = $DIC->refinery();
// Build some basic constraints
$gt0 = $f->int()->isGreaterThan(0);
$lt10 = $f->int()->isLessThan(10);
// Check them and react:
if (!$gt0->accepts(1)) {
assert(false); // does not happen
}
// Let them throw an exception:
$raised = false;
try {
$lt10->check(20);
assert(false); // does not happen
}
catch (\UnexpectedValueException $e) {
$raised = true;
}
assert($raised);
// Get to know what the problem with some value is:
assert(is_string($gt0->problemWith(-10)));
// Combine them in a way that the constraints are checked one after another:
$between_0_10 = $f->logical()->sequential([$gt0, $lt10]);
// Or in a way that they are checked independently:
$also_between_0_10 = $f->logical()->parallel([$gt0, $lt10]);
// One can also create a new error message by supplying a builder for an error
// message:
$between_0_10->withProblemBuilder(function($txt, $value) {
return "Value must be between 0 and 10, but is '$value'.";
});
// To perform internationalisation, the provided $txt could be used, please
// see `ILIAS\Refinery\Validation\Constraint::withProblemBuilder` for further information.