utils
utils copied to clipboard
Added Nette\Utils\Future for lazy value evaluation
- new feature
- doc PR: will if accepted
The Future class is a helper for "building data structures" which I use about 4 years. It is handy for small tasks where ORM is too huge, or where two independent data sources are mixing up (database and database, HTTP API and database, local CSV and HTTP API...).
I used to call it "promise" but it is not a promise pattern. It is closer to "future evaluation strategy" but it is not the same. Maybe there exists the corrent name for this evaluation method but I didn't find it.
An example of usage:
# Fetch all books from database with theirs author & editor person ID.
$books = $db->query('
SELECT
title,
author_id AS author, -- this is an integer, ID of person
editor_id AS editor -- this is an integer, ID of person
FROM
book
')->fetchAll();
# Replace all authors & editors ID by corresponding entities.
$futurePersons = new Future(function (array $ids) use ($db) {
return $db->query('SELECT id, first_name, last_name FROM person')->fetchAssoc('id');
});
$futurePersons->bindArraysKey('author', $books);
$futurePersons->bindArraysKey('editor', $books);
$futurePersons->resolve();
On resolve() call, the resolver (callback from constructor) is called with all required authors and editors ID. So there is no need to iterate over books, collects every ID, fetch them and iterate again to build the desired data structure.
The example with database is a little bit funny (even I'm using it in this way). Some ORM does this task probably better. But this Future class is pretty low level helper. If you compose data structures from heterogeneous sources, like files in filesystem, HTTP API or some other JSON sources, the resulting code is nice, short and clean.
Another use case is scalar to value object translation. For example, translate every e-mail string, to Email object:
$resolver = function (array $keys) {
$result = [];
foreach ($keys as $key) {
$result[$key] = new Email($key);
}
return $result;
};
(new Future($resolver))
->bindArraysKey('from', $data)
->bindArraysKey('to', $data)
->resolve();
Or scalar to scalar translation (language translator).
Anyway, the workflow is following:
- write resolver
- bind variables for future evaluation
- resolve (translate)
and API looks like:
# Resolve 'key' and store it into $var
->bind('key', $var)
# Get $var as a key, resolve it and store it back to $var
->bindVar($var)
# Get an array of values as keys, and resolve them - like bindVar() for every array item
->bindArrayValues($array)
# The array keys are used as keys to be resolved and stored into array values - like bind('key', $var) for every key/value pair
->bindArrayKeys($array)
# Expects every item of $arrays is an array - do bind('key', $item) for every item
->bindArraysKey('key', $arrays)
# dtto but expects every item is an object and resolved value is stored into propery
->bindObjectsProperty('key', $arrays)
These are common use cases I hit and examples can be found in attached test.
In general, there is a space for ->bindAssoc('a[]->foo->bar', $structure) but I rarely use it. It could be nice but if required, manual iteration through the $structure and ->bind...() calls work too.
One dark side - it is quite hard to goole future term so maybe different name should be used.
I like it. But it really should have better name. It basically just maps/binds values from data source into. Maybe something like LazyMapper? LazyBinder?
Yep, the hardest thing :)
Deferred? Delayed?
I think there's a missing $ids, or am I misunderstanding?
$futurePersons = new Future(function (array $ids) use ($db) {
return $db->query('SELECT id, first_name, last_name FROM person')->fetchAssoc('id');
});
Yes, it is. Sorry, I wrote it from top of my head. Wrong way to introduce new concept :)
$futurePersons = new Future(function (array $ids) use ($db) {
return $db->query('SELECT id, first_name, last_name FROM person WHERE id IN %in', $ids)->fetchAssoc('id');
});
As for the name, IMHO it has nothing to do with the future, deferring or lazy loading. Rather, it is a bulk replacement of values in various structures, where the important thing is that the all values are first collected and then the replacements are generated, right?
Actually yes.
One note from a class history - I used to call it Promise. It was the very first idea I got and I didn't want to waste the time so I used it. And the higher level code looked like:
class PersonsRepository
{
public function promisePersonsById(): Promise
...
public function promisePersonsByUsername(): Promise
...
}
# and in an another service code
$promisePersonById = $this->personsRepo->promisePersonsById();
# or directly
$this->personsRepo->promisePersonsById()->bind(...)->resolve();
Or usage as a DI service
final class PersonByIdPromise extends Promise
{
public function __construct(private PersonsRepository $personsRepo)
{
parent::__construct(fn(array $ids) => $this->personsRepo->findByIds($ids));
}
}
final class PersonPromiseFactory
{
public function byId(): PersonByIdPromise
...
public function byUsername(): PersonByUsernamePromise
...
}
Promise is not a right name due to the promise pattern, but it was really good readable and understandable. So something like that - to be well readable in mentioned contexts.
What about calling it simply Loader or DataLoader?