DEV Community

Cover image for 5 PHP Features You're Probably Not Using (But Should)
Mahdyar
Mahdyar

Posted on

5 PHP Features You're Probably Not Using (But Should)

5 PHP Features You're Probably Not Using (But Should)

These aren't obscure tricks. They're tools that ship with modern PHP — and once you start using them, you'll wonder how you lived without them.


1. Generators — Process 1 Million Rows with Almost Zero Memory

Here's a scenario every backend developer has faced: you need to read a large CSV file, process it row by row, and do something with the data. The instinct is to load everything into an array first.

That instinct will kill your server.

The traditional approach:

function readCsv(string $path): array
{
    $rows = [];
    $handle = fopen($path, 'r');

    while ($row = fgetcsv($handle)) {
        $rows[] = $row; // everything lands in RAM
    }

    fclose($handle);
    return $rows;
}

// 1 million rows = ~460MB RAM 💀
foreach (readCsv('big.csv') as $row) {
    process($row);
}
Enter fullscreen mode Exit fullscreen mode

With a Generator:

function readCsv(string $path): Generator
{
    $handle = fopen($path, 'r');

    while ($row = fgetcsv($handle)) {
        yield $row; // one row at a time, nothing more
    }

    fclose($handle);
}

// 1 million rows = ~0.3MB RAM ✨
foreach (readCsv('big.csv') as $row) {
    process($row);
}
Enter fullscreen mode Exit fullscreen mode

The only difference is yield instead of $rows[] =. But the impact is dramatic.

Real benchmark — 1 million rows, 42MB file:

Traditional Generator
Time 6.5s 2.67s
Memory 462 MB ~0.3 MB
Result identical identical

The Generator is 2.4× faster and uses virtually no memory. PHP doesn't have to build a massive array in memory before you can start processing — it gives you one row, you process it, it gives you the next.

yield pauses the function, hands a value to the caller, then resumes exactly where it left off when the caller asks for the next value. The file handle stays open between calls — no jumping back to the start.

Beyond CSV: Generators shine anywhere you deal with large sequences — reading database results in chunks, paginating API responses, generating infinite sequences, or streaming log files in real time.


2. Named Arguments — Never Misorder Parameters Again

Quick quiz. What does this code do?

$result = array_slice($items, 1, true, 3);
Enter fullscreen mode Exit fullscreen mode

You probably need to open the docs. Is true the preserve_keys flag? Is 3 the length? What's what?

This is a bug waiting to happen. And it has happened to all of us.

Named Arguments fix this entirely:

// ❌ Before — which true is which?
$result = array_slice($items, 1, 3, true);

// ✅ After — reads like documentation
$result = array_slice(
    array: $items,
    offset: 1,
    length: 3,
    preserve_keys: true
);
Enter fullscreen mode Exit fullscreen mode

But the real power isn't just readability. It's skipping optional parameters in the middle.

PHP has many built-in functions with optional parameters buried in the middle of their signatures. With positional arguments, you have to spell out every parameter until you reach the one you want:

// ❌ Before — had to repeat ENT_QUOTES and 'UTF-8' just to reach double_encode
$safe = htmlspecialchars($input, ENT_QUOTES, 'UTF-8', false);

// ✅ After — jump straight to what you care about
$safe = htmlspecialchars(string: $input, double_encode: false);
Enter fullscreen mode Exit fullscreen mode

Named Arguments + Spread = clean dependency injection:

$config = [
    'table'   => 'orders',
    'limit'   => 20,
    'cache'   => true,
    'orderBy' => 'total',
];

// spread a named array directly into a constructor
$query = new QueryBuilder(...$config);
Enter fullscreen mode Exit fullscreen mode

And before you ask — no, there's no performance cost. In our benchmark of 1 million calls, named arguments were actually marginally faster than positional ones (PHP 8's internal optimizer handles them efficiently).


3. Fibers — The Foundation Under ReactPHP and Swoole

Fibers are the most misunderstood feature on this list. They're not magic async/await. They won't automatically make your code run in parallel. But they are the primitive that makes modern PHP async frameworks possible.

A Fiber is a function that can pause itself and hand control back to the caller — then be resumed later from exactly where it left off.

$fiber = new Fiber(function(): string {
    echo "[Fiber] starting" . PHP_EOL;

    $received = Fiber::suspend('first pause'); // pause + send value out
    echo "[Fiber] resumed, got: $received" . PHP_EOL;

    $received = Fiber::suspend('second pause');
    echo "[Fiber] resumed again, got: $received" . PHP_EOL;

    return 'done';
});

echo "[Main] starting fiber" . PHP_EOL;
$yielded = $fiber->start();                 // runs until first suspend
echo "[Main] fiber paused, sent: $yielded" . PHP_EOL;

$yielded = $fiber->resume('message one');   // resume, get next suspend value
echo "[Main] fiber paused again, sent: $yielded" . PHP_EOL;

$fiber->resume('message two');              // resume to completion
echo "[Main] fiber returned: " . $fiber->getReturn() . PHP_EOL;
Enter fullscreen mode Exit fullscreen mode

Output:

[Main] starting fiber
[Fiber] starting
[Main] fiber paused, sent: first pause
[Fiber] resumed, got: message one
[Main] fiber paused again, sent: second pause
[Fiber] resumed again, got: message two
[Main] fiber returned: done
Enter fullscreen mode Exit fullscreen mode

Notice the two-way communication. This is what separates Fibers from Generators — data flows both in and out. suspend() sends a value to the caller; resume($value) sends a value back into the Fiber.

The key insight: Fibers don't give you parallelism on their own. They give you cooperative multitasking — each Fiber voluntarily yields control at a known point (an I/O wait, a network call, a database query). A scheduler can then run another Fiber while the first one waits.

That scheduler is what ReactPHP, Swoole, and the Revolt event loop provide. PHP 8.1 gave us the primitive; the frameworks built async on top of it.

If you're writing application code, you probably won't create Fibers directly. But understanding them tells you why amphp and ReactPHP work the way they do — and why PHP async is fundamentally different from JavaScript's single-threaded event loop.


4. First-Class Callable Syntax — Functions as Values, Finally Clean

PHP has always let you pass functions as values. But the syntax was painful enough that most people just wrote inline closures instead.

// before PHP 8.1 — verbose wrappers everywhere
$fn = Closure::fromCallable('strlen');
$lengths = array_map(Closure::fromCallable('strtoupper'), $words);

// or inline closures that wrap a single function call
$lengths = array_map(function($w) { return strlen($w); }, $words);
Enter fullscreen mode Exit fullscreen mode

PHP 8.1 introduced a clean syntax: functionName(...) creates a first-class Closure from any callable — built-in functions, static methods, instance methods, all of them.

// ✅ PHP 8.1+
$fn     = strlen(...);
$upper  = strtoupper(...);
$method = $obj->format(...);
$static = Formatter::clean(...);

$lengths = array_map(strlen(...), $words);
Enter fullscreen mode Exit fullscreen mode

This is where it really shines — pipeline patterns:

function pipeline(mixed $value, callable ...$fns): mixed
{
    return array_reduce($fns, fn($carry, $fn) => $fn($carry), $value);
}

$result = pipeline(
    "  Hello, World! This is PHP.  ",
    trim(...),
    strtolower(...),
    fn($s) => str_replace([',', '!', '.'], '', $s),
    fn($s) => explode(' ', $s),
    array_filter(...),
    array_values(...),
);

// ['hello', 'world', 'this', 'is', 'php']
Enter fullscreen mode Exit fullscreen mode

Or validation chains where each rule is just a callable:

function validate(mixed $value, callable ...$rules): array
{
    return array_filter(array_map(fn($rule) => $rule($value), $rules));
}

$errors = validate(
    $password,
    fn($v) => strlen($v) < 8    ? 'Minimum 8 characters' : null,
    fn($v) => !preg_match('/\d/', $v) ? 'Must contain a number' : null,
    fn($v) => !preg_match('/[A-Z]/', $v) ? 'Must contain uppercase' : null,
);
Enter fullscreen mode Exit fullscreen mode

Clean, composable, testable — each rule is independently unit-testable, and adding a new one is a one-liner.


5. The Underrated Built-ins You Keep Reimplementing

PHP's standard library has some genuinely useful functions that don't get enough attention. These aren't new features — they've been around for a while — but they solve problems that developers keep solving manually.

array_is_list() — PHP 8.1+

// before: manual check
function isList(array $arr): bool {
    return $arr === [] || array_keys($arr) === range(0, count($arr) - 1);
}

// now
array_is_list([1, 2, 3]);         // true
array_is_list(['a' => 1, 'b' => 2]); // false
array_is_list([0 => 'a', 1 => 'b']); // true
Enter fullscreen mode Exit fullscreen mode

Essential when you're deciding whether to JSON-encode as [] or {}.

str_contains(), str_starts_with(), str_ends_with() — PHP 8.0+

// before
if (strpos($haystack, $needle) !== false) { ... }

// now
if (str_contains($haystack, $needle)) { ... }
if (str_starts_with($url, 'https://')) { ... }
if (str_ends_with($file, '.php')) { ... }
Enter fullscreen mode Exit fullscreen mode

No more !== false comparisons. No more accidentally treating 0 as falsy.

array_any() and array_all() — PHP 8.5+

// before: manual loops or array_filter tricks
$hasAdmin = count(array_filter($users, fn($u) => $u->role === 'admin')) > 0;

// PHP 8.5
$hasAdmin = array_any($users, fn($u) => $u->role === 'admin');
$allActive = array_all($users, fn($u) => $u->isActive());
Enter fullscreen mode Exit fullscreen mode

Putting It Together

None of these features require a new framework or a major refactor. They're available in PHP 8.0–8.1+, which most production environments already run.

The pattern across all five is the same: less boilerplate, clearer intent, better resource usage. Generators keep memory under control. Named Arguments make signatures self-documenting. Fibers power the async ecosystem. First-class callables make functional composition clean. And the built-ins remove manual reimplementations.

Start with whichever one solves a problem you're already facing. Once it clicks, the rest follow naturally.


All benchmarks were run on PHP 8.3 CLI on Ubuntu. Source code for every example is available on GitHub.`

Top comments (0)