0

Pure functions and side effects — functional PHP philosophy

Intermediate5 min read·php-06-015
solid

Concept

A pure function has two properties: (1) given the same arguments, it always returns the same result, and (2) it has no side effects (doesn't modify external state, doesn't perform I/O, doesn't depend on mutable global state).

Side effects are any observable changes outside the function: writing to a file, modifying a database, updating $_SESSION or globals, printing output, modifying objects passed by reference, calling date() or rand() (which depend on external state).

Why pure functions matter in PHP:

  • Testable without mocking: A pure function is trivially testable — call it, check the return value. No database setup, no fake filesystem, no clock mocking.
  • Cacheable: Same inputs → same output means the result can be memoized without correctness concerns.
  • Parallelizable: Pure functions have no shared state to coordinate.
  • Composable: Pure functions chain without unexpected coupling.

PHP's functional tools (array_map, array_filter, array_reduce) naturally encourage pure functions because they operate on data and return results without mutation.

Realistic balance: Real applications must have side effects (database writes, emails, logs). The goal is not to eliminate side effects but to push them to the edges — keep business logic pure and side effects in dedicated, isolated places (repositories, service classes, command handlers).

Code Example

php
<?php
declare(strict_types=1);

// Pure functions — deterministic, no side effects
function add(int $a, int $b): int { return $a + $b; }
function double(int $n): int { return $n * 2; }
function slugify(string $title): string
{
    return strtolower(preg_replace('/[^a-z0-9]+/i', '-', $title));
}

// Impure functions — have side effects
function impureDouble(int $n): int
{
    echo "Doubling $n\n"; // side effect: output
    return $n * 2;
}
function getAge(): int
{
    return (int) date('Y') - 1990; // depends on current time — not deterministic
}
function saveUser(array $user): void
{
    // database write — side effect
}

// Pipeline of pure functions
$prices = [10.5, 20.0, 5.25, 99.99];
$result = array_reduce(
    array_filter(
        array_map(fn($p) => round($p * 1.1, 2), $prices), // apply 10% tax
        fn($p) => $p > 10,                                 // filter < 10
    ),
    fn($carry, $p) => $carry + $p,                         // sum
    0
);

// Impure function with pure core
class OrderService
{
    // Pure core — testable without DB
    public function calculateTotal(array $items, float $taxRate): float
    {
        return array_reduce(
            $items,
            fn($total, $item) => $total + ($item['price'] * $item['qty']),
            0.0
        ) * (1 + $taxRate);
    }

    // Impure shell — wraps pure logic with I/O
    public function processOrder(int $orderId): void
    {
        $order = $this->repository->find($orderId);      // I/O
        $total = $this->calculateTotal($order->items, 0.20); // pure
        $order->total = $total;
        $this->repository->save($order);                 // I/O
    }
}