fluffydiscord / sylius-honkers-plugin
Plug-and-play Sylius wrapper for the Honkers chatbot tool-server: default tools, data sources and shop widget
Package info
github.com/FluffyDiscord/sylius-honkers-plugin
Type:sylius-plugin
pkg:composer/fluffydiscord/sylius-honkers-plugin
Requires
- php: ^8.1
- ext-intl: *
- doctrine/collections: ^1.8 || ^2.1
- doctrine/doctrine-bundle: ^2.13 || ^3.2 || ^4.0
- doctrine/orm: ^2.20 || ^3.6 || ^4.0
- fluffydiscord/honkers-sdk: ^2.0
- fluffydiscord/symfony-honkers-bundle: ^2.0
- liip/imagine-bundle: ^2.13
- sylius/sylius: ^1.14 || ^2.2 || ^2.3@alpha
- symfony/config: ^6.4 || ^7.0 || ^8.0
- symfony/console: ^6.4 || ^7.0 || ^8.0
- symfony/dependency-injection: ^6.4 || ^7.0 || ^8.0
- symfony/event-dispatcher: ^6.4 || ^7.0 || ^8.0
- symfony/http-client: ^6.4 || ^7.0 || ^8.0
- symfony/http-foundation: ^6.4 || ^7.0 || ^8.0
- symfony/http-kernel: ^6.4 || ^7.0 || ^8.0
- symfony/intl: ^6.4 || ^7.0 || ^8.0
- symfony/translation: ^6.4 || ^7.0 || ^8.0
- twig/twig: ^2.12 || ^3.3
Requires (Dev)
- monsieurbiz/sylius-cms-page-plugin: ^2.2
- nyholm/psr7: ^1.8
- phpunit/phpunit: ^11.0 || ^12.0 || ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Renamed from
fluffydiscord/sylius-honkers-bundle→ see UPGRADE-2.0.md.
Sylius plugin for the honkers.dev chatbot: shop tools, catalog sources and the shop widget. Sylius 1.14 and 2.x.
Tool-server mechanics (endpoints, error format, locales, your own tools and sources) live in the Symfony bundle README — this page covers the Sylius pieces.
Features
- Connect from the dashboard — one click pairs a channel; nobody copies a secret
- Credentials — env for a single site, one site per channel, or your own store
- Shipped tools — order status and product availability in the chat
- Shipped sources — products, categories and CMS pages, with links on each channel's domain
- Catalog change notifications — saved products reach the chatbot without waiting for the nightly sync
- Widget — the chat on every shop page, no template edits
- Chat click tracking — visits and revenue from chat links
- Orders from chat — open the conversation behind an order from its admin page
Requirements
| Package | Constraint |
|---|---|
php |
^8.1 |
ext-intl |
* |
sylius/sylius |
^1.14 || ^2.2 || ^2.3@alpha |
doctrine/orm |
^2.20 || ^3.6 || ^4.0 |
symfony/* |
^6.4 || ^7.0 || ^8.0 |
On Sylius 1.14 the widget uses a deprecated hook. 1.14 has no Twig Hooks, so the widget is a
sylius_uitemplate block onsylius.shop.layout.javascripts. The container build reportssylius/ui-bundledeprecations for it: expected. Everything else works the same on both lines.
Install
composer require fluffydiscord/sylius-honkers-plugin
Usage
-
config/bundles.php— register the Symfony bundle and this plugin (Symfony Flex already did this if it's on):return [ // ... + FluffyDiscord\HonkersBundle\FluffyDiscordHonkersBundle::class => ['all' => true], + FluffyDiscord\SyliusHonkersPlugin\FluffyDiscordSyliusHonkersPlugin::class => ['all' => true], ]; -
config/routes/fluffy_discord_honkers.yaml— the routes live in the Symfony bundle,/chatbot/pairincluded:fluffy_discord_honkers: resource: '@FluffyDiscordHonkersBundle/config/routes.php'
Keep them off the shop's
_localeprefix. The backend calls/chatbot/v1/*on the shop host; the dashboard's Connect button opens/chatbot/pair. -
config/packages/security.yaml— guard/chatbot/v1, before the Syliusshopfirewall (firewalls match in order andshopmatches^/):security: + providers: + chatbot_backend: + memory: + users: [] firewalls: + chatbot_api: + pattern: ^/chatbot/v1 + stateless: true + provider: chatbot_backend + entry_point: FluffyDiscord\HonkersBundle\Security\ApiAuthenticationFailureHandler + access_token: + token_handler: FluffyDiscord\HonkersBundle\Security\ApiSecretAuthenticator + failure_handler: FluffyDiscord\HonkersBundle\Security\ApiAuthenticationFailureHandler shop: # ... access_control: + - { path: ^/chatbot/v1, roles: ROLE_CHATBOT_BACKEND }
/chatbot/pairstays outside this firewall — it's a browser redirect, not a backend call. -
config/packages/fluffy_discord_honkers.yaml— where the backend is:fluffy_discord_honkers: backend_url: '%env(CHATBOT_BACKEND_URL)%'
-
.env.local— the backend URL, plus the credentials unless you connect from the dashboard:+CHATBOT_BACKEND_URL=https://honkers.dev +CHATBOT_API_SECRET=change-me +CHATBOT_INGEST_SECRET=change-me +CHATBOT_SITE_KEY=site-key
Configuration
The connection and the widget belong to the Symfony bundle. Defaults:
fluffy_discord_honkers: backend_url: '' widget: enabled: true cdn_url: '' defer: true
| Option | Default | Meaning |
|---|---|---|
backend_url |
'' |
honkers.dev origin; the widget, notifications, reports and pairing talk to it. Empty → nothing is sent |
widget.enabled |
true |
false → no widget. Literal boolean, no %env()% |
widget.cdn_url |
'' |
where chat.js loads from. Empty → https://honkers.b-cdn.net/widget/v1/chat.js |
widget.defer |
true |
false → plain <script src>, no defer. Literal boolean, no %env()% |
Outside
dev, a non-httpsbackend_urlis refused and nothing is sent.
Credentials
The widget needs only a site key. Notifications and order reports also need an ingest secret (outbound:
Authorization: Bearer <site key>.<ingest secret>); the backend calls /chatbot/v1 with the API secret. Per
channel, the first match wins:
| Channel | Site key | Ingest secret |
|---|---|---|
| paired | stored on the channel | stored on the channel |
in channel_site_keys |
the entry | CHATBOT_INGEST_SECRET |
in channel_site_keys with '' |
none → no widget, no notifications | — |
| any other | CHATBOT_SITE_KEY |
CHATBOT_INGEST_SECRET |
Site key without an ingest secret → the widget renders; notifications and order reports are skipped.
The backend's API secret is accepted when it matches CHATBOT_API_SECRET or any paired channel's secret.
A paired API secret works on every channel, not only on the channels it was paired with.
One site per channel
Map channel codes to site keys when each channel is its own site on the backend.
config/packages/fluffy_discord_sylius_honkers_plugin.yaml:
fluffy_discord_sylius_honkers_plugin: channel_site_keys: CZ_WEB: '%env(CHATBOT_SITE_KEY_CZ)%' SK_WEB: '%env(CHATBOT_SITE_KEY_SK)%' DE_WEB: 'pk_live_de'
| Option | Default | Meaning |
|---|---|---|
channel_site_keys |
{} |
{ <channel code>: <site key> }; codes verbatim (cz-web stays cz-web), values literal or %env()% |
- An entry wins over
CHATBOT_SITE_KEY.''opts the channel out — it never falls back. CHATBOT_SITE_KEYthen serves only unmapped channels; leave it empty when every channel is mapped.
Your own store
Alias ChannelCredentialsProviderInterface to your service in config/services.yaml:
services: FluffyDiscord\SyliusHonkersPlugin\Credentials\ChannelCredentialsProviderInterface: '@App\Chatbot\VaultCredentialsProvider'
The Symfony bundle's CredentialsProviderInterface points at the same service. To make Connect store into it too,
also implement FluffyDiscord\HonkersBundle\Contract\CredentialsWriterInterface and alias it the same way.
Alias the Sylius interface, not only the Symfony one. Aliasing just
CredentialsProviderInterfacefails the container build: Sylius resolves credentials per channel.
Connect from the dashboard
Click Connect on the tool-server connection in the chatbot dashboard — the shop generates its secrets, sends them
to backend_url and stores them on the connection's channels (none listed → the channel of the shop host).
Nothing to copy. A new key works at once, no cache clear.
-
src/Entity/Channel/Channel.php— let the channel store credentials:+use FluffyDiscord\SyliusHonkersPlugin\Credentials\HonkersChannelInterface; +use FluffyDiscord\SyliusHonkersPlugin\Credentials\HonkersChannelTrait; use Sylius\Component\Core\Model\Channel as BaseChannel; -class Channel extends BaseChannel +class Channel extends BaseChannel implements HonkersChannelInterface { + use HonkersChannelTrait;
Use
HonkersChannelInterfaceonly together withHonkersChannelTrait. The lookup relies on the trait's fields. -
Add the
honkers_site_key,honkers_api_secret_hashandhonkers_ingest_secretcolumns:bin/console doctrine:migrations:diff bin/console doctrine:migrations:migrate
-
Admin → Channels → set Hostname to the host of the connection's base URL (
shop.czforhttps://shop.cz). Case doesn't matter;www.shop.cz≠shop.cz.Pairing several channels on different hosts? Every channel's hostname must be on a verified domain of the website (
shop.skoreshop.shop.skforshop.sk), else nothing is saved. -
Click Connect in the dashboard. You land back there when it's done.
Adding the trait changes nothing until a channel is paired: unpaired channels keep their configured credentials.
Unset
CHATBOT_API_SECRETonce every channel is paired. It stays valid otherwise.
The API secret is stored only as a SHA-256 hash; the ingest secret is stored as-is (it has to be sent). The trait maps the columns with Doctrine attributes and annotations. Channel mapped in XML → map
honkersSiteKey(string, 64),honkersApiSecretHash(string, 64, fixed) andhonkersIngestSecret(string, 128), all nullable, yourself.
What can go wrong:
| Situation | Result |
|---|---|
| Channel entity without the trait | 409 page: paste the credentials into the shop's configuration |
backend_url empty, or not https outside dev |
400 backend_url_refused, nothing sent |
| Shop host's channel has no hostname | 400 pairing_hostname_missing, nothing sent |
| A paired channel has no hostname | 400 pairing_hostname_missing, nothing saved |
| Connection's base URL on another host | 400 pairing_wrong_shop, nothing saved |
| A paired channel's hostname not on a verified domain | 400 pairing_wrong_shop, nothing saved |
| Connection names a channel code the shop doesn't have | 400 pairing_unsupported, nothing saved |
| Link older than 10 minutes, or used | 400 pairing_not_found |
A failed Connect is retried by clicking Connect again.
Shipped tools
The backend calls these mid-conversation for live data:
get_order_status— order state, payment and shipping state, tracking codes, item count and total; requires the order number and the customer e-mail (uniform "not found" otherwise).get_product_availability— price, currency and stock for up to 20 variant codes; returns aproductsblock for the widget.
Add your own tool → Symfony bundle README.
Shipped sources
The backend pulls these in bulk and ingests them into its retrieval index:
products— indexable channel products per locale withProductMetadata(code, name, url, imageUrl, priceMinor, currency, inStock, taxons, taxonNames, attributes);taxonscarries taxon codes,taxonNamestheir names in the requested locale; the names and main taxon path also live in the document text.categories— enabled taxons of the channel tree per locale (code, name, path, url, productCount).- Tree = the channel's menu taxon subtree. A taxon is served only when every ancestor below the tree top is enabled, so a disabled branch never leaks its children.
- The tree top (menu taxon, or tree root when there is none) never disqualifies what is beneath it. Disabling it empties the channel's menu without emptying this source.
- The menu taxon is served as a category document; a bare tree root is not.
- A channel with no menu taxon serves the shop's only taxon tree. With several trees it is refused
409 ambiguous_taxon_tree. productCountcounts the taxon's whole subtree, only products passingProductIndexabilityInterface— so it agrees with the category page (include_all_descendants: true) and the chatbot.
cms_pages— enabled CMS pages of the channel per locale, from Monsieur Biz CMS or BitBag CMS (registered only when one of the plugins is installed).
Every absolute URL a source or tool emits is built on the resolved channel's hostname, not the
request host. A read for channel=X returns links on X's domain; only the host is swapped.
- Channel hostname empty → the request host stands in.
- Sylius stores no per-channel scheme or port, so both come from the request context. Set
router.request_context.scheme(andhost) fornotify-all— it runs outside a request and would otherwise emithttp://. A shop on a non-standard port carries that port into every channel's URLs.
imageUrl: aliip_imaginecacheresolver in front ofweb_pathmemoises the finished absolute URL under a host-less key — the first channel read then feeds its host to every other channel. Keep the chatbot's image filter on a host-agnostic resolver.
Extension points
Both govern the products source; redefine the container alias in services.yaml to replace them:
| Interface | Default | Purpose |
|---|---|---|
ProductViewFactoryInterface |
ProductViewFactory |
price, stock, image and URL of an indexed product |
ProductIndexabilityInterface |
ProductIndexability |
which products the source serves (enabled, in the channel, at least one priced enabled variant) |
Add your own source → Symfony bundle README.
Catalog change notifications
These keep the backend's ingested sources fresh. Saving a Product, ProductTranslation, Taxon
or TaxonTranslation collects the changed ids; one POST {backend_url}/api/v1/catalog/changes
per (source, locale, site) is sent after the response (kernel.terminate, ConsoleEvents::TERMINATE), one at a
time, max 500 ids per request.
The backend accepts notifications only from an active site. A draft site answers 401 — activate it in the dashboard first. The same goes for order reports and the widget.
| Saved entity | Announces |
|---|---|
ProductTranslation |
its own locale |
TaxonTranslation |
categories for its own locale |
Product |
every locale of sylius_locale |
Taxon |
categories for every locale, no product fan-out |
Recipients: every enabled channel serving the locale. Channels with the same site key share one request.
- Channel without a site key or ingest secret → skipped, warning.
- Channel mapped to
''→ skipped, debug record. - Locale no enabled channel serves → sent nowhere.
backend_urlempty → nothing sent.- Backend asks to retry within 2 s (rate limit) → the batch is sent once more.
- Backend fails or can't queue for longer → logged warning; the shop request is never affected. A dropped notification costs at most one nightly sync of staleness.
Re-announce everything
bin/console fluffydiscord:chatbot:notify-all [--source=products|categories|cms_pages] [--locale=cs_CZ] [--channel=code]
Re-announces the whole catalog of one channel to its site in 500-id batches, pausing 2 s between batches.
A 429 or 503 → waits Retry-After and sends the same batch again, up to 5 times.
- Pass
--channelwhen no channel can be resolved from the CLI context. Run it once per channel. - Disabled channel, or one without a site key or ingest secret → aborts.
- Without
--locale, the channel's locales are announced. --localemust be an ICU-known locale (any spelling); an unknown or unserved locale aborts the run.
Widget
When widget.enabled is true the plugin injects, via the sylius_shop.base#javascripts twig hook on
Sylius 2 and the sylius.shop.layout.javascripts template block on Sylius 1.14:
<script src="{widget.cdn_url}" defer></script> <ai-chat-widget site-key="{site key}" locale="{app.locale}" backend-url="{backend_url}"></ai-chat-widget>
- The site key is the current channel's, looked up on every render (
fluffydiscord_chatbot_site_key()). - No site key → nothing renders, neither script nor element.
widget.defer: false→<script src="…">withoutdefer.
Include the template yourself with a plain context:
{% include '@FluffyDiscordSyliusHonkersPlugin/shop/widget.html.twig' with {
backend_url: 'https://chatbot.example.com',
widget_cdn_url: '',
defer: true,
} only %}
The widget talks only to backend_url; it never calls /chatbot/v1 itself.
Chat click tracking
Visits and orders from chat links are reported for you. No setup beyond backend_url and credentials.
- Visit → the Symfony bundle's landing beacon reports it, with the current channel's site key.
- Product page opened from a chat link (
?gooseclid=…) → the product is remembered in the session. Last click per product wins, at most 20 products. - Order placed with a remembered product → reported after the response is sent: revenue of the matching items (incl. tax and item discounts, excl. shipping), the order's currency, the order channel's site key. Unpaid orders count — it's reported when placed.
- Any placed order resets the session, with or without a chat product.
Only shop checkout is covered (
sylius.order.post_complete). Orders completed through the API or your own code aren't reported.
Backend unreachable → logged warning; checkout is never affected. Non-https backend_url → skipped outside
the dev env.
Orders from chat
Open the chat conversation behind an order from its admin page. Add the interface and trait to your
Order entity:
+use FluffyDiscord\SyliusHonkersPlugin\Attribution\ChatAttributedOrderInterface; +use FluffyDiscord\SyliusHonkersPlugin\Attribution\ChatAttributedOrderTrait; use Sylius\Component\Core\Model\Order as BaseOrder; -class Order extends BaseOrder +class Order extends BaseOrder implements ChatAttributedOrderInterface { + use ChatAttributedOrderTrait;
Then add the chat_click_ids column:
bin/console doctrine:migrations:diff bin/console doctrine:migrations:migrate
The order keeps the click id (gooseclid) of every chat link that led to one of its products, oldest click
first. The order detail shows a From chatbot box, last in the right column:
- Chat order → a numbered list, one Open conversation link per click, to the conversation in the honkers.dev console.
- Other orders →
no.
The trait maps the column with Doctrine attributes and annotations. Order mapped in XML → map
chatClickIds(json, nullable) yourself.
Overriding
@SyliusAdmin/Order/show.html.twigon Sylius 1 drops the sidebar blocks. Include the field yourself:{% include '@FluffyDiscordSyliusHonkersPlugin/admin/order/from_chat.html.twig' %}.
License
MIT