Description
I recently stumbled across a problem applying the Dependency Inversion Principle (DIP) trying to write classes for communication with either local (fopen()
) or remote (fsockopen()
) serial ports. (In the following example I'll stick to the Tcp
class using fsockopen()
and leave out the interfaces.)
<?php
namespace Gregor\ExampleSrc;
final class Socket implements SocketFunctionsInterface
{
public static function fsockopen($hostname, $port, &$errno, &$errstr, $timeout): ?SocketFunctionsInterface
{
$socket = fsockopen($hostname, $port, $errno, $errstr, $timeout);
if (is_resource($socket)) {
return new self($socket);
}
return null;
}
/**
* @var resource
*/
private $socket;
public function __construct($socket)
{
$this->socket = $socket;
}
public function __destruct()
{
fclose($this->socket);
$this->socket = null;
}
public function fwrite($string, $length)
{
return fwrite($this->socket, $string, $length);
}
public function fgetc()
{
return fgetc($this->socket);
}
}
<?php
namespace Gregor\ExampleSrc;
final class Tcp implements CommunicationInterface
{
/**
* @var SocketFunctionsInterface
*/
private $socket;
/**
* @var SocketFunctionsInterface
*/
private static $socketClass = Socket::class;
public function __construct(string $host, int $port)
{
//... code preparing the class, that needs to be tested
$socketClass = self::$socketClass;
while (($this->socket = $socketClass::fsockopen($host, $port, $errNo, $errMessage, $timeout)) === null) {
//... lots of code, that needs to be tested
}
}
public static function setSocketFunctionsClass(string $className): void
{
$class = new ReflectionClass($className);
if (!$class->implementsInterface(SocketFunctionsInterface::class)) {
throw new InvalidConfigException(sprintf(
'The given class %s doesn\'t implement %s!',
$className,
SocketFunctionsInterface::class
));
}
self::$socketClass = $className;
}
//... code using $this->socket->... methods, that needs to be tested
}
In order to test the code of the Tcp
class, I moved fsockopen()
fgetc()
and frwite()
into a separate class Socket
. Socket
mustn't contain any code, that would need testing. However, in order to create an instance of that Socket
class, fsockopen()
needs to be called and that can't be done outside of the Tcp
class.
My concern is:
The case of injecting a static class before the constructor is called, in this case self::$socketClass
, is not covered by DIP. The solution above doesn't look clean to me.
What can I do?