iq2i/storia-bundle
A Symfony bundle for integrating Storia into PHP applications. It provides the service wiring and configuration needed to enable Storia features in a Symfony project, so you can install via Composer, register the bundle, and start using it without building custom integration code.
Framework reality check first: this is a Symfony bundle, not a Laravel package. There is no service provider, no config/storia.php to publish, no Facade. The Laravel equivalents map like this:
| Laravel | Symfony / this bundle |
|---|---|
config/*.php + vendor:publish |
config/packages/iq2i_storia.yaml (Flex recipe) |
Service provider register() |
StoriaBundle in config/bundles.php |
php artisan vendor:publish |
bin/console config:dump-reference iq2i_storia |
routes/web.php |
bundle-provided routing, import it yourself |
Minimal path:
composer require --dev iq2i/storia-bundle
Symfony Flex should register it under the dev key in config/bundles.php. If not:
// config/bundles.php
return [
// ...
IQ2i\StoriaBundle\StoriaBundle::class => ['dev' => true, 'test' => true],
];
As of 0.6.0 the UI boots whenever kernel.debug is true — you don't need any YAML to see the sidebar in dev. Only add config if you want it somewhere debug is off (e.g. a staging env):
# config/packages/iq2i_storia.yaml
when@dev:
iq2i_storia:
enabled: true # only needed on envs where kernel.debug is false
Confirm what's actually mounted before writing anything:
bin/console config:dump-reference iq2i_storia
bin/console debug:router | grep -i storia
bin/console lista storia # 0.6+: storia:list, storia:render, storia:test, storia:export
Where to look first, in order:
https://ui-storia.com/docs/ — 0.6 split the docs into pages (getting started, YAML reference, compatibility policy) and publishes a demo. Start here now, not the README.src/DependencyInjection/Configuration.php — the actual config schema. Docs drift, the tree node does not.config/routes/ / Resources/config/routes.yaml — real route names for Twig path() calls.skills/ui-storia/ (0.6+) — the agent skill doubles as an executable "how to use this" spec; it exercises storia:list, storia:render, storia:test end-to-end.tests/ — still the most honest documentation of intended usage.storia/data/ — 0.6 shared data files; if you're inheriting a project, this tells you how fixtures were wired.First use case: pick one existing Twig component (a button, a card, a form partial), write a single story definition, boot in dev, hit the Storia route. Even better, verify from the CLI first:
bin/console storia:list # confirm the variant is discovered
bin/console storia:render Button default # confirm it renders
Then open the browser and use the Controls tab (new in 0.6) to tweak args live. That one loop — story file → discovery → render → tweak → snapshot — teaches you 80% of the bundle. Only then scale to a component inventory.
Story-per-component, colocated or centralized. Pick one convention on day one and enforce it. The two shapes:
templates/components/Button.html.twig + Button.stories.yaml|php — great for ownership, awkward when the story needs PHP data.storia/ mirroring your component tree — easier to review in PRs. In 0.6 the docs explicitly recommend treating storia/ as code (it now holds data files too, not just stories).Shared data in storia/data (0.6). This is the single biggest day-to-day win. Stop hard-coding fixture arrays into each story:
# storia/data/products.yaml
- !lorem title 6 10
- name: !data App\Storia\Fixtures::product
Reference them with !data, generate with !repeat / !lorem. PHP providers use !data Class::method — note that bare Class::method is deprecated in 0.6, always prefix with !data.
Variants over one-off stories. Model states as a list — default, disabled, loading, error — rather than duplicating files. The variant name is now the tab label (documented behavior change in 0.6), so name them like a UI state, not like a filename.
Controls tab. Added in 0.6, it derives editable arguments from the component props, its PHP class, or {% types %} on a page. Use it during design reviews and for writing bug reports ("open Controls, set size: lg, hit reload") — it's faster than a screen share.
Variants as tests — wire this on day one. storia:test renders every variant and diffs against an HTML snapshot. For PHPUnit:
use IQ2i\StoriaBundle\Test\StoriaVariants;
use IQ2i\StoriaBundle\Test\StoriaTestCaseTrait;
class StoriaSnapshotTest extends \PHPUnit\Framework\TestCase
{
use StoriaTestCaseTrait;
public function testCatalog(): void
{
(new StoriaVariants())->assertAllMatch($this);
}
}
This is the feature that converts Storia from "nice demo" into a regression net. Run storia:test in CI and commit snapshots — the diff on a PR is the design review.
Static export for previews. bin/console storia:export produces a static site (HTML + assets). Two uses: (a) publish to a PR-preview subdomain / GitHub Pages so non-engineers can browse without running Symfony, (b) attach as a CI artifact on every UI-touching build. No PHP runtime in prod, no route exposure risk.
AI-agent friendly. 0.6 ships storia:list and storia:render as first-class commands, plus skills/ui-storia and Symfony Mate tools (storia-list, storia-render, storia-test). If you use Claude/Codex-type agents, point them at the skill and they can inspect and render components without opening a browser. Handy for "which variants exist and what does disabled render as?" questions.
Wire real assets, not placeholders. Point the preview at your actual Webpack Encore / AssetMapper entrypoints. A component library preview rendered with no CSS is a demo, not a tool. Verify storia:export output too — the static site bundles assets, so a missing manifest.json will be obvious there.
Gate it appropriately. With kernel.debug driving enablement you mostly get this for free, but staging-without-debug needs an explicit enabled: true, and you should still assert it never appears in prod:
# config/routes/dev/storia.yaml — only if the recipe route is unconditional
storia:
resource: '@StoriaBundle/Resources/config/routes.yaml'
prefix: /_storia
Form previews changed semantics in 0.6. A YAML variant's options now override the global form options (previously the global won for automatic variants). If you have custom form stories, re-check them — this is a silent behavioral flip, not an error. Also, CSRF is now disabled for form previews so snapshots are stable and storia:render doesn't need a session — don't write assertions that assume a CSRF token exists.
Put it in review workflow. Link the preview URL (or the storia:export artifact) in PR descriptions for UI-touching changes; run storia:test in CI; let storia:export produce a browsable catalog for stakeholders.
If you're actually a Laravel shop: don't force this into Laravel. Two sane options — (a) stand up a throwaway Symfony skeleton app purely as a component preview harness (storia:export makes this cheap — you don't need a running app for consumers), or (b) read the source as a design reference and reproduce the discovery/render/export loop in Blade + a /dev/components route. Option (b) is usually less work than people assume, and 0.6's split docs + static export make it easier to copy the model without adopting the runtime.
0.6 breaking changes — the checklist before upgrading from 0.5:
kernel.debug. Any non-debug env (staging, a demo host) that previously served the UI now won't unless you set iq2i_storia.enabled: true.iq2i_storia_menu() was removed and the sidebar is built by the bundle. Delete any override template calling it.options now override globals (reversal from 0.5). Re-verify every form story.blocks are printed as HTML and no longer compiled as Twig. If a 0.5 story put Twig in a block (e.g. {{ ... }} in a slot), it will now be output verbatim. Move logic to args or a data provider. This is a deliberate security change — args and blocks go through the rendering context, and values from data providers are never compiled.Follow the upgrade guide at https://ui-storia.com/docs/upgrade/ and read composer show -a iq2i/storia-bundle for the tag history.
Deprecations to fix now: bare Class::method strings for data providers are deprecated — always !data Class::method. Grep your storia/ tree for un-prefixed strings once.
Optional deps: symfony/form and symfony/ux-twig-component are no longer required. The bundle compiles without them, but the matching view types simply won't be available. If form previews or TwigComponent discovery are missing, check composer show | grep -E 'symfony/(form|ux-twig-component)' before assuming a bug.
Read Configuration.php, not the README. On a small bundle the config tree is authoritative. bin/console config:dump-reference iq2i_storia costs two seconds. The 0.6 YAML reference in the docs is much better than the 0.5 README, but the tree node still wins.
Story discovery is cached. After creating a story, if it doesn't appear, bin/console cache:clear before suspecting your path config. Custom discovery services carry kernel.reset inconsistently.
Live reload closing after ~55s is intentional. The 0.6 stream closes and the browser reconnects. Don't chase it as a bug; if you have a very long-lived preview session it will look like a hiccup.
The Twig tab only shows project templates. 0.6 limits absolute template paths to the project directory — vendor templates are hidden. Expected, not a regression.
Asset 404s are a build step, not a bundle bug. Encore manifests are per-environment. Check public/build/manifest.json exists for the env, and — for static export — verify storia:export was run after yarn build / asset-map:compile.
Never let the preview route reach prod. bin/console debug:router --env=prod | grep -i storia in CI. With kernel.debug defaulting off in prod this is mostly handled, but staging hosts and custom deploys can flip debug on.
Snapshot tests need a stable environment. storia:test compares HTML, so pin the PHP version, lockfile, and assets in CI. Form previews no longer include a CSRF token (0.6) which removes one source of flake, but dates from !data providers still need to be deterministic — the docs' !lorem/!data fixtures are examples, not guaranteed-stable values. Seed or freeze anything time-dependent.
Template overrides beat forking. Standard Symfony bundle convention: drop replacements in templates/bundles/StoriaBundle/. For behavior, decorate services in services.yaml. The 0.6 command surface (storia:list|render|test|export) is your real integration point — prefer scripting those over patching internals.
Extension points worth knowing: custom story discovery/loading service, the data-provider mechanism (!data Class::method — any class with a public method works, so this is effectively a DI-friendly fixture hook), the preview controller (for auth or an env banner), and the new CLI commands. Scan src/DependencyInjection/ for service IDs you'd tag or decorate — that list is your real customization surface.
Version pinning — still pre-1.0, but maturing. 0.6 ships a compatibility policy and a proper docs site, so upgrades from here should be less archaeological. Still: commit the lockfile, read the diff, and keep story definitions boring (data + template reference) so rendering-layer churn on the bundle side doesn't cascade into your files. 0.6 is a good baseline to pin to — it fixes the "which variant exists?" CLI gap with storia:list, which is the first thing you'll want in a busy repo.
How can I help you explore Laravel packages today?