The project I'm working on requires me to have some objects, including an event manager, (read-only) configuration manager and plugin manager, that are available everywhere in the system.
I was using global variables for these, until someone (with a C++ background) kindly pointed out that "You're probably doing something wrong if you need global variables".
He suggested using a state object that's passed to all functions that need it.
So I did:
$state = new State();
$state->register('eventManager' , new EventManager());
$state->register('configManager', new ConfigManager());
$state->register('cacheManager' , new CacheManager());
$state->register('pluginManager', new PluginManager());
$state->get('pluginManager')->initialize($state);
While I can see the benefit of this method in more stateful languages, it seems kind of pointless to me in a (mostly?) stateless language like PHP, where the state is lost after the page is done loading.
Is there any benefit to passing a state object around in a (mostly) stateless language like PHP, does it hold any benefits over other approaches (i.e. a globals-based system) and are there better ways to handle this?
The registry that you propose is still a global variable. And if you want to access a global variable (even if it is an object, although a global one), you are doing something wrong.
A proper application only has on phase where global state plays a role: When bootstrapping it. The request starting the script is global, any request data sent with it is global, and any configuration that affects the application and is stored in a file or some other appropriate storage is global.
The first phase should initialize some dependency injection that puts all the parts that make up the application together. That object graph would be created on demand when the processing of the request has decided which part of the code should be called to respond to the request.
Usually this decision is done inside a framework processing the request, and the dependency injection likely will be also done via a framework. Your own code would only accept either the values needed to operate, or the other objects that are needed.
For example, if your code would need a database, then you'd configure the database object to accept the URL and credentials for your database, and then you'd configure your reader object to accept that database object.
It would be the task of the dependency injection to either create only one single database object, or multiple of them. You don't have to use the outdated "singleton antipattern" because it has many drawbacks.
So in this scenario, there are some objects existing in the dependency injection part that are only created once and injected when needed. These objects do not enforce to be only created once, and they are not stored inside a globally accessible variable. However, something has to live in a global variable, but this is only the main framework object and probably the dependency injection container, and they are never shared into the remaining code as a global variable - so this is not harmful at all.