When working with legacy PHP code or external dependencies, you might encounter situations where you need to replace a class with your own implementation without modifying the original source. This is common when dealing with vendor code, outdated libraries, or rigid project structures. Let’s explore how to achieve this in PHP 8.0 (without Composer) while minimizing code changes.
Overriding an External Dependency
Consider this scenario:
- An external dependency defines a class
ExternalDependency
that implements an interfaceContract
. - Dozens of services in your project extend
ExternalDependency
, e.g.,class Service extends ExternalDependency {}
. - You want to replace
ExternalDependency
with your own classMyFix
, but you cannot modify the original dependency file.
Here’s the existing code structure:
interface Contract { public static function getFoo(): string; } // ExternalDependency (vendor file you can't edit) class ExternalDependency implements Contract { public static function getFoo(): string { return 'gnarf!'; } } // Your code class Service extends ExternalDependency {}
Your goal: Make Service
extend MyFix
instead, ensuring all calls to getFoo()
return 'Foo'
instead of 'gnarf!'
.
First Attempt: Class Aliasing
A common approach is to use class_alias
to map your custom class to the original class name:
class MyFix implements Contract { public static function getFoo(): string { return 'Foo'; } } // Attempt to alias MyFix to ExternalDependency class_alias(MyFix::class, ExternalDependency::class); class Service extends ExternalDependency {}
Result: A PHP warning:Cannot declare class ExternalDependency because the name is already in use
.
This happens because the original ExternalDependency
is already loaded, and PHP prohibits redefining classes.
Rename the Original Class
To resolve the conflict, you need to rename the original class before aliasing your custom class. This requires the Runkit7 extension, which allows runtime class manipulation.
Install Runkit7
Install the extension via PECL:
pecl install runkit7
Add extension=runkit7.so
to your php.ini
(Linux) or php.ini
(Windows).
Rename and Replace the Class
// 1. Load the original dependency require_once 'path/to/ExternalDependency.php'; // 2. Rename the original class to free up the name runkit7_class_rename('ExternalDependency', 'OriginalExternalDependency'); // 3. Define your custom class class MyFix implements Contract { public static function getFoo(): string { return 'Foo'; } } // 4. Alias MyFix to the original class name class_alias(MyFix::class, 'ExternalDependency'); // 5. Existing services now extend your MyFix class! class Service extends ExternalDependency {}
How It Works:
- The original
ExternalDependency
is renamed toOriginalExternalDependency
. MyFix
is aliased toExternalDependency
, so all references toExternalDependency
(like inService
) now point to your class.
Alternative Approach: Autoloader Override (Without Runkit)
If you can’t install extensions, use a custom autoloader to intercept the class load:
// Register an autoloader early in your code spl_autoload_register(function ($class) { if ($class === 'ExternalDependency') { // Define your custom class instead of loading the original class MyFix implements Contract { public static function getFoo(): string { return 'Foo'; } } class_alias(MyFix::class, 'ExternalDependency'); } }); // Ensure the original ExternalDependency is never loaded // Now, when Service extends ExternalDependency, it uses MyFix
Caveat: This works only if the autoloader runs before the original class is loaded. If the external dependency is included via require
earlier in the code, this method fails.
Practical Enhancements
To make this pattern more robust:
- Add Logging:
Track when your custom class is used:
class MyFix implements Contract {
public static function getFoo(): string {
error_log("MyFix::getFoo() called");
return 'Foo';
}
}
- Fallback to Original Logic:
Use composition to delegate to the original class if needed:
class MyFix implements Contract {
public static function getFoo(): string {
// Delegate to the original class if necessary
return OriginalExternalDependency::getFoo();
}
}
- Integration Checks:
Verify that the replacement works:
Final Thoughts
Replacing classes in PHP without Composer requires creativity. While class_alias
is limited by PHP’s class declaration rules, tools like Runkit7 or strategic autoloader usage can help.
Key Takeaways:
- Runkit7 is powerful for runtime manipulation but requires installation.
- Autoloader Overrides work if you control class-loading order.
- Minimize Risk: Test replacements thoroughly in non-production environments.
This approach is particularly useful for patching legacy code, fixing bugs in dependencies, or testing mock implementations. Use it judiciously, and document your overrides for future maintainers!