PHP global functions: how they affect code extensibility, testability and modularity
Tags: php, testing, oop, functional
Published: 2021-09-29 21:36:23


We all have been told not to use **global variables**, as they can be modified and read from whatever part of code, which causes confusion and errors. Using **global functions** by contrast is quite common, but can be problematic too.

We will discuss issues of global function calls on a simple `Notification` class, which calls global function `logMessage()` internally.
```php
<?php
class Notification {
  private $isRead = false;

  public function markAsRead() {
    $this->isRead = true;
    logMessage('article was marked as read');
  }
}
```

## Issue 1: Extensibility

Function `logMessage()` probably logs given message into a file. The problem is that the call is hardcoded into the function `markAsRead()` and cannot be changed externally, which violates the [**Open-closed principle**](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle).

Common solution is to use an **object** (not function) for the purpose of logging and pass it to the `Notification` class constructor. This technique is called [**Dependency-injection**](https://en.wikipedia.org/wiki/Dependency_injection).
```php
<?php
class Notification {
  private $isRead = false;
  private $logger;

  public function __construct($logger) {
    $this->logger = $logger;
  }

  public function markAsRead() {
    $this->isRead = true;
    $this->logger->logMessage('article was marked as read');
  }
}
```

This way is the logging logic separated into a distinct object `$logger`, which is injected into the `Notification` class externally. It means we can affect the logging logic from the outside (e.g. we can use logger object which logs messages to a file or use another one which logs to database etc.)

(Alternatively it is possible to use [**Factory method pattern**](https://en.wikipedia.org/wiki/Factory_method_pattern). In that case, we would pass **factory object** (not the actual logger object) to the `Notification` class constructor and the logger object would be created by `Notification` class itself calling a factory object method.)

## Issue 2: Testability
When writing unit tests, we often need to **change behaviour of the tested class slightly** (e.g. we want to turn off logging completely during tests instead of usual logging into a file).

Again, with global function it is impossible to change logging behaviour from the outside of the class. By contrast, the above mentioned solution with **dependency injection** provides possibility to pass custom logger object which will silently ignore all log messages, which is the desired behaviour.

## Issue 3: Modularity
Modularity is closely related to testability. We need to test separate code parts which ideally do *not* interact with each other (see also [**Single-responsibility principle**](https://en.wikipedia.org/wiki/Single-responsibility_principle). This way only are we able to find errors in the tested module itself without mixing them with errors from other interacting modules.

Solution with **dependency injection** described above gives us opportunity to choose custom logger object. With the help of testing framework such as [PHPUnit](https://phpunit.de/) we can create **stub** object instead of regular logger object, which allows us to freely redefine or suppress object functionality for purposes of the test and also watch which methods on the logger object has been called from inside of tested module (**mocking**). Neither of these features can be achieved when implementing logging with a global function.

Last but not least, modularity gives you knowledge about what other modules your current module uses (E.g. with dependency injection, you can just look on class constructor's parameters. They represent injected objects and this objects in turn represent injected functionality from other modules.) With this knowledge of module dependencies you can more easily extract your code and use it in another project, as you know which other modules it depends on and which of them you therefore have to extract as well.

There is one alternative solution of modularity using **namespaced global functions**.
```php
<?php
use \utils\logging\filesystem\logMessage;

class Notification {
  private $isRead = false;

  public function markAsRead() {
    $this->isRead = true;
    logMessage('article was marked as read');
  }
}
```
Enclosing functions in namespaces gives you some modularity per se. Moreover, each called namespaced function is usually mentioned with `use` operator statement in the beginning of the file. Thus you can more easily overview which functions from other modules are called from inside your class. You can also change the namespace (for example to `\utils\logging\database\logMessage`) and thus change logging behaviour as well, but this change *cannot* be made on runtime, because doing the change requires you to modify the source code. (In contrast, with dependency injection the change *can* be done on runtime, because the changed behaviour is represented by runtime injected object.)

## Conclusion
Given the limitations of global function calls, **you should generally avoid them when possible** (at least if you work on OOP project) or use them only in several cases when they become handy, e.g.:
* very **frequently used** functions (calling global functions is much shorter in code than instancing a class and calling a class method)
* functions with **unimportant** functionality (debugging functions, shorthand functions which probably only call some object method)
* very **widely used** functions (used almost in every project module)
* functions whose logic is *not* meant to be modified or mocked (e.g. during unit tests, see above)
* **platform functions** - built-in PHP functionality is mostly implemented as global functions (`array_push()`, `strlen()` etc.) - we usually don't use objects and dependency injection for basic built-in functionality like string or array manipulation

See also [Laravel Miscellaneous functions list](https://laravel.com/docs/8.x/helpers#miscellaneous-method-list) as an example of what type of functions the framework defines globally (as opposed to object methods which make up the majority in Laravel).