Functional Composition in PHP: Combining Functions with compose()
AI generated
<?php
8.4
PHP · Functional Programming · Closures
Functional Composition in PHP
Combining Functions with compose() Instead of Nesting Them

Functional composition assembles small, individually tested functions into a new function, instead of nesting calls deeply inside one another. With a self-written compose() function, this pattern can be implemented in PHP 8.4 in a type-safe way and without any extra dependency.

18 min read compose · Closures · Function Composition PHP 8.2 · 8.3 · 8.4

1. What functional composition in PHP means

Functional composition refers to assembling two or more functions into a new function, where the result of one function automatically becomes the input value of the next. Expressed mathematically: from f and g a new function h emerges with h(x) = f(g(x)). In PHP this concept has no built-in language syntax, but it can be modeled through a self-written compose function that takes closures as values and returns a new closure.

The practical appeal of functional composition lies in assembling small, individually named and individually testable functions into larger processing steps, without every combination having to exist as its own separately written function. A function that normalizes a string, trims it and converts it to lowercase can be composed from three individual building blocks, instead of being written as its own monolithic function that mixes all three steps into one function body.

This article shows what a self-written compose function looks like in PHP 8.4, how it can be made type-safe with PHPStan templates, and where the line to pipe simulation runs, which is a related but distinct pattern.

2. The nesting problem: why f(g(h(x))) scales poorly

Without functional composition, several successive transformations are often written as nested function calls: trim(strtolower(str_replace(' ', '-', $input))). This code is correct, but reads from the inside out, which contradicts the actual execution order and becomes harder to follow with every additional transformation. With five or six nested calls, it quickly becomes difficult to track which call runs first and which runs last.

A second problem of nesting is the lack of reusability of intermediate steps as a named unit. If the same combination of three transformations is needed in several places in the code, either the entire nested expression must be copied, or an additional helper function is created that again manually recreates the nesting by hand. Functional composition solves exactly this problem by turning the combination itself into a named, reusable value.


<?php

declare(strict_types=1);

// Nested calls: correct, but read inside-out and hard to extend
function slugifyNested(string $input): string
{
    return trim(strtolower(str_replace(' ', '-', $input)));
}

echo slugifyNested('  My Blog Post  '); // my-blog-post

// The same logic, but each step is a separately testable named function
function replaceSpaces(string $s): string
{
    return str_replace(' ', '-', $s);
}

function toLower(string $s): string
{
    return strtolower($s);
}

function trimWhitespace(string $s): string
{
    return trim($s);
}

3. Building your own compose() function

A minimal compose function takes two closures and returns a new closure that first calls the second and then the first function with its result. This order, right to left, matches the mathematical notation f ∘ g and is the classic convention in functional languages. It is important to explicitly declare the return type as Closure, so the composed function itself can again be used as a building block for further functional composition.

The decisive advantage over the nested notation: the composed function now exists as its own named value, which can be assigned to a variable, passed around, or combined again with other functions. The caller no longer needs to know the internal structure of the composition, but treats compose($f, $g) like any other function with a clearly defined signature.


<?php

declare(strict_types=1);

/**
 * Compose two functions right to left: compose(f, g)(x) === f(g(x))
 *
 * @return Closure(string): string
 */
function compose(Closure $f, Closure $g): Closure
{
    return function (string $x) use ($f, $g): string {
        return $f($g($x));
    };
}

$replaceSpaces = fn (string $s): string => str_replace(' ', '-', $s);
$toLower = fn (string $s): string => strtolower($s);
$trimWhitespace = fn (string $s): string => trim($s);

// Composition reads right to left: trim, then lowercase, then replace
$normalizeStep1 = compose($toLower, $replaceSpaces);
$slugify = compose($trimWhitespace, $normalizeStep1);

echo $slugify('  My Blog Post  '); // my-blog-post

// The composed function is itself a reusable, named value
$anotherSlug = $slugify('  Second Article Title  ');
echo $anotherSlug; // second-article-title

4. Composing any number of functions: variadic parameters

A compose function that only accepts two arguments quickly becomes unwieldy with more than two transformation steps, because every additional step requires another nested compose statement. The practical solution is a variadic version that accepts any number of functions and internally folds them into a single closure using array_reduce. This version keeps the right-to-left order but processes any number of functions without manual nesting.

An important detail of the variadic implementation: the order of the fold determines whether the first or the last function passed in runs first. Folding from the left across the argument list with array_reduce produces a composition where the last function in the parameter list is applied first to the input value, which matches the classic mathematical convention f ∘ g ∘ h.


<?php

declare(strict_types=1);

/**
 * Compose any number of functions right to left.
 *
 * @param Closure(mixed): mixed ...$fns
 * @return Closure(mixed): mixed
 */
function composeAll(Closure ...$fns): Closure
{
    return array_reduce(
        $fns,
        function (?Closure $carry, Closure $fn): Closure {
            if ($carry === null) {
                return $fn;
            }

            return fn (mixed $x): mixed => $carry($fn($x));
        },
        null,
    );
}

$double = fn (int $n): int => $n * 2;
$addTen = fn (int $n): int => $n + 10;
$square = fn (int $n): int => $n ** 2;

// Executes right to left: square first, then addTen, then double
$pipeline = composeAll($double, $addTen, $square);

echo $pipeline(3); // double(addTen(square(3))) = double(addTen(9)) = double(19) = 38

5. Securing type safety with PHPStan templates

A generic compose function that works with mixed loses all information for PHPStan about the actual input and output types of the composed function. PHPStan template types via @template fix this: the signature describes that the output type of the inner function must match the input type of the outer function, and PHPStan reports an error as soon as two incompatible functions are composed.

This typing costs nothing at runtime, because PHPDoc templates are purely development-time metadata. The benefit shows up during refactoring: if the signature of one of the involved functions changes, PHPStan immediately flags every place where the functional composition is no longer type compatible, instead of the error only becoming visible at runtime as a wrong return value.


<?php

declare(strict_types=1);

/**
 * Type-safe composition with PHPStan templates.
 *
 * @template TA
 * @template TB
 * @template TC
 * @param Closure(TB): TC $f
 * @param Closure(TA): TB $g
 * @return Closure(TA): TC
 */
function composeTyped(Closure $f, Closure $g): Closure
{
    return fn (mixed $x): mixed => $f($g($x));
}

/** @var Closure(string): int $stringLength */
$stringLength = strlen(...);

/** @var Closure(int): bool $isEven */
$isEven = fn (int $n): bool => $n % 2 === 0;

// PHPStan infers Closure(string): bool for the composed function
$hasEvenLength = composeTyped($isEven, $stringLength);

var_dump($hasEvenLength('test')); // true, "test" has 4 characters

6. Point-free style: defining functions without named arguments

Point-free style means writing functions without explicitly naming their arguments, by defining the function purely through composition of existing functions. Instead of writing fn (string $s): string => trim(strtolower($s)), where $s only serves as a pass-through variable, the function is defined directly as compose($trimWhitespace, $toLower), without a single named parameter in one's own code.

The advantage of this style lies in its compactness and in the fact that the function remains readable as a pure description of its components: what happens is expressed through the names of the composed building blocks, not through an explicit parameter variable. The downside shows up with more complex transformations: point-free code can quickly become hard to read when too many layers of composition are nested inside one another without any intermediate step getting an explanatory name. In PHP teams, a moderate use of point-free style is usually the most pragmatic choice.

7. Real-world example: validation chains from small predicates

A concrete use case for functional composition is validation rules, where several independent checks need to be combined into an overall rule. Instead of writing a single, long validation function with many if branches, small predicates are defined, each responsible for exactly one rule, and composed into a combined check that only returns true once all individual rules are satisfied.

This pattern differs from pure mathematical composition f(g(x)), because here the result of one function doesn't become the input of the next, but several predicates over the same input value are combined with a logical AND. Still, it is the same underlying idea: small, individually named and tested building blocks are assembled into a larger, reusable unit, without the overall rule existing as a monolithic block of code.


<?php

declare(strict_types=1);

/**
 * Combine any number of predicates into a single predicate (logical AND).
 *
 * @param Closure(string): bool ...$predicates
 * @return Closure(string): bool
 */
function allOf(Closure ...$predicates): Closure
{
    return function (string $value) use ($predicates): bool {
        foreach ($predicates as $predicate) {
            if (!$predicate($value)) {
                return false;
            }
        }

        return true;
    };
}

$hasMinLength = fn (string $s): bool => strlen($s) >= 8;
$hasDigit = fn (string $s): bool => (bool) preg_match('/\d/', $s);
$hasUppercase = fn (string $s): bool => (bool) preg_match('/[A-Z]/', $s);

$isStrongPassword = allOf($hasMinLength, $hasDigit, $hasUppercase);

var_dump($isStrongPassword('weak'));        // false
var_dump($isStrongPassword('Str0ngPass'));  // true

8. Drawing the line to pipe simulation and the decorator pattern

Functional composition is often confused with pipe simulation, because both patterns chain multiple functions together. The difference lies in reading direction and the moment of binding: compose(f, g) fixes the order into a new, reusable function and reads right to left, while a pipe simulation typically sends a concrete value directly left to right through a chain of functions, without necessarily producing a new, standalone function.

There is also a line to draw against the decorator pattern: a decorator such as withRetry or withTiming alters the behavior of a function by running additional code before and after the actual call, while functional composition turns the output value of one function directly into the input of the next, without introducing additional behavior around the call. Both patterns can be combined, but are conceptually motivated differently.

9. Composition in direct comparison

Depending on the situation, nesting, functional composition, or a pipe-like structure is the better choice. The following table classifies typical scenarios.

Scenario Nested Functional Composition Recommendation
Two to three transformation steps f(g(x)) compose(f, g) Both acceptable, composition is more named
Combination reused multiple times Code duplication on every call Composed once, named variable Clearly prefer composition
Combining validation rules One large if block allOf($p1, $p2, $p3) Composition, clearly extensible
Very many, frequently changing steps Unreadable beyond 4 levels Point-free hard to read without names Pipe simulation with intermediate values
Additional behavior around a call Not directly expressible Not the right pattern Decorator pattern (withRetry)

The rule of thumb: functional composition is particularly well suited for short, clearly named chains of two to four functions meant to exist as a standalone, reusable value. For very long chains or frequently changing intermediate steps, a pipe simulation with readable intermediate values is often clearer than deeply nested composition.

Mironsoft

PHP architecture, code reviews and functional patterns in everyday team work

Nested code that has become hard to read?

We review existing PHP code for deeply nested calls and show where functional composition assembles small, testable building blocks into clear, reusable functions.

Code Review

Analysis for deeply nested calls and missing reusability

Refactoring

Replacing nested transformations with type-safe composition

Training

Introducing and documenting functional composition hands on within the team

10. Summary

Functional composition assembles small, individually named functions into a new, reusable function, instead of nesting them deeply inside one another. A self-written compose() function that takes two closures and returns a new closure can be implemented in PHP 8.4 without any extra dependency, and extended to any number of functions with a variadic version. PHPStan templates make this composition type-safe without costing a single extra check at runtime.

It remains important to distinguish related patterns: pipe simulation sends a concrete value left to right through a chain, while the decorator pattern wraps additional behavior around a call instead of chaining output to input. Anyone who understands these differences chooses the fitting functional pattern for each situation, instead of applying a single pattern to every problem.

Functional Composition in PHP — The Key Takeaways

Definition

compose(f, g)(x) === f(g(x)), two functions produce one new, reusable function.

Variadic Version

array_reduce folds any number of closures into a single composition.

Type Safety

PHPStan templates verify that the output type of the inner and the input type of the outer function match.

Distinctions

Differs from pipe simulation (reading direction) and the decorator pattern (additional behavior).

11. FAQ: Functional Composition in PHP

1What is functional composition?
Assembling two functions into a new function, expressed as compose(f, g)(x) = f(g(x)).
2Built-in compose function?
No, PHP has no native compose function, but it is easy to write yourself in a few lines.
3What order does compose() run in?
Right to left, like f ∘ g. compose(f, g)(x) runs g first and passes the result to f.
4Composing more than two functions?
Use a variadic version that folds any number of closures into one via array_reduce.
5How to make it type-safe?
With PHPStan @template types ensuring output and input types of the composed functions match.
6What is point-free style?
A function defined only through composition of existing functions, with no named parameters of its own.
7Difference to pipe simulation?
Composition reads right to left and fixes order into a new function, pipe simulation sends values left to right.
8Difference to the decorator pattern?
Decorator runs code before and after a call, composition chains output directly into the next function's input.
9Combining validation rules?
With an allOf() function taking several predicates, returning true only if all rules are satisfied.
10When does it get hard to read?
Beyond four or five nested levels without explanatory intermediate values, pipe simulation is usually clearer.