Skip to content
| Marketplace
Sign in
Visual Studio Code>Programming Languages>Wicker: Symfony & Twig ToolingNew to Visual Studio Code? Get it now.
Wicker: Symfony & Twig Tooling

Wicker: Symfony & Twig Tooling

William Smith Edward

|
5 installs
| (0) | Free
Symfony and Twig navigation, variables, filters, functions and components.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

Wicker: Symfony & Twig Tooling

Wicker aims to be a one-stop shop for Symfony and Twig development in VS Code, with a developer experience that is a joy to use and lowers cognitive load as much as possible. It starts by connecting controllers and templates directly in the editor.

Status

Early, and working. The first capability, the bridge between a controller and the template it renders, is finished and driven from the editor.

In the editor today:

  • Go to definition on every template name, from PHP and from Twig
  • Hover naming the resolved file, the reference that reached it, and the variables the controller passes
  • Completion of indexed template names inside the quotes
  • Twig variable completion and hover from controller keys, local bindings, literal includes and inheritance, updated as PHP or Twig is edited
  • Twig filter and function completion from the project's console discovery, with hover, official reference links for standard names and unknown-name warnings
  • Twig Component name and prop completion, hover and navigation to the registered PHP class, Twig template and prop declarations
  • Diagnostics for a template that resolves to nothing, distinguishing a missing file from an unregistered namespace, and staying silent on a name built at runtime
  • A quick fix that creates the missing file, which declines when the namespace is unregistered and there is nowhere correct to put it
  • Template names that resolve coloured as their own semantic token, so the references the extension actually understands are visible in the code
  • Twig syntax highlighting, with HTML, JavaScript and CSS embedded
  • "Rendered by" links above a Twig template, opening each PHP render call or #[Template] attribute that resolves to it, including unsaved PHP edits, and a count of the templates extending it
  • A Wicker sidebar with a leaf icon, detected projects, namespace status and templates grouped by namespace

Underneath it:

  • Symfony project discovery from conclusive evidence, with the reasons recorded
  • Twig template reference parsing, covering the @Namespace and @!Namespace forms and rejecting the syntax Symfony 5 removed
  • Namespace resolution from bin/console debug:twig --format=json, remembered so a stopped container does not lose it, falling back to config/packages/twig.yaml so it works with no PHP available at all
  • A template index queryable by name and by file, modelling override order
  • Template references found in PHP: render() and its siblings, #[Template], named arguments, and the context keys passed alongside
  • Template references found in Twig: extends, include, embed, use, import, from, and the include() and source() functions
  • composer.json parsing with a PSR-4 map that resolves a class to its file and back again

Pointed at a real Symfony 8.1 application, that resolves every one of its 34 template references, from both sides, with no false positives. The editor features are covered by integration tests that drive a real VS Code instance against a fixture project.

Install Wicker from the Visual Studio Marketplace

Or build it and run it from source with the instructions under Development.

Why it exists

A Symfony request crosses a boundary that editors normally lose track of: a controller names a template as a bare string, passes it an array of variables, and neither side can see the other. Wicker's goal is to close that gap, so that the template name is navigable, the variables the controller passes are known inside the template, and a typo in either direction is an error you see while typing rather than at runtime.

The wider vision connects the Symfony ecosystem through consistent navigation, completion and explanations. Developers should spend less time remembering names, finding related files, switching contexts and configuring tools. Project discovery and automatic updates keep assistance relevant; a calm interface puts useful information at the point of work and deeper detail within reach. Each feature should make a real development task easier from start to finish.

That experience should work for people learning Symfony and those who use it every day. Discoverable actions and explanations should help people get started with a workflow, while familiar, efficient interactions keep it pleasant as their experience grows. Real use and iteration will guide what comes next.

The focus begins once a project is set up. Initial project and environment setup are outside the intended scope; the ambition for everyday development after that is broad. Creating application code, refactoring, running project commands, testing and debugging are possible future directions alongside editor intelligence, evaluated one capability at a time.

No dependencies

The extension ships no third-party runtime code. Its parsers are its own: a Twig lexer, a PHP lexer, and a reader for the subset of YAML that Symfony configuration uses. Nothing is bundled that was not written for it.

That is a deliberate constraint rather than a boast. It keeps the supply chain empty, keeps the bundle small, and means every behaviour is one the project can change. Build tooling is a separate matter and stays conventional.

Architecture

Three packages, so the intelligence is not welded to one editor:

Package Contents
packages/core The engine. Pure TypeScript, no editor or protocol dependencies, fully unit-testable.
packages/server A Language Server Protocol server wrapping the engine. Not yet started.
packages/vscode The VS Code extension, calling the engine directly through native editor providers.

Two conventions in the engine are worth knowing before reading the code.

Paths are project-relative. Everything indexed, compared, or reported is a forward-slashed path relative to the project root. This is what lets a single index stay correct when Symfony sees the project at /app inside a container while the editor sees it on a Windows drive or through a WSL UNC path. It is also why debug:twig reporting its loader paths relative to the project root is so convenient.

The console is a data source, not a dependency. Symfony can describe itself through debug:twig, debug:router, and debug:container. Wicker consumes that where it is available and falls back to static analysis where it is not, so the extension still works when PHP cannot be run.

Development

Requires Node 20.19 or newer.

npm install
npm run check

check runs the whole gate: a build first, since the extension type checks against the engine's emitted declarations, then type checking, linting and tests.

Command Purpose
npm run build Compile every package
npm run typecheck Type check without emitting
npm run lint ESLint across the repo
npm test Run the test suite once
npm run test:watch Run tests in watch mode
npm run test:integration -w wicker Drive the extension in a real VS Code instance

Running it against a project

Build the extension, then open a Symfony project in a window that loads it from this checkout:

npm run build -w wicker
code --extensionDevelopmentPath=/path/to/wicker/packages/vscode /path/to/symfony-app

Namespaces registered by bundles are declared in no configuration file, so Wicker asks bin/console debug:twig for them. When PHP is not on the same machine as the editor, say because the application runs in a container, tell it how to reach the console in the project's .vscode/settings.json:

{
  "wicker.console.command": ["docker", "exec", "my-php-1", "php", "bin/console"]
}

Without that it falls back to config/packages/twig.yaml, which resolves the namespaces an application declares for itself but not the ones its bundles register.

Browsing the sidebar

Click the leaf in the Activity Bar to open Wicker. Each Symfony project shows Controllers and Templates sections. Under Controllers, controllers are grouped by the part of their PHP namespace that tells them apart, so App\Controller\Admin\UserController sits under an Admin row while a project with one namespace is left as a flat list. Set wicker.sidebar.controllerNamespaces to false to group none of them. A controller lists what it produces: each template it renders, then its JSON endpoints, then any other action. A template carries the action that renders it beneath, so a page reads as Twig, then the route that serves it. An endpoint that renders nothing is its own row. Clicking the controller opens its PHP file; clicking a template opens the Twig file; clicking the action selects its render call or #[Template] attribute. Unresolved names stay visible with a warning. A controller is listed when it renders a template or a route names it, so one that only answers JSON has a row too.

Action rows lead with their registered HTTP method and URL, such as GET /dashboard, with index() as secondary detail. When route discovery is unavailable, the method name stays visible. Hover shows all registered routes and rendered templates for the action. Route rows sort by URL path in every section, including under controllers.

This view includes literal template references in classes following the Controller directory, namespace or class-suffix convention. Actions without a known route follow routed actions, retaining source order. Methods without literal template references are omitted, and rendering services outside those conventions remain available through Rendered by links. Unsaved PHP edits update the tree.

A controller's Dependencies branch links project types declared on its constructor, method parameters and properties: services, repositories, entities and other classes or interfaces. Each type appears once; hover lists the parameters and methods that declare it. Imports and aliases resolve to actual project declarations, including current unsaved buffers. Interfaces open their declaration. Container aliases, inherited dependencies, dynamic service lookups, union/intersection types, vendor types and ambiguous declarations are not inferred. Service, Repository and Entity namespaces supply distinct role icons; other dependencies use their PHP declaration kind.

Expand a Twig file to see its associated JavaScript and TypeScript files. Controllers collect these in a Scripts branch, together with scripts that reference their routes. Connections follow registered Stimulus bindings and literal Twig includes, embeds and layouts; hover explains each connection. Dynamic includes, general asset/import-map entrypoints and arbitrary script imports are not followed. Clicking a template still opens Twig, and clicking a script opens that file.

When generated JavaScript explicitly links a local source map with one existing TypeScript source, the tree prefers that source and merges duplicate script entries. Hover retains the generated file path. Independent same-named files, multi-source bundles, missing maps and missing sources keep their own identity. Both external local .map files and inline maps are supported; remote maps are not fetched. This preference changes tree navigation, not Stimulus registration.

Under Templates, expand Application or a named namespace to browse collapsible folders, with folders sorted before files and template counts beside them. Click a filename to open the file Twig resolves to; its tooltip retains the full template name and project path.

The title buttons reveal the current template, rebuild the index, and open Wicker settings. Bundle namespaces such as @Turbo are hidden by default; application namespaces such as @Design and overrides under Application → bundles remain visible. The eye button shows or hides bundle templates and remembers that choice for this workspace. Revealing an open bundle template also shows bundle namespaces. This filter affects browsing under Templates; explicit controller targets, navigation, completion and diagnostics still use the full index.

Hover the project name to see where namespaces came from. Limited namespace discovery or a truncated index shows an expandable warning with Retry and the relevant Settings action. A saved Symfony namespace list stays in the tooltip while the console is unavailable. Right-click a project and choose Show diagnostics to open its detailed report in Output, including how long each console command took. The same command in the Command Palette reports all detected projects.

A project appears as soon as its files are read, drawn with the routes, components and namespaces Symfony reported the last time it was opened. The console is asked after that; on a container or a remote machine it can take a while to boot, and until it answers the tree shows an Asking the Symfony console row and holds back the checks that need a current answer. What the console says then replaces what was remembered. A console that has not answered within a minute is reported as unavailable, with Retry.

Symfony rewrites its cache under var/ every time the console boots, and VS Code's own watcher exclusions do not cover it, so Wicker excludes **/var/** from the file watcher by default. Set it to false in your own files.watcherExclude to watch it again.

The tree updates when template files or configuration change. Turning off wicker.enable replaces the tree with a link to settings so it can be enabled again. In an empty workspace, the sidebar offers to open a Symfony application folder.

The sidebar follows the active editor: opening a template selects its row, as the Explorer selects the active file. wicker.sidebar.autoReveal turns that off. The Reveal button in the view title still finds the current template, and shows bundle templates when it is one of those.

Right-click a row to open its file to the side, reveal it in the Explorer, or copy what it names: a template's logical name, ready for render() or an include tag, a Stimulus controller's identifier, or a route's name or path.

Wicker: Go to Template in the Command Palette opens a template by its logical name, which Quick Open cannot match when the name and the path differ, as they do for a namespace or a bundle override.

Rows are coloured by what they are: green for templates, purple for PHP, yellow for JavaScript, blue for TypeScript, cyan for API routes, orange for Stimulus, magenta for stylesheets and red for components. wicker.sidebar.colors turns the colours off, and only warnings keep theirs.

Navigating back to PHP

Open a Twig template and click a Rendered by Controller::method link above its first line to select the template name in that PHP call or attribute. Each render site has its own link, and a layout also reports how many templates extend it. Both are the direction the file cannot state about itself: what a template extends is on its own first line and already navigates.

wicker.codeLens.enabled turns these links off, and VS Code's own editor.codeLens setting hides every extension's links at once. The same two connections are also rows under a template in the Wicker sidebar, where they can be browsed without opening the file.

Wicker scans project PHP files outside vendor, var, node_modules, and .git. Links follow unsaved edits, file changes, and Twig loader precedence. Only direct, literal render references count: an inherited layout or included partial does not borrow its caller's controllers, and runtime template names are not guessed. Turning off wicker.enable hides the links too.

Variables in Twig

When a PHP call renders a literal template name with an array such as ['tasks' => $tasks, 'heading' => 'Tasks'], Wicker suggests tasks and heading at Twig expression positions. Type inside {{ }} or press Ctrl+Space to see suggestions. Hover a supplied variable to see the controller method, render call and source file. Both features follow unsaved PHP edits.

A template rendered by several calls receives the union of their known keys. Hover reports how many indexed calls explicitly supply a key and identifies those calls. A key supplied by one call may be absent from another; Wicker does not infer its type or promise that it always exists.

Local set variables, loop keys and items, loop, macro parameters and literal with keys are suggested in their scope. Loop bindings disappear outside the loop; macros and with ... only do not borrow their enclosing context. Imported macro aliases hide matching variable names.

Literal include tags and include() calls carry the caller's visible context into the included template. Literal map keys add or replace variables; only and with_context: false restrict suggestions to explicitly passed keys. Hover identifies the PHP or Twig source and the include chain. Several callers produce a union of possible inputs, not a promise that every input is available.

Literal extends relationships carry controller keys and child top-level assignments into layouts. Child blocks can see parent assignments made before the corresponding block. Sibling templates keep their own rendering context. Unsaved PHP and Twig edits, discarded edits and file changes update these suggestions automatically. The Rendered by links still describe direct PHP render sites only.

Inference remains conservative: PHP variable arrays and #[Template] method return values, dynamic template names, candidate lists, dynamic context maps, cross-file embed/use behavior and arrow-function scopes are not inferred. Template text and graph traversal have bounded limits. There are no inferred value types or unknown-variable diagnostics. Turning off wicker.enable disables all variable assistance.

Twig blocks

A block overrides the nearest ancestor's block of the same name, and neither file says so. Go to Definition on a block name, or on parent() inside the block, opens the block it overrides. Find All References lists every definition up and down the inheritance chain, and hover names the ancestor a block overrides and the templates overriding it. After {% block, completion offers the names the ancestors declare and this template has not overridden. A top-level block that no ancestor defines is reported, because Twig renders nothing for it and says nothing; wicker.diagnostics.unknownBlock turns that down or off. Blocks inside {% embed %} are read against the embedded template, {% use %} counts, and a layout named at runtime is left alone.

Twig filters and functions

Type | after a value for filter suggestions, or use Ctrl+Space at a Twig expression position for functions. Wicker reads the names registered in your project from debug:twig, including custom extensions and installed bundles. Hover a filter or function to see its registration, arguments reported by Symfony, and an official reference link for recognized standard names. Symfony's argument output can include implicit PHP parameters, so Wicker does not present it as a Twig signature or insert argument placeholders.

Unknown filters and functions receive warnings when discovery is fresh and complete. Wildcard registrations are recognized; methods, imported macros, Twig tests, strings, comments and verbatim content are excluded. This is a conservative reader of common Twig expressions and tags, not a full syntax checker; custom tag syntax and string interpolation are not analyzed.

Saved application PHP, configuration, Composer and supported environment-file changes refresh discovery automatically. Unsaved project changes suspend unknown-name warnings until saved or discarded. If the console becomes unavailable, these warnings and callable suggestions are cleared; template namespace fallback continues to work. Runtime-only registrations that are not listed by debug:twig cannot be discovered.

Set wicker.diagnostics.unknownCallable to error, warning (the default), information or off. wicker.enable disables all of these surfaces, and wicker.console.enabled controls console discovery. Show diagnostics from a project's sidebar context menu reports the discovered callable counts.

Twig Components

In a project using Symfony UX Twig Components, Wicker discovers registrations with debug:twig-component. Complete names inside <twig:...>, closing tags, component('...') and {% component '...' %}. Hover explains the registration; Go to Definition offers its PHP class and Twig template when available locally.

Inside an opening component tag, Ctrl+Space suggests props from writable public properties, setters and mount() parameters declared on the registered class. Anonymous components use their template's {% props %} declarations. A suggested prop inserts name="" with the cursor inside the quotes; an existing value is preserved. Already supplied props are omitted. Hover or Go to Definition on a prop identifies its declaration.

Prop suggestions follow unsaved PHP/Twig edits. Saved PHP, configuration and dependency changes, plus template creation/deletion, refresh registrations. Console execution must be enabled and the workspace trusted. In Restricted Mode, and in a virtual workspace, the console is not run and everything else still works. When discovery is unavailable, component assistance stays quiet; the project's diagnostics report explains why. No unknown-component or unknown-prop warnings are added.

This first component step covers literal names and directly declared props. Inherited/trait props, dynamic component names, prop type checking, custom property hooks and Live Component behavior are not inferred. Classes outside the application's Composer PSR-4 mappings still link to their indexed template.

Live Components

In the template of a component the console reports as live, data-model completes the props the class marks #[LiveProp(writable: true)], past any modifiers such as on(change)|, and live_action() and data-live-action-param complete the methods marked #[LiveAction]. Hover says what a name is and which file declares it; Go to Definition opens the declaration. A data-model naming no prop, or a prop that is not writable, and an action the class does not declare, are reported, because each is refused when the request arrives and says nothing in the editor; wicker.diagnostics.unknownLiveMember turns that down or off.

Stimulus and API connections

In a StimulusBundle project, complete controller names in data-controller and the stimulus_controller(), stimulus_action() and stimulus_target() helpers. Complete action methods, targets and value keys, or Ctrl-click them to open their JavaScript/TypeScript declarations. The same navigation works in data-action, data-…-target and data-…-value attributes.

Wicker reads debug:config stimulus and maps the runtime project root back to your workspace, including Docker projects. Local controller identifiers follow StimulusBundle's configured directories and filename conventions. Enabled UX controllers come from controllers.json and their installed package metadata. Member suggestions cover direct declarations on a default exported class; inherited/computed members and runtime registrations are not inferred.

Both directions of a binding are checked, because a template asks for a member by name and the controller declares it by name with nothing between them. A binding naming a member its controller does not declare is reported where it is written, under wicker.diagnostics.unknownStimulusMember: Stimulus attaches nothing and reports nothing, in the browser or anywhere else. A static targets, classes or outlets entry no page binds is greyed where it is declared, under wicker.diagnostics.unusedStimulusMember, since reading it throws where nothing provides it. Values are not reported that way, because a value declares its own default. Both are suspended for a controller built on a base class of your own, whose inherited members are not read.

Quick fixes write what a binding is already asking for. A controller nothing registers is created in the configured directory, in TypeScript when the project's controllers are. A method, target, value, class or outlet the controller lacks is added to it. A relative import written without its extension gains the one that exists. On a data-controller attribute, a refactoring connects the controller to another through an outlet, declaring it in the controller and binding it on the element in one edit.

JavaScript imports are checked the way the browser resolves them: a relative path has to be a mapped asset written with its extension, and a bare specifier has to be an importmap.php entry. That file is the whole import map only while Symfony's own reader reads it. Where a project's configuration or PHP names asset_mapper.importmap.config_reader, it can generate entries the file never holds, so bare imports are not checked, and a generated # alias such as #app/analytics/modal.js is followed to the mapped asset whose logical path it names, in an import and as an entrypoint passed to importmap(), which is read whether it takes one name or a list. The chain from an entrypoint then still reaches the scripts and stylesheets it loads. Show diagnostics says which file names the service.

Routes come from debug:router --format=json. In Twig, complete literal route names in path() and url(), then navigate to the application's PHP action. Inside the call, complete the route's parameters; hover lists each placeholder with its requirement and default, and hover on a key says what it fills. A call that names an unregistered route, or leaves out a placeholder its route requires, is reported; wicker.diagnostics.missingRouteParameter turns that down or off. A key the route does not declare is not an error, since Symfony appends it as a query string. redirectToRoute() and generateUrl() in PHP get the same completion, hover and checks, the route name included. In JavaScript, Ctrl-click an unambiguous root-relative fetch() URL, or a Stimulus URL value supplied by a Twig helper/data attribute, to follow the request. When the action renders Twig, Go to Definition also offers its template. Hover shows the route, response fields, rendered templates and known consumers. Find All References on a route reference or PHP action name opens its Twig and JavaScript consumers. Wicker: Go to Route in the Command Palette lists every route whose action is in the workspace, by path, name and controller, and opens the action.

The sidebar's Template routes branch lists routes whose actions render Twig HTML. Click a URL to open its PHP action, or expand it to open the template. A fragment that JavaScript fetches is still a template route, and its row says which file fetches it. The API routes branch appears when there are JSON endpoints, by response format or by what the action returns. Click an endpoint to open its action; expand it to browse what calls it. A /api path prefix alone does not claim that an endpoint returns JSON, and a route that renders a template is never listed here. Both route sections are grouped by URL path, so /api/v1/tasks/{id} sits under api › v1 › tasks, each folder counts what lies beneath it, and each row is named by its last segment with the whole path in its tooltip. Set wicker.sidebar.routeHierarchy to false for one level with full paths. JSON routes use the {} object icon and routes rendering Twig a leaf with a route arrow. Twig template files use a plain leaf. A row under a controller is an action of that controller and wears what it produces, read from the PHP rather than from its route: the object for JSON, the leaf for Twig, and the plain method icon for one that does neither. The icons include light and dark theme variants.

JSON field completion and navigation follow local awaited assignments:

const response = await fetch(this.statusUrlValue);
const data = await response.json();
data.message; // Complete a field or open its PHP declaration.

This works in .js, .ts and JavaScript inside <script> blocks in Twig. Fields come from literal arrays returned by $this->json() or a directly constructed Symfony JsonResponse. Nested literal fields are supported; multiple return branches expose their shared fields. JSON fetched in the browser does not introduce server-side Twig variables.

Dynamic URLs, ambiguous routes, promise chains, cross-function data flow, serializer/DTO schemas and external API contracts remain unknown. Wicker does not call endpoints to inspect their responses. Application source edits, including unsaved changes, update consumer links and declarations; saved PHP and configuration changes refresh runtime registrations. Discovery requires a trusted workspace and enabled console execution. No new warnings are added.

Roadmap

Built one capability at a time, each finished before the next starts. The intended scope spans the Symfony ecosystem, including its frontend integrations through Symfony UX and Stimulus.

Done. The controller and template bridge: go to definition, hover, completion, missing-template diagnostics and semantic colouring on every template reference from both PHP and Twig, with a quick fix that creates the file. Twig syntax highlighting with HTML, JavaScript and CSS embedded. Reverse navigation from a template to its PHP render calls and attributes. A native sidebar for browsing controllers, actions and templates by namespace. Controller context keys suggested inside Twig, with hover identifying their PHP sources. Filter and function completion, hover and unknown-name warnings grounded in the project's Twig discovery. Scoped local variables and context propagation through literal includes and inheritance, with PHP/Twig sources identified on hover. Twig Component name and prop completion, hover and navigation to registered classes, templates and directly declared props. Stimulus controller/action/target/value assistance, route navigation, API tree connections and literal JSON response fields. Controller dependencies and associated scripts connect the project tree, with source-map-based TypeScript preference and distinct icons for templates, routes and project types.

Next, in order:

  1. Stimulus and AssetMapper: the rest of the coverage. Shipped in 0.5.0: outlets, CSS classes, action descriptors, events between controllers, generated members, value types, mapped assets, importmaps and import navigation, explanations and a first repair. Since then: action parameters, value change callbacks, the loading chain as a browsable tree, CSS url() references, and the create and connect edits. Still open: a repair for a missing importmap entry, and connecting controllers written with the stimulus_controller() helper. The sprint scope records the status of every row.
  2. Symfony UX and Live Components. Deepen component workflows through real use. This part of the roadmap covers Twig Components, Live Components, StimulusBundle, UX Toolkit, Icons and CalendarLink.
  3. Routes and response contracts. Extend the initial route and API connections with parameter assistance and additional explicitly traceable response shapes.
  4. Services and the container. Autowiring and parameter intelligence.
  5. Translations, forms, and the rest of the Symfony surface.

Not planned: PHP as a general language

Navigation, completion and diagnostics across a PHP codebase at large, the things an established PHP extension already does, are deliberately out of scope.

The reason is how VS Code resolves overlapping extensions. Hovers and definitions merge, so two extensions can both contribute. Completion does not: providers are grouped by selector score and the first group to answer wins, so a lower-scoring provider is never asked. Semantic tokens are stricter still, with one provider selected outright. Competing for those slots is decided by selector specificity and, on a tie, by whichever extension activated last.

That is a contest with no good outcome. Winning it means replacing a mature PHP implementation with a newer one; losing it means shipping features that silently never run. Neither is worth it when the gap actually worth closing is the one between a controller and its template, which nothing else covers.

Wicker stays on Symfony and Twig, and expects to be installed next to a PHP extension rather than instead of one.

License

MIT. See LICENSE.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft