Take a list of raw API records, derive a fresh view model for each — with computed fields, nested lookups, and conditional logic — using recordEncoder, encodeProperty, pluckProperty and compose. No foreach loops, no temporary variables.
recordEncoder — the scaffold that builds each output record from scratchencodeProperty — defines one output field by pairing a key with a value-producing callablegetProperty — reads a single field from an array or objectpluckProperty — reads a value along a nested pathpropertyEquals — predicate that asks "does this field equal that value?"compose — stitches several small transforms into oneifElse — conditional branch lifted into a callablealways — a callable that ignores its input and returns a fixed valueYou’re pulling user records out of an API. Each one looks like this — flat, a mix of strings, booleans, numbers, plus a nested profile object:
$apiUsers = [
[
'id' => 1,
'first' => 'ada',
'last' => 'lovelace',
'role' => 'admin',
'signups' => 12,
'profile' => (object) ['country' => 'GB', 'joined' => '2019-05-01'],
],
[
'id' => 2,
'first' => 'bea',
'last' => 'smith',
'role' => 'user',
'signups' => 0,
'profile' => (object) ['country' => 'US', 'joined' => '2024-11-17'],
],
];But the view layer wants something different — a tidy view model with a formatted display name, a derived boolean, a default when data is missing, and a field pulled from the nested object:
// What we want for each user:
[
'id' => 1,
'displayName' => 'Ada Lovelace',
'isAdmin' => true,
'active' => true,
'country' => 'GB',
]The imperative way to do this is a foreach with nested ifs and string concatenation. The compositional way is to describe the output shape declaratively — each field as a small pure function over the input — and let recordEncoder wire the pieces together.
encodePropertyEach field in the output record is one encodeProperty call. It pairs the output key with a callable that takes the source record and returns the value for that key.
The simplest case: id is already in the source, just copied across. getProperty('id') is the callable that reads it.
use PinkCrab\FunctionConstructors\GeneralFunctions as F;
$encodeId = F\encodeProperty('id', F\getProperty('id'));On its own, encodeProperty does nothing useful — it just remembers “when you see a record, set the output’s id to whatever getProperty('id') returns on it”. The work happens later, when recordEncoder runs all the steps together.
composedisplayName needs transformation: concatenate first and last, separated by a space, with each word capitalised.
Build the callable by composing smaller pieces:
use PinkCrab\FunctionConstructors\Strings as Str;
// Takes a user record, returns "Ada Lovelace"
$toDisplayName = function (array $user): string {
$first = ucfirst($user['first']);
$last = ucfirst($user['last']);
return "$first $last";
};
$encodeDisplayName = F\encodeProperty('displayName', $toDisplayName);$toDisplayName is a small pure function — given a user record, returns a string. Any callable that takes the record and returns a value fits here.
isAdmin is a plain boolean. propertyEquals('role', 'admin') returns a predicate — exactly the shape encodeProperty needs.
$encodeIsAdmin = F\encodeProperty('isAdmin', F\propertyEquals('role', 'admin'));No boolean casting, no if — the predicate’s bool return value becomes the output field.
ifElseA user is “active” if they’ve signed up at least once — signups > 0. Express that without an inline if:
$isActive = F\ifElse(
fn($user) => $user['signups'] > 0, // condition
F\always(true), // when true
F\always(false) // when false
);
$encodeActive = F\encodeProperty('active', $isActive);ifElse takes three callables and returns a new one that branches. always(true) / always(false) are tiny “return this value no matter what” callables — useful as the branch bodies of ifElse.
pluckPropertycountry lives inside the profile object. pluckProperty walks an arbitrary path through arrays and objects in one go:
$encodeCountry = F\encodeProperty('country', F\pluckProperty('profile', 'country'));pluckProperty('profile', 'country') reads $user['profile'] then ->country — seamlessly, whichever mix of arrays and objects the path involves. Missing path → null.
recordEncoderrecordEncoder([]) says “the output is an array, start empty”. Feed it the five encodeProperty steps and it returns a factory Closure — give that factory a user record and you get the view model out.
$toViewModel = F\recordEncoder([])(
$encodeId,
$encodeDisplayName,
$encodeIsAdmin,
$encodeActive,
$encodeCountry
);Apply it to one user:
print_r($toViewModel($apiUsers[0]));
/*
[
'id' => 1,
'displayName' => 'Ada Lovelace',
'isAdmin' => true,
'active' => true,
'country' => 'GB',
]
*/Or across the whole list — array_map alone does the job because $toViewModel is just another callable:
$viewModels = array_map($toViewModel, $apiUsers);
/*
[
['id' => 1, 'displayName' => 'Ada Lovelace', 'isAdmin' => true, 'active' => true, 'country' => 'GB'],
['id' => 2, 'displayName' => 'Bea Smith', 'isAdmin' => false, 'active' => false, 'country' => 'US'],
]
*/encodeProperty calls reorders the output fields. Adding a new field is one line, inserted wherever you like.$toViewModel is a callable. Drop it into pipe, compose, array_map, or another encoder as a nested producer.Pass an object instance to recordEncoder instead of [] and the encoder writes to its properties (via setProperty):
$toDto = F\recordEncoder(new UserViewModel())(
$encodeId,
$encodeDisplayName,
/* ... */
);Make the encoded value itself use ifElse — no need to branch at the encoder level:
F\encodeProperty(
'greeting',
F\ifElse(
F\propertyEquals('role', 'admin'),
F\always('Welcome, admin'),
fn($u) => "Hi {$u['first']}"
)
)$toViewModel doesn’t care what kind of record it gets, as long as the accessor callables match. A list of objects with the same fields works identically — getProperty / pluckProperty handle both arrays and objects.