DEV Community

Martin Kordas
Martin Kordas

Posted on • Updated on

PHP global functions: how they affect code extensibility, testability and modularity

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
class Notification {
  private $isRead = false;

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

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.

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.

<?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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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. 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. 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 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
use \utils\logging\filesystem\logMessage;

class Notification {
  private $isRead = false;

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

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 as an example of what type of functions the framework defines globally (as opposed to object methods which make up the majority in Laravel).

Top comments (0)