elfennol/env-to-js-symfony-bundle
Inject Symfony .env variables into the browser at runtime, via <script type="application/json"> tags read by the @elfennol/env-to-js TypeScript package.
BEFORE build(staging) → app.staging.js ← rebuild required for each environment
build(prod) → app.prod.js
AFTER build() → app.js ← one artifact, promoted as-is
On each request, Symfony adds to the HTML:
<script type="application/json" id="rc-app">{"apiBaseUrl":"https://api.example.com"}</script>
In a Symfony + JavaScript application, variables from .env often need to reach the browser (API URLs, third-party keys, feature flags, etc.). The common solution is to bake them in at build time via DefinePlugin (Webpack) or define (Vite).
Consequences:
.env become two separate systems to maintainenv-to-js lets Symfony inject .env variables into the HTML at runtime, as typed <script type="application/json"> tags read by a zero-dependency TypeScript reader.
composer require elfennol/env-to-js-symfony-bundle
npm install @elfennol/env-to-js
{# templates/base.html.twig #}
{% block env_to_js %}
{{ env_to_js_scripts()|raw }}
{% endblock %}
// src/Config/AppConfigProvider.php
namespace App\Config;
use Elfennol\EnvToJs\ConfigProviderInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;
final readonly class AppConfigProvider implements ConfigProviderInterface
{
public function __construct(
#[Autowire(env: 'API_BASE_URL')] private string $apiBaseUrl,
#[Autowire(env: 'CDN_BASE_URL')] private string $cdnBaseUrl,
) {}
public function supports(Request $request): bool { return true; }
public function getScope(): string { return 'app'; }
public function getConfig(): array
{
return [
'apiBaseUrl' => $this->apiBaseUrl,
'cdnBaseUrl' => $this->cdnBaseUrl,
];
}
}
No registration needed — any class implementing ConfigProviderInterface is auto-tagged.
A provider that only runs on specific pages inspects the request:
// src/Config/MapConfigProvider.php
namespace App\Config;
use Elfennol\EnvToJs\ConfigProviderInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;
final readonly class MapConfigProvider implements ConfigProviderInterface
{
public function __construct(
#[Autowire(env: 'MAP_API_KEY')] private string $mapApiKey,
#[Autowire(env: 'MAP_ID')] private string $mapId,
) {}
public function supports(Request $request): bool
{
return in_array($request->attributes->get('_route'), ['app_map_index', 'app_map_show'], true);
}
public function getScope(): string { return 'map'; }
public function getConfig(): array
{
return [
'mapApiKey' => $this->mapApiKey,
'mapId' => $this->mapId,
];
}
}
The <script id="rc-map"> tag is injected only on the matched routes.
# config/packages/env_to_js.yaml
env_to_js:
id_prefix: 'rc-' # default
The injected tag id is {id_prefix}{scope}. Pass the same value as the selector to createConfigReader().
If two active providers return the same scope, their configs are merged with array_merge. Last registered wins on key conflict.
DefinePlugin / process.envconfigureDefinePlugin block from your Webpack config (or define from your Vite config).ConfigProviderInterface providers.process.env.MY_VAR in your source code with getAppConfig('myVar') (the getter returned by createConfigReader).Your webpack/vite artifact is now environment-agnostic — one build, promoted from staging to prod without a rebuild.
window.* or data-options attributesConfigProviderInterface providers for each group of variables currently assigned on window or in data-* attributes.window.myVariable accesses with getAppConfig('myVariable') (the getter returned by createConfigReader).<script> or data-* injection from your templates.make qa-vendor-install # install dependencies
make qa # phpcs + phpstan + phpunit + rector
make help # list all available commands
MIT
How can I help you explore Laravel packages today?