headzoo/polymer-bundle
A Symfony bundle for integrating the Polymer PHP framework into Symfony applications. It handles bundle registration and service/configuration wiring, making Polymer’s component-oriented features easier to use alongside standard Symfony conventions.
Install and register
composer require headzoo/polymer-bundle
This bundle targets the Polymer 1.x / HTML Imports era, so check its composer.json constraints first — it will likely pull in Symfony 2.7/3.x. On a modern Symfony app you may need --ignore-platform-reqs and manual bundle registration, or you should treat it as a reference implementation rather than a dependency.
Register it the way your Symfony version expects:
// Symfony 2/3: app/AppKernel.php
$bundles = [ /* ... */ new Headzoo\PolymerBundle\PolymerBundle() ];
// Symfony 4+: config/bundles.php
return [ Headzoo\PolymerBundle\PolymerBundle::class => ['all' => true] ];
Where to look first
DependencyInjection/Configuration.php — this is the authoritative list of config keys. Don't guess at YAML; read this file, or run bin/console config:dump-reference polymer if the bundle wires up a reference dump.Twig/ — the extension class tells you the exact function/tag names and their signatures. Everything day-to-day flows through this.Resources/config/ — service definitions, to see whether the Twig extension is public and what it depends on (paths, asset packages, environment).Command/ — check for a Vulcanize/build command; that's the production path.Front-end prerequisites
The bundle orchestrates imports; it does not ship Polymer itself. You still need the runtime available under your web root:
bower install polymer --save # or vendor the files manually
and the polyfill loaded before anything else:
<script src="/components/webcomponentsjs/webcomponents-lite.js"></script>
First use case
The canonical first task is: import one component in a Twig template and render its custom element.
{# layout.html.twig #}
<!doctype html>
<html>
<head>
{{ polymer_import('paper-button') }}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
{# page.html.twig #}
{% extends 'layout.html.twig' %}
{% block body %}
<paper-button raised>Click me</paper-button>
{% endblock %}
If polymer_import() isn't the real name, the Twig extension will show you the actual helper — the shape (a Twig callable that emits <link rel="import" href="..."> from a short component name) is the thing to internalize.
Centralize imports in the base layout, override per page
Put the components every page needs in the layout, and let page templates add their own via a Twig block. This keeps the import graph predictable and avoids the "which template imports paper-dialog?" hunt.
{# layout.html.twig #}
<head>
<script src="/components/webcomponentsjs/webcomponents-lite.js"></script>
{{ polymer_import('polymer/polymer') }}
{% block polymer_imports %}{% endblock %}
</head>
Sub-templates then do {% block polymer_imports %}{{ parent() }}{{ polymer_import('paper-dialog') }}{% endblock %}.
Treat component names as logical aliases
Map short names to real paths in bundle config so templates never contain deep relative URLs. When you later switch from Bower's flat layout to a bundled/vulcanized file, only the config changes:
# app/config/config.yml — confirm keys against Configuration.php
polymer:
components_dir: "%kernel.root_dir%/../web/components"
imports:
paper-button: "paper-button/paper-button.html"
paper-dialog: "paper-dialog/paper-dialog.html"
Split dev and prod import strategies
In development you want individual HTML imports (fast edit-refresh, readable network panel). In production you want a single vulcanized file. Drive this from config keyed on %kernel.debug%, and keep the Twig call sites identical so templates don't branch on environment.
# config_prod.yml
polymer:
vulcanize: true
bundle_file: "components/elements.html"
Then the layout emits the bundled import in prod and the individual ones in dev. If the bundle exposes a command (look in Command/), wire it into your deploy script rather than running it manually.
Render custom elements from Twig with explicit attributes
Custom elements are just HTML, so Twig handles them natively — but remember attributes are case-sensitive-ish and Polymer 1 uses hyphenated attribute names for properties. Prefer static markup for element declarations, and use Twig only for data:
<paper-input label="{{ form_label }}" value="{{ current_value }}"></paper-input>
<iron-ajax url="{{ path('api_items') }}" handle-as="json" auto></iron-ajax>
For anything dynamic, render the element with attributes set from Twig and let Polymer's binding take over inside a <dom-module>/<template> — do not try to generate Polymer binding syntax ({{foo}} vs [[foo]]) from Twig, because the delimiters collide.
Integration tip: keep Twig out of component internals
Define your own elements in web/components/my-element/my-element.html as plain files. If you need server data inside a component, pass it in through attributes or fetch it with iron-ajax pointing at a Symfony route. Mixing Twig-rendered HTML into a <dom-module><template> is where people lose days to escaping bugs.
Cache-busting
Since the bundle emits <link rel="import"> URLs, hook them into Symfony's asset versioning or append a build hash to bundle_file. HTML imports are aggressively cached, and a stale import is indistinguishable from a broken component.
Polymer 3+ is not supported, and neither is modern Symfony
HTML Imports were removed from browsers, and Polymer 3 moved to ES modules (import { PolymerElement } from '@polymer/polymer'). This bundle's entire model — <link rel="import"> plus Vulcanize — is obsolete. If the project is greenfield, use @polymer/polymer with a JS bundler (Webpack Encore) instead. This assessment is only useful if you're maintaining a legacy app or mining the bundle for patterns.
Load order is load-bearing
webcomponents-lite.js must execute before any import. Custom element registration is also order-dependent: if <paper-button> appears before its import resolves, you get an unstyled, non-functional element and only a console note about an unknown element. When a component "renders as plain text," check the network tab for a 404 on the import first.
Escaping surprises
Twig autoescaping will mangle attribute values containing quotes, and any inline <script> inside a component template is unaffected by Twig but is affected by CSP. If your app ships a Content-Security-Policy, Polymer 1's inline scripts and style attributes will be blocked — you'll need unsafe-inline or hashes, which is usually the moment teams decide to migrate.
Path resolution is relative to the web root, not the bundle
A frequent failure is components_dir pointing at Resources/ inside the bundle while the browser requests /components/.... The bundle can only emit URLs the browser can reach; anything you want served must exist under web/. Verify with a direct curl against the generated URL.
Vulcanize output must be rebuilt on deploy
If prod config points at a vulcanized file, that file is a build artifact. Add it to .gitignore, generate it in your deploy pipeline, and make sure the command runs after assets are copied and before cache warmup. Forgetting this is the classic "works on my machine, blank page in prod."
Debugging checklist
Failed to load resource: 404 → import path wrong.Unknown element <x-foo> → import order or missing import.dom-module id doesn't match the element name.attribute-name="value" in the initial HTML, then set properties imperatively after WebComponentsReady.Resources/config/services.yml and that the bundle is actually in the kernel/bundles.php).Extension point worth knowing
The Twig extension is the seam. If you need to change how imports are emitted (e.g., add crossorigin, switch to a CDN, emit a single vendor bundle), subclass or decorate that service rather than editing vendor code. Register your replacement with the same service id, and keep the function names identical so templates don't change.
Honest cost/benefit
With 11 stars, no recent activity, and a stack that browsers have removed support for, treat this as a migration aid. The useful, portable idea in it — a config-driven alias map for component imports, with dev/prod bundling switched by environment — is worth reimplementing in ~50 lines of Twig extension plus Encore. That's usually cheaper than keeping the bundle alive.
How can I help you explore Laravel packages today?