Skip to content

Question on coupling of static classes #178

Open
@gregor-j

Description

@gregor-j

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions