Starting with the premise that you want to implement a unified replacement of disparate classes consumed by existing code, without modifying the existing consumers, then I have a... "solution".
Here's an example of the current problem:
class A
{
public function test()
{
echo "A\n";
}
}
class B
{
public function test()
{
echo "B\n";
}
}
class Consumer
{
public function runTestA(A $a)
{
$a->test();
}
public function runTestB(B $b)
{
$b->test();
}
}
$con = new Consumer();
$a = new A();
$b = new B();
$con->runTestA($a);
$con->runTestB($b);
You're trying to find a solution that will allow something like, without modifying anything in Consumer:
$con = new Consumer();
$c = new C();
$con->runTestA($c);
$con->runTestB($c);
I'm going to heavily advise against doing what I'm about to outline. It would be better to modify the method signatures in Consumer to allow a new class to be passed that has the joint functionality. But, I'm going to answer the question as asked...
To start with, we need a couple of classes which can pass any existing method signatures. I'll use a trait to define the joint functionality.
trait ExtensionTrait
{
public function test()
{
echo "New Functionality\n";
}
}
class ExtendedA extends A
{
use ExtensionTrait;
}
class ExtendedB extends B
{
use ExtensionTrait;
}
Now we have some classes with the new functionality, which can pass the method checks... if we pass the right one. So, how do we do that?
Let's first put together a quick utility class that allows easy switching between the two classes.
class ModeSwitcher
{
private $a;
private $b;
public $mode;
public function __construct($a, $b)
{
$this->a = $a;
$this->b = $b;
$this->mode = $this->a;
}
public function switchMode()
{
if ($this->mode instanceof ExtendedA)
{
$this->mode = $this->b;
}
elseif ($this->mode instanceof ExtendedB)
{
$this->mode = $this->a;
}
}
public function __set($name, $value)
{
$this->a->$name = $value;
$this->b->$name = $value;
}
public function __isset($name)
{
return isset($this->mode->$name);
}
public function __unset($name)
{
unset($this->a->$name);
unset($this->b->$name);
}
public function __call($meth, $args)
{
return call_user_func_array([$this->mode, $meth], $args);
}
}
This mode switcher class maintains a current mode class, which passes through gets and calls. Sets and unsets are applied to both classes, so any properties modified aren't lost upon a mode switch.
Now, if we can modify the consumer of the consumer, we can put together a translation layer that automatically switches between modes to find the correct mode.
class ConsumerTranslator
{
private $consumer;
public function __construct(Consumer $consumer)
{
$this->consumer = $consumer;
}
public function __get($name)
{
return $this->consumer->$name;
}
public function __set($name, $value)
{
$this->consumer->$name = $value;
}
public function __isset($name)
{
return isset($this->consumer->$name);
}
public function __unset($name)
{
unset($this->consumer->$name);
}
public function __call($methName, $arguments)
{
try
{
$tempArgs = $arguments;
foreach ($tempArgs as $i => $arg)
{
if ($arg instanceof ModeSwitcher)
{
$tempArgs[$i] = $arg->mode;
}
}
return call_user_func_array([$this->consumer, $methName], $tempArgs);
}
catch (\TypeError $e)
{
$tempArgs = $arguments;
foreach ($tempArgs as $i => $arg)
{
if ($arg instanceof ModeSwitcher)
{
$arg->switchMode();
$tempArgs[$i] = $arg->mode;
}
}
return call_user_func_array([$this->consumer, $methName], $tempArgs);
}
}
}
Then, we can use the combined functionality like so:
$con = new Consumer();
$t = new ConsumerTranslator($con);
$a = new ExtendedA();
$b = new ExtendedB();
$m = new ModeSwitcher($a, $b);
$t->runTestA($m);
$t->runTestB($m);
This allows you to interchangeably utilize either class tree without any modification of Consumer whatsoever, nor any major changes to the usage profile of Consumer, as the Translator is basically a passthrough wrapper.
It works by catching the TypeError thrown by a signature mismatch, switching to the paired class, and trying again.
This is... not recommended to actually implement. The constraints declared provided an interesting puzzle, though, so, here we are.
TL;DR: Don't bother with any of this mess, just modify the consuming contract and use a joint interface, like you were intending.