Why a stale DI config cache injects null into Hyvä services
This page explains the root cause behind the Argument must be of type array, null given TypeError that can appear after setup:di:compile. If you just want the fix, see Resolution on the troubleshooting page. This page is here for when you want to understand why it happens and how to keep it from happening in your build pipeline.
The short version
setup:di:compile does not read your etc/di.xml files directly. It reads a cached, pre-merged copy of the global DI configuration (cache id global::DiConfig). That cache entry is not invalidated when the set of installed modules changes. If the cache was warmed before a Hyvä module was registered, the compiler bakes a configuration that is missing that module's arguments, and it records an explicit null for the required array constructor argument. At runtime the compiled object manager hands that null to the Hyvä service, and PHP throws a TypeError.
In other words: the compile "succeeds" against a stale snapshot of your configuration, so a green build is not proof the environment is healthy.
How the config cache gets poisoned during a deploy
The problem is a matter of ordering. When Composer installs a Hyvä module, it only changes files on disk. The module is not registered in app/etc/config.php until setup:upgrade runs. But the very first bin/magento bootstrap during that setup:upgrade merges and caches the DI configuration before the module list is updated, so the cached merge is missing the Hyvä module's di.xml.
A typical production build runs these commands in order:
# installs the Hyvä modules on disk
composer update
# flushes the caches, registers new modules AND
# warms global::DiConfig with the OLD list of active modules
bin/magento setup:upgrade
bin/magento deploy:mode:set production --skip-compilation
# compiles from the stale cached merge
bin/magento setup:di:compile
# fails: null given, once per asset
bin/magento setup:static-content:deploy -j 4
# too late, the compile already happened
bin/magento cache:flush
The cache:flush at the end runs after the compile that already baked in the null, so it does nothing to prevent the failure. This is what makes the issue confusing: the cache flush is present in the pipeline, just in the wrong place.
Why --skip-compilation is commonly used here
A plain bin/magento deploy:mode:set production runs both setup:di:compile and setup:static-content:deploy for you. Builds pass --skip-compilation to skip that, so they can run setup:static-content:deploy themselves with fine-tuned arguments, such as specific themes and locales or a -j job count.
Why the deploy can't recover on its own
Once the compiled metadata contains the null, bin/magento itself hits the same TypeError while building its command list and silently falls back to a minimal set of commands. The cache commands disappear, so a trailing bin/magento cache:flush reports There are no commands defined in the "cache" namespace and no-ops.
The command sequence that prevents it
Flush the config cache immediately before setup:di:compile, so the compiler re-merges the DI configuration from the current etc/di.xml files rather than the stale cache:
composer update
bin/magento setup:upgrade
bin/magento deploy:mode:set production --skip-compilation
# drop the stale global::DiConfig FIRST
bin/magento cache:flush
# now compiles from current di.xml
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -j 4
The single added bin/magento cache:flush before the compile is the whole fix. If your cache backend is Redis and you want to be certain the entry is gone even when the CLI is degraded, flush the config-cache database directly instead:
Treat a degraded CLI as a hard failure
In build scripts, watch for There are no commands defined in any bin/magento output and fail the build on it. That message means the compile is already poisoned, and any later cache:flush in the script is silently doing nothing.
Why it is intermittent
This failure is not tied to a specific Magento version and can appear to come and go. Whether the stale merge survives all the way to setup:di:compile depends on the ordering and timing of the module registration during the build. The same code can build cleanly on one run and fatal on the next, especially when several builds share a host and compete for resources. That timing sensitivity is exactly why front-loading the cache:flush is the reliable fix: it removes the stale entry regardless of how the build was timed.
It also only affects array and scalar constructor arguments. Object arguments are auto-wired by the compiler even when the configuration is missing, so they never receive a bare null.
← Back to Troubleshooting: "Argument must be of type array, null given" TypeError for the symptoms and the step-by-step fix.