PHP 7.0: performance, types, and the features that stuck
PHP 7.0 dropped on December 3rd. A month and a half in, I’ve migrated two projects and the results are hard to ignore.
The headline number is 2x faster than PHP 5.6. That’s not a benchmark cherry-pick — it’s the median across real applications. The Zend Engine was rewritten to use a new internal value representation that cuts memory usage significantly and reduces allocations. On one project, average response time dropped by 40% with zero code changes. You just upgrade and it goes faster.
But performance isn’t the most interesting part.
Types, finally
PHP has had type hints for objects since 5.0, for arrays since 5.1. In 7.0, you can finally declare scalar types for function parameters and return values:
function add(int $a, int $b): int {
return $a + $b;
}
In strict mode (declare(strict_types=1)), passing a float to that function throws a TypeError. In the default coercive mode, PHP converts the value. That distinction matters: strict mode is per-file, so you can adopt it gradually without nuking your whole codebase at once.
Return type declarations are the other half. Putting intent in the signature rather than a docblock means the engine enforces it, not a code reviewer who might be half-asleep.
The null coalescing operator
?? is small but used constantly:
$username = $_GET['user'] ?? 'guest';
That replaces isset($_GET['user']) ? $_GET['user'] : 'guest'. It chains too: $a ?? $b ?? $c. After years of isset() noise, this alone was worth upgrading.
The breaking part
The error handling overhaul is the real upgrade risk. Many fatal errors are now Error exceptions, catchable but different from Exception. Code that relied on fatal errors to halt execution silently now needs explicit handling. Legacy error suppression with @ also works differently in places.
Read the migration guide before touching a production app. The payoff is real, but the gap between 5.6 and 7.0 is the widest PHP has ever had.
The spaceship operator
<=> is a combined comparison operator that returns -1, 0, or 1. It’s mostly there for sorting:
usort($users, function ($a, $b) {
return $a->age <=> $b->age;
});
Before this, a custom sort comparator was a small exercise in remembered arithmetic. $a - $b works for integers but silently breaks for floats. <=> does the right thing for every comparable type.
Anonymous classes
You can now instantiate a class defined inline, on the spot, without giving it a name:
$logger = new class($config) implements LoggerInterface {
public function __construct(private array $config) {}
public function log(string $message): void {
file_put_contents($this->config['path'], $message . PHP_EOL, FILE_APPEND);
}
};
The canonical use case is test doubles and one-off interface implementations that don’t deserve a file. It removes a real friction point: the gap between “I need an object” and “I have to create a class file for a 10-line thing”.
Cryptographically secure randomness
PHP 5’s rand() and mt_rand() were never meant for security. 7.0 adds two functions that are:
$token = bin2hex(random_bytes(32)); // 64-character hex token
$pin = random_int(100000, 999999);
random_bytes() pulls from the OS CSPRNG. random_int() wraps that for integers. These replace every home-grown token generation scheme that was quietly doing it wrong, which is most of them.
Group use declarations
Before 7.0, importing five things from the same namespace meant five use statements. Now:
use App\Model\{User, Order, Product};
use function App\Helpers\{formatDate, slugify};
use const App\Config\{MAX_RETRIES, TIMEOUT};
Small ergonomic improvement, but it reduces the visual noise at the top of files with deep namespace hierarchies.
Generators grew up
Generators in 5.5 were interesting but incomplete. 7.0 adds two things. First, a generator can now have a return value, accessible after iteration ends:
function process(): Generator {
yield 'step 1';
yield 'step 2';
return 'done';
}
$gen = process();
foreach ($gen as $step) { /* ... */ }
echo $gen->getReturn(); // "done"
Second, yield from delegates to another generator or iterable, transparently passing values and return values through:
function inner(): Generator {
yield 1;
yield 2;
return 'inner done';
}
function outer(): Generator {
$result = yield from inner();
echo $result; // "inner done"
yield 3;
}
This makes composing generators practical without manually plumbing values between them.
Closure::call()
A more direct way to bind a closure to an object and call it immediately:
class Counter {
private int $count = 0;
}
$increment = function (int $by): void {
$this->count += $by;
};
$increment->call(new Counter(), 5);
bindTo() existed before but required two steps. call() collapses them and is faster at runtime because it skips the intermediate closure creation.
Unicode escape syntax in strings
You can now embed Unicode characters directly in double-quoted strings or heredocs using a codepoint:
echo "\u{1F418}"; // 🐘
echo "\u{00E9}"; // é
Beats copy-pasting characters from a Unicode table into source files, which is what people were actually doing.
Safer unserialize()
unserialize() has a long history of being a vector for object injection attacks. 7.0 adds an allowed_classes option:
$data = unserialize($input, ['allowed_classes' => false]);
$data = unserialize($input, ['allowed_classes' => [User::class, Order::class]]);
Passing false prevents any object from being instantiated during deserialization. This is the default you want when deserializing untrusted input.
Integer division
intdiv() is explicit integer division with no float intermediate:
$pages = intdiv(count($items), $perPage); // int, no casting needed
Yes, you could cast the result of a division. intdiv() makes the intent clear and avoids the float precision edge cases that casting introduces for large numbers.
Constants as arrays
Before 7.0, define() only accepted scalar values. Arrays worked with const at class or namespace scope but not with define(). Now they do:
define('HTTP_METHODS', ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']);
Useful for configuration that needs to be a constant but lives outside a class.
Assertions with teeth
assert() got a proper redesign. In PHP 5, assertions were a runtime eval of strings. Now they can throw exceptions and be completely removed in production with zero overhead:
// In php.ini or at bootstrap:
// assert.active = 1 (dev), 0 (prod)
// assert.exception = 1
assert($user->isVerified(), new \LogicException('Unverified user reached checkout'));
When assert.active = 0, the expression is never evaluated. When it’s on, a failing assertion throws the provided exception directly. This is finally a tool worth reaching for, without the embarrassment of admitting you used it.
The session_start() overhaul
session_start() now accepts an array of options that override php.ini directives for that call:
session_start([
'cookie_lifetime' => 86400,
'cookie_secure' => true,
'cookie_httponly' => true,
'cookie_samesite' => 'Lax',
]);
Before this, you either set options globally in php.ini or called ini_set() before session_start(). Neither was great when you needed different session configurations in different parts of an app.