How to Replace a Class with a Custom Class in PHP (Without Composer)

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 interface Contract.
  • Dozens of services in your project extend ExternalDependency, e.g., class Service extends ExternalDependency {}.
  • You want to replace ExternalDependency with your own class MyFix, 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:

  1. The original ExternalDependency is renamed to OriginalExternalDependency.
  2. MyFix is aliased to ExternalDependency, so all references to ExternalDependency (like in Service) 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!

Related blog posts