← Back to DevBytes

Functional Programming in PHP

What is Functional Programming?

Functional programming (FP) is a programming paradigm centered around pure functions, immutability, and declarative data transformation. Instead of mutating state and issuing step-by-step commands, FP treats computation as the evaluation of mathematical functions and avoids side effects.

PHP, often known for its object-oriented and procedural styles, fully supports functional programming concepts. Since PHP 5.3 introduced closures and callable types, and with later additions like first-class callable syntax (PHP 8.1), PHP provides a rich toolkit for writing functional code.

In functional PHP, you work with functions as first-class citizens, pass them around, compose them, and build data pipelines using built-in functions like array_map, array_filter, and array_reduce.

Why Functional Programming Matters in PHP

πŸš€ Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Adopting FP principles in PHP leads to code that is:

PHP applications, especially those handling data processing, API responses, or complex business logic, can greatly benefit from a functional approach. It encourages a modular, predictable codebase that plays well with modern frameworks like Laravel and Symfony.

Core Concepts and How to Use Them in PHP

Pure Functions and Immutability

A pure function depends only on its input arguments and produces a return value without modifying any external state. It never touches global variables, static state, or performs I/O. PHP arrays are mutable by default, but you can treat them immutably by returning new copies instead of modifying the original.

<?php
// Pure function: same input, same output, no side effects
function add(int $a, int $b): int
{
    return $a + $b;
}

// Impure function: mutates an external array reference
function impurePush(array &$items, mixed $value): void
{
    $items[] = $value; // modifies the original array
}

// Immutable array transformation using array_map
$numbers = [1, 2, 3, 4];
$doubled = array_map(fn($n) => $n * 2, $numbers);

// $numbers remains [1, 2, 3, 4]
// $doubled is [2, 4, 6, 8] β€” a brand new array

First-Class Functions and Higher-Order Functions

PHP treats functions as first-class citizens. You can assign them to variables, pass them as arguments, and return them from other functions. A higher-order function is one that takes or returns another function.

<?php
// Assign a closure to a variable
$greet = fn(string $name): string => "Hello, $name!";

// Higher-order function: takes a function and applies it to each element
function applyToAll(callable $fn, array $items): array
{
    return array_map($fn, $items);
}

$result = applyToAll(fn($n) => $n * $n, [1, 2, 3]); // [1, 4, 9]

// Built-in higher-order functions
$filtered = array_filter([1, 2, 3, 4], fn($n) => $n > 2); // [3, 4]
$sum = array_reduce([1, 2, 3], fn($carry, $n) => $carry + $n, 0); // 6

Function Composition

Composition combines multiple functions into one pipeline, where the output of one becomes the input of the next. PHP doesn't have a native compose operator, but you can build a helper.

<?php
// Manual composition
$trimUppercase = fn(string $s): string => strtoupper(trim($s));
echo $trimUppercase('  hello  '); // "HELLO"

// Generic compose function (right-to-left)
function compose(callable ...$fns): callable
{
    return fn($x) => array_reduce(
        array_reverse($fns),
        fn($acc, $fn) => $fn($acc),
        $x
    );
}

$double = fn($n) => $n * 2;
$addOne = fn($n) => $n + 1;
$doubleThenAddOne = compose($addOne, $double); // rightmost applied first

echo $doubleThenAddOne(5); // (5 * 2) + 1 = 11

// Real-world pipeline: clean and extract email domains
$extractDomain = fn(string $email): string => 
    explode('@', strtolower(trim($email)))[1] ?? 'invalid';

$emails = [' Alice@Example.COM ', 'Bob@Test.org'];
$domains = array_map($extractDomain, $emails); // ['example.com', 'test.org']

Working with Immutable Data Structures

While PHP lacks a built-in persistent data structure library, you can enforce immutability by never mutating existing arrays or objects. Use array_map, array_filter, array_merge to create new copies. For objects, consider immutable value objects or clone with modifications.

<?php
// Immutable array update: return a new array with replaced value
function arraySet(array $arr, int $index, mixed $value): array
{
    $copy = $arr;
    $copy[$index] = $value;
    return $copy;
}

$original = [10, 20, 30];
$updated = arraySet($original, 1, 99);
// $original unchanged

// Immutable object example (readonly class in PHP 8.2+)
readonly class Money
{
    public function __construct(
        public float $amount,
        public string $currency
    ) {}

    public function add(Money $other): Money
    {
        return new self($this->amount + $other->amount, $this->currency);
    }
}

Recursion and Avoiding Loops

Functional programming often replaces imperative loops with recursion. PHP doesn't optimize tail calls, so deep recursion may cause stack overflow, but for many tasks it works fine and expresses logic clearly.

<?php
// Recursive factorial (pure function)
function factorial(int $n): int
{
    return $n <= 1 ? 1 : $n * factorial($n - 1);
}

echo factorial(5); // 120

// Recursive sum of nested arrays
function deepSum(array $arr): float
{
    return array_reduce($arr, fn($carry, $item) => 
        is_array($item) ? $carry + deepSum($item) : $carry + $item,
        0
    );
}

$data = [1, [2, 3], [4, [5, 6]]];
echo deepSum($data); // 21

Using Closures and Partial Application

Closures capture variables from the surrounding scope via the use keyword. Partial application fixes some arguments of a function, returning a new function that accepts the remaining arguments.

<?php
// Closure with captured value
$multiplier = 3;
$times = fn(int $n): int => $n * $multiplier;
echo $times(5); // 15

// Partial application helper
function partial(callable $fn, ...$presetArgs): callable
{
    return fn(...$args) => $fn(...$presetArgs, ...$args);
}

$add = fn(int $a, int $b): int => $a + $b;
$addFive = partial($add, 5);
echo $addFive(10); // 15

// Currying-like behavior: multi-argument function into chain of single-argument functions
function curry(callable $fn): callable
{
    $reflection = new ReflectionFunction($fn);
    $paramCount = $reflection->getNumberOfParameters();
    
    return function (...$args) use ($fn, $paramCount) {
        if (count($args) >= $paramCount) {
            return $fn(...$args);
        }
        return fn(...$moreArgs) => $fn(...array_merge($args, $moreArgs));
    };
}

$curriedAdd = curry($add);
$addToFive = $curriedAdd(5);
echo $addToFive(3); // 8

Functional Error Handling

Instead of throwing exceptions, functional code often uses result containers like Maybe or Either. You can implement a simple version in PHP to avoid null checks scattered everywhere.

<?php
class Maybe
{
    private mixed $value;
    private bool $isNothing;

    private function __construct(mixed $value, bool $isNothing)
    {
        $this->value = $value;
        $this->isNothing = $isNothing;
    }

    public static function just(mixed $value): self
    {
        return new self($value, false);
    }

    public static function nothing(): self
    {
        return new self(null, true);
    }

    public function map(callable $fn): self
    {
        return $this->isNothing ? $this : self::just($fn($this->value));
    }

    public function getOrDefault(mixed $default): mixed
    {
        return $this->isNothing ? $default : $this->value;
    }
}

// Usage
function findUser(array $users, int $id): Maybe
{
    return isset($users[$id]) ? Maybe::just($users[$id]) : Maybe::nothing();
}

$users = [1 => ['name' => 'Alice']];
$name = findUser($users, 2)
    ->map(fn($u) => $u['name'])
    ->getOrDefault('Guest');

echo $name; // "Guest"

Practical Examples

Let's combine these concepts into a real-world pipeline: processing an order list, filtering high-value orders, applying discounts, and calculating totals.

<?php
$orders = [
    ['id' => 1, 'total' => 120, 'status' => 'paid'],
    ['id' => 2, 'total' => 45, 'status' => 'pending'],
    ['id' => 3, 'total' => 200, 'status' => 'paid'],
    ['id' => 4, 'total' => 10, 'status' => 'paid'],
];

// Pure functions
$isHighValue = fn(array $order): bool => $order['total'] >= 100;
$isPaid = fn(array $order): bool => $order['status'] === 'paid';
$applyDiscount = fn(array $order): array => [
    'id' => $order['id'],
    'total' => $order['total'] * 0.9,
    'status' => $order['status']
];

// Pipeline: filter paid -> filter high-value -> apply discount -> sum totals
$paidOrders = array_filter($orders, $isPaid);
$highValuePaid = array_filter($paidOrders, $isHighValue);
$withDiscount = array_map($applyDiscount, $highValuePaid);
$grandTotal = array_reduce(
    $withDiscount,
    fn($sum, $order) => $sum + $order['total'],
    0
);

echo $grandTotal; // (120*0.9) + (200*0.9) = 108 + 180 = 288

// Same pipeline composed into one function
$pipeline = compose(
    fn(array $orders) => array_reduce($orders, fn($s, $o) => $s + $o['total'], 0),
    fn(array $orders) => array_map($applyDiscount, $orders),
    fn(array $orders) => array_filter($orders, $isHighValue),
    fn(array $orders) => array_filter($orders, $isPaid)
);

echo $pipeline($orders); // 288

Best Practices

Conclusion

Functional programming in PHP is not only possible but also highly practical. By focusing on pure functions, immutability, and higher-order composition, you can write cleaner, more maintainable, and testable code. Start small: replace a few imperative loops with array_map and array_filter, extract pure functions from your classes, and gradually adopt a more declarative style. The result is a PHP codebase that is easier to extend, debug, and reason about β€” a valuable skill in any modern PHP developer’s toolkit.

πŸš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started β€” $23.99/mo
← Back to all articles