Search code examples
phpdesign-patternsreflectiondependency-injectionglobal-variables

Avoiding $GLOBAL state in PHP with dependancy injection?


I have encountered a situation where I would like to avoid using $GLOBAL state, but cannot workout how to do so. I believe Reflection and Dependancy injection could solve this:

Here is a contrived example (Yes I know its a little bent...); Say we have a Calculator class and helper functions that basically replicate its functionality e.g. add, subtract. Now we also want a history of our calculations to be accessible.

How can I use these helper functions without having to manually insert the Calculator as a dependancy?

class Calculator
{
    private $history = [];

    public function add(int $a, int $b): int
    {
        $result = $a + $b;

        $this->history[] = $result;

        return $result;
    }

    public function subtract(int $a, int $b): int
    {
        $result = $a - $b;

        $this->history[] = $result;

        return $result;
    }

    public function history(): array
    {
        return $this->history;
    }
}

function add(int $a, int $b): int
{
    $calculator = new Calculator;
    return $calculator->add($a, $b);
}

function subtract(int $a, int $b): int
{
    $calculator = new Calculator;
    return $calculator->subtract($a, $b);
}

function history(): array
{
    $calculator = new Calculator;
    return $calculator->history(); // Clearly this will be empty
}

See what I mean? Currently calling history() will of course return an empty array...

Of course this would work:

function add(Calculator $calculator, int $a, int $b): int
{
    return $calculator->add($a, $b);
}

function history(Calculator $calculator): array
{
    return $calculator->history();
}

Although if i use this as a package its very error prone, labour intensive to manually wire up these dependancies... let alone every time I call a helper function.

Another method that would work is globals:

$GLOBALS['calculator'] = new Calculator;

function add(int $a, int $b): int
{
    return $GLOBALS['calculator']->add($a, $b);
}

function history(): array
{
    return $GLOBALS['calculator']->history();
}

Although ... yuk yuk yuk. No thank you.

HELP!


Solution

  • This is usually when people turn to an IoC, aka DI Container.

    You could also use the singleton pattern, providing a static accessor for a single instance of your class. You can track history, or state, in the calculator class because there will only be one instance of this class.

    class Calculator
    {
        private static $instance;
    
        public static getInstance(): Calculator
        {
            if (static::$instance === null) {
               static::$instance = new Calculator;
            }
    
            return static::$instance;
        }
    
        ...rest of code...
    }
    
    Calculator::getInstance()->add($x, $y);
    

    The main concern with statics is state and testability, your use of $GLOBALS for global functions doesn't seem too problematic to me as you're essentially using $GLOBALS as a container or service locator.