dstuecken/php7ify
A compatibility layer that brings PHP 7 features to older PHP 5 codebases, letting legacy projects use modern language behavior without upgrading their runtime. A lightweight bridge for apps still stuck on PHP 5.
php7ify is a compatibility shim, not a framework — it backfills PHP 7's Throwable hierarchy (Error, TypeError, ParseError, AssertionError, ArithmeticError, DivisionByZeroError) and a handful of related classes into PHP 5.x so your code can be written for PHP 7 today and still run on your legacy target.
dist-style polyfill with an autoload files entry, so no manual require is needed):
composer require dstuecken/php7ify
composer.json for the php constraint (typically >=5.4) and make sure every environment in your deploy chain meets it. The polyfill is a no-op on PHP 7+, so a mixed 5.6 / 7.x fleet is the intended scenario.src/ — the files map one-to-one to the PHP 7 class names. That directory is the API surface; there is no facade, service provider, or config.catch (Throwable $e). Before php7ify, Throwable doesn't exist on 5.x and your catch never matches. After it, the same block compiles and behaves the same on both versions.
try {
$service->handle($payload);
} catch (Throwable $e) { // works on 5.6 via php7ify, native on 7+
$this->logger->error($e->getMessage());
}
Guarded class definitions. Because the polyfill conditionally declares classes only when class_exists()/interface_exists() returns false, it is safe to leave installed on PHP 7/8. Standard pattern:
if (!interface_exists('Throwable')) {
// polyfill defines it
}
This means your application bootstrap does not need to branch on PHP_VERSION_ID — just require the package unconditionally.
Forward-compatible exception handling. Write your catch blocks for PHP 7 semantics from the start and let php7ify bridge the gap on 5.x. Ordering still matters — put Error / TypeError catches before a broad Throwable or Exception catch, same as native PHP 7:
catch (TypeError $e) { /* bad argument type */ }
catch (Error $e) { /* engine-level failure */ }
catch (Throwable $e) { /* everything else */ }
Laravel / Symfony integration. Frameworks that already catch \Throwable in their exception handlers (Laravel's Handler::render, Symfony's ErrorHandler) can be pointed at the polyfill to get consistent reporting on 5.x. Drop the dependency in and let the handler's existing Throwable reference resolve.
Library authors: advertise PHP 7 types without requiring PHP 7. If you ship a package that type-hints \Error in a catch or @throws docblock, list dstuecken/php7ify in require (not require-dev) so downstream 5.x projects don't fatal on missing class names at runtime.
Migration workflow. Treat the polyfill as scaffolding on a two-phase upgrade:
Exception-only handling into Throwable/Error-aware handling, run your PHPUnit suite on both 5.6 and 7.x in a CI matrix.Testing. In PHPUnit assertions use @expectedException \Error or expectException(\Error::class) — both resolve identically against the polyfill and native classes, which lets your tests survive the version bump.
??, <=>, scalar/return type declarations, anonymous classes, grouped use, and constant array define() are engine-level in PHP 7. php7ify only adds classes and interfaces. You cannot write function f(): int and expect 5.x to accept it — that's a parse error, not a missing class.ParseError cannot be thrown by the 5.x engine. The class exists so catch (ParseError $e) compiles, and you can throw new ParseError(...) yourself, but a real syntax error in an include/eval still produces a fatal, uncatchable error on 5.x. Don't build recovery logic assuming otherwise.Throwable is an interface; Error implements it, but native Exception does not. The polyfill typically aliases or bridges Exception so catch (Throwable $e) matches both branches — verify this in src/ for your version, because if the bridge is missing, exceptions thrown from PHP 5 core code will slip past a Throwable catch. When in doubt, catch Exception and Throwable, or fall back to \Exception.files autoload step, any code that does class_exists('Error') before vendor/autoload.php runs will get a false negative. Never hand-roll require of app files ahead of the autoloader.class_exists guard, but review it. If a future PHP version introduces a class with the same name and different signature, the guard usually saves you — confirm the guard exists for each class you rely on, and pin the package.src/ if you need fixes, or prefer it strictly as a short-lived migration aid rather than a permanent runtime dependency.get_declared_classes() / get_declared_interfaces() after bootstrap to confirm which polyfilled names actually registered — the guards mean a partial set is possible if something else aliased a name first.if (!class_exists(...)) guards, keeping the polyfill and your additions in one place.How can I help you explore Laravel packages today?