Magento, DI and env vars

Some time ago, I was integrating an external library into Magento 2. The library required API credentials to be passed into its constructor, so I thought environment variables (env vars) would be a perfect fit. Magento’s Dependency Injection (DI) system supports this—or so I thought. Well, it does in theory… or rather, I’ve used this method before.

The Issue with External Libraries and Environment Variables

To simplify, let’s assume the library requires only one parameter in its constructor (string $token):

php
Copied
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

namespace Vendor\Library;

class FooService
{
    public const ENV_VAR_TOKEN = 'FOO_API_TOKEN';

    public function __construct(string $token)
    {
        // ...
    }
}

Here’s the configuration in di.xml:

xml
Copied
1
2
3
4
5
6
7
<type name="App\Module\FooService">
    <arguments>
        <argument name="token" xsi:type="init_parameter">App\Module\FooService::ENV_VAR_TOKEN</argument>
    </arguments>
</type>

<preference for="Vendor\Library\FooService" type="App\Module\FooService"/>

And finally, the .env file:

plaintext
FOO_API_TOKEN=secret

So, how does this work? Simple: when an argument type in the service is init_parameter, its value must be a constant. Magento then uses this constant’s value as a key to look up the corresponding value in the global $_SERVER array. Straightforward, right?

Except—it doesn’t work.

Why Doesn’t It Work?

The answer was so simple it’s embarrassing to admit I spent over half a day figuring it out.

Magento can’t handle init_parameter if the class isn’t part of a Magento module and instead resides in /vendor. The only exception is if you’re in developer mode (MAGE_MODE=developer). In that case, everything works just fine.

Another exception? When running in production mode, you can re-run these commands: bin/magento setup:upgrade && bin/magento setup:di:compile. And it works.

WTF and Magento: Inseparable Friends

There’s no other way to describe this than as yet another classic “WTF” moment with Magento.

Integrating via a Magento Module

Time to find a solution. After setting dozens of breakpoints in Magento’s vendor code, it became clear that during the initial DI compilation, Magento has no clue that it’s supposed to retrieve data from an environment variable.

The simplest workaround I found (considering the time pressure—I didn’t look for more) was to extend the library’s class within the module handling its integration. The resulting code looks like this:

php
Copied
1
2
3
4
5
6
7
8
<?php

namespace App\Module;

class FooService extends \Vendor\Library\FooService
{
    public const ENV_VAR_TOKEN = 'FOO_API_TOKEN';
}

Of course, the di.xml needed adjustments:

xml
Copied
1
2
3
4
5
6
7
<type name="App\Module\FooService">
    <arguments>
        <argument name="token" xsi:type="init_parameter">App\Module\FooService::ENV_VAR_TOKEN</argument>
    </arguments>
</type>

<preference for="Vendor\Library\FooService" type="App\Module\FooService"/>

I didn’t even bother adding a constructor in the module’s class. Simply extending the library’s class was enough for Magento to inject the required data into the correct places.

Conclusion

Honestly, I’m not sure how to sum this up. If Magento claims to support environment variables in some form, they should work. On the other hand, this is Magento—we can’t expect miracles.

Lesson Learned

If you want to do something while bypassing Magento’s convoluted configurations—don’t. No matter how unintelligible, awkward, or clunky the resulting code may be, it’s better than spending hours dealing with the issue I’ve just described.

Related posts