PHPackages                             setono/sylius-qr-code-plugin - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. setono/sylius-qr-code-plugin

ActiveSylius-plugin[Utility &amp; Helpers](/categories/utility)

setono/sylius-qr-code-plugin
============================

QR code generation, management, and scan tracking plugin for Sylius.

v1.0.0-alpha.3(2mo ago)0376MITPHPPHP &gt;=8.1CI passing

Since Apr 29Pushed 2mo agoCompare

[ Source](https://github.com/Setono/sylius-qr-code-plugin)[ Packagist](https://packagist.org/packages/setono/sylius-qr-code-plugin)[ GitHub Sponsors](https://github.com/Setono)[ RSS](/packages/setono-sylius-qr-code-plugin/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (3)Dependencies (47)Versions (4)Used By (0)

Setono SyliusQRCodePlugin
=========================

[](#setono-syliusqrcodeplugin)

[![Latest Version](https://camo.githubusercontent.com/81855739f27e928b43f22ace32666352d489cc749935af998634af7b9bdc2919/68747470733a2f2f706f7365722e707567782e6f72672f7365746f6e6f2f73796c6975732d71722d636f64652d706c7567696e2f762f737461626c65)](https://packagist.org/packages/setono/sylius-qr-code-plugin)[![Software License](https://camo.githubusercontent.com/075a4b62be0f9787319e9c586557b6b350dd519208f7d991293fb3475ffbe2fa/68747470733a2f2f706f7365722e707567782e6f72672f7365746f6e6f2f73796c6975732d71722d636f64652d706c7567696e2f6c6963656e7365)](LICENSE)[![Build Status](https://github.com/Setono/sylius-qr-code-plugin/workflows/build/badge.svg)](https://github.com/Setono/sylius-qr-code-plugin/actions)[![Code Coverage](https://camo.githubusercontent.com/f1cb5112653f7d617d447bb7dcac00da852fb04463360e92489544aac48a04d1/68747470733a2f2f636f6465636f762e696f2f67682f5365746f6e6f2f73796c6975732d71722d636f64652d706c7567696e2f6272616e63682f6d61737465722f67726170682f62616467652e737667)](https://codecov.io/gh/Setono/sylius-qr-code-plugin)[![Mutation testing](https://camo.githubusercontent.com/b3401335cc5619a5190f72d6dec2d70ae1af3a711800177205f98b1d8711f4c8/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f7374796c653d666c61742675726c3d687474707325334125324625324662616467652d6170692e737472796b65722d6d757461746f722e696f2532466769746875622e636f6d2532465365746f6e6f25324673796c6975732d71722d636f64652d706c7567696e2532466d6173746572)](https://dashboard.stryker-mutator.io/reports/github.com/Setono/sylius-qr-code-plugin/master)

Generate, manage, and track QR codes from Sylius admin. Each QR code points at a stable plugin-owned redirect URL (`/qr/{slug}`) so the printed code never has to change when the underlying destination does, and every scan is recorded against the QR code that produced it.

Features
--------

[](#features)

- **Two QR code types** out of the box:
    - **URL QR code** — redirects to any absolute target URL.
    - **Product QR code** — redirects to a Sylius product's slug-based URL on the current channel. Both are first-class Sylius resources backed by Single Table Inheritance, so you can add your own subtype later without touching the existing schema.
- **Per-channel rendering.** The same QR code rendered against two channels encodes two different redirect URLs (the channel hostname is baked into the image), so you can print channel-specific codes from a single QR code definition.
- **Stable public redirect** at `GET /qr/{slug}` with a plugin-wide HTTP status code (`setono_sylius_qr_code.redirect_type`; default `302`). UTM parameters configured on the QR code are appended to the resolved target URL.
- **PNG, SVG, and PDF download** from the admin (`/admin/qr-codes/{id}/download/{format}/{channel}`).
- **Scan tracking.** Every successful redirect persists a `QRCodeScan` row (timestamp, IP, user agent) and dispatches a `QRCodeScannedEvent` for your own analytics integrations. Listener exceptions never block the redirect.
- **Admin stats page** per QR code — total / 7d / 30d / 90d, scans-over-time, recent scans.
- **Bulk-generate product QR codes** from the Sylius product grid: select products, click the bulk action, get one Product QR code per selected product (skipping ones that already have a QR code with that slug).
- **10 translations** shipped: en, da, de, es, fr, it, nl, no, pl, sv.

Requirements
------------

[](#requirements)

- PHP 8.1+
- Sylius 1.x (the plugin is developed against the 1.14 branch; check the branch alias in `composer.json` for the current target)
- A relational database supported by Doctrine ORM (MySQL/MariaDB, PostgreSQL, SQLite)
- `stof/doctrine-extensions-bundle` enabled with the timestampable listener turned on (the plugin uses Gedmo's `@Timestampable` for `created_at` / `updated_at` and `scanned_at`)

Installation
------------

[](#installation)

### 1. Require the plugin

[](#1-require-the-plugin)

```
composer require setono/sylius-qr-code-plugin
```

### 2. Register the bundle

[](#2-register-the-bundle)

The plugin **must** be registered *above* `SyliusGridBundle` in `config/bundles.php`. The plugin's grid configuration references container parameters (e.g. `setono_sylius_qr_code.model.qr_code.class`) that are registered by the plugin's `sylius_resource` resources. If `SyliusGridBundle` boots first, it tries to resolve those parameters before the plugin has had a chance to register them and you get:

```
You have requested a non-existent parameter "setono_sylius_qr_code.model.qr_code.class".

```

```
// config/bundles.php
return [
    // ...
    Setono\SyliusQRCodePlugin\SetonoSyliusQRCodePlugin::class => ['all' => true],
    Sylius\Bundle\GridBundle\SyliusGridBundle::class => ['all' => true],
    // ...
];
```

### 3. Import routes

[](#3-import-routes)

```
# config/routes/setono_sylius_qr_code.yaml
setono_sylius_qr_code:
    resource: "@SetonoSyliusQRCodePlugin/Resources/config/routes.yaml"
```

If your store doesn't use locale-prefixed URLs, import `routes_no_locale.yaml` instead. The two files are parallel dispatchers — pick the one that matches how the rest of your shop is wired.

The import registers:

- `GET /qr/{slug}` — public redirect (shop)
- `/admin/qr-codes/...` — admin grid, create/update/show/delete, stats, download, bulk-generate

### 4. Verify Gedmo's timestampable listener is enabled

[](#4-verify-gedmos-timestampable-listener-is-enabled)

A standard Sylius installation already ships `stof/doctrine-extensions-bundle` with the `timestampable` listener enabled — there is nothing to do on a stock Sylius project. If you are running a customised setup that disables it, re-enable it for the default ORM manager:

```
# config/packages/stof_doctrine_extensions.yaml
stof_doctrine_extensions:
    orm:
        default:
            timestampable: true
```

### 5. Update the database schema

[](#5-update-the-database-schema)

Either generate a migration:

```
bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate
```

…or, in a fresh project, just create the schema:

```
bin/console doctrine:schema:update --force
```

The plugin creates two tables: `setono_sylius_qr_code__qr_code` (single-table inheritance for both subtypes) and `setono_sylius_qr_code__qr_code_scan`.

That's it — visit `/admin/qr-codes/` to see the grid.

Public redirect endpoint
------------------------

[](#public-redirect-endpoint)

`GET /qr/{slug}` is the URL the QR codes encode. The handler:

1. Looks up an enabled QR code by slug. Unknown / disabled slugs return 404.
2. Resolves the target URL via `TargetUrlResolverInterface` (subtype-aware: product → channel product slug URL, target URL → the stored URL, decorated with UTM parameters).
3. Persists a `QRCodeScan` and dispatches `QRCodeScannedEvent`.
4. Returns a redirect with the plugin-wide configured status (`setono_sylius_qr_code.redirect_type`, default `302`). The status code is plugin-wide rather than per-QR because permanent redirects (301) get cached aggressively by browsers and crawlers — once issued, the slug cannot be repointed without users hitting the stale target. Override the parameter if your deployment has a different policy.

Slugs are restricted to `[a-z0-9-]+`. The factory derives slugs Sylius-style from the entity name, so you usually don't pick them by hand.

Configuration reference
-----------------------

[](#configuration-reference)

Defaults match the snippet below — drop only the keys you want to change.

```
# config/packages/setono_sylius_qr_code.yaml
setono_sylius_qr_code:
    # HTTP status code returned by the public /qr/{slug} redirect. Plugin-wide, not per-QR —
    # 302 (the default) is the safe choice, because 301 gets cached by browsers and crawlers
    # and prevents repointing the slug. Allowed: 301, 302, 307.
    redirect_type: 302

    utm:
        # Default UTM source/medium written onto new QR codes by the factory. Each QR code
        # then carries its own snapshot — changing these defaults later does NOT rewrite
        # existing QR codes.
        source: qr
        medium: qrcode

    # Each resource block accepts the standard Sylius `classes.{model,controller,repository,form,factory}`
    # overrides if you need to subclass the entity, swap the controller, etc.
    resources:
        qr_code: ~
        product_related_qr_code: ~
        target_url_qr_code: ~
        qr_code_scan: ~
```

Customization
-------------

[](#customization)

### Hooking into scan events

[](#hooking-into-scan-events)

Every time the public `/qr/{slug}` endpoint resolves an enabled QR code, the plugin dispatches `Setono\SyliusQRCodePlugin\Event\QRCodeScannedEvent` before returning the redirect. The event carries the resolved `QRCodeInterface` and the incoming `Request` and is the single extension point for anything you want to do on a scan — analytics, Slack notifications, server-side Google Analytics / Matomo / Segment, queueing async work, anything.

The plugin's own scan tracker is wired as a subscriber on this event, so you don't need to do anything to keep the built-in `QRCodeScan` rows being persisted.

```
namespace App\EventSubscriber;

use Setono\SyliusQRCodePlugin\Event\QRCodeScannedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class TrackScanInGoogleAnalytics implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [QRCodeScannedEvent::class => 'onScan'];
    }

    public function onScan(QRCodeScannedEvent $event): void
    {
        $qrCode = $event->qrCode;
        $request = $event->request;

        // … send a Measurement Protocol hit, enqueue a Messenger message, etc.
    }
}
```

Register it with `autoconfigure: true` (Symfony picks up the tag automatically) or with `tags: [kernel.event_subscriber]` explicitly.

**Listener exceptions never block the redirect.** `RedirectAction` wraps the dispatch in a try/catch — if a listener throws, the exception is logged at `error` level with the QR code id and slug, and the user is still redirected. Misbehaving third-party tracking code can never strand a scanner on a broken page.

### Replacing the built-in scan tracker

[](#replacing-the-built-in-scan-tracker)

Two options, depending on intent:

1. **Decorate `Setono\SyliusQRCodePlugin\Tracker\ScanTrackerInterface`.** The shipped subscriber delegates to it, so your replacement transparently picks up the built-in flow without touching the event wiring. Useful for swapping in async persistence (Symfony Messenger, write-behind cache, etc.).
2. **Remove the shipped subscriber and register your own.** Use this when you want to drop `QRCodeScan` persistence entirely or replace it with a fundamentally different storage shape.

### Writing a custom target URL resolver

[](#writing-a-custom-target-url-resolver)

`TargetUrlResolverInterface` is the seam for adding new QR code subtypes (or rerouting an existing one). Each implementation reports `supports(QRCodeInterface)` and `resolve(...)`; the composite resolver picks the first supporting service. Tag your service with `setono_sylius_qr_code.target_url_resolver`:

```

```

UTM parameters are layered on top by `UtmTargetUrlResolver`, which decorates the composite — you do not need to handle UTM in your subtype resolver.

### Choosing a default channel

[](#choosing-a-default-channel)

When an admin downloads a QR code without picking a channel (the channel field defaults to "first enabled"), the plugin asks `Setono\SyliusQRCodePlugin\Channel\DefaultChannelResolverInterface`. The shipped `FirstEnabledChannelResolver` returns the first channel the channel repository considers enabled. Bind a different implementation to the interface to change that policy:

```

```

### Subclassing the QR code entities

[](#subclassing-the-qr-code-entities)

The plugin ships `` Doctrine mappings, so you can override any model class the standard Sylius way:

```
# config/packages/setono_sylius_qr_code.yaml
setono_sylius_qr_code:
    resources:
        product_related_qr_code:
            classes:
                model: App\Entity\QRCode\ProductRelatedQRCode
```

The plugin's `QRCodeDiscriminatorMapListener` rebuilds the Single Table Inheritance discriminator map from the resource configuration at runtime, so your subclass is wired automatically — no need to duplicate STI annotations on the base class. App-level subclasses can also extend the STI hierarchy with their own `DiscriminatorMap` if you need to add new subtypes that aren't first-class plugin resources.

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance86

Actively maintained with recent releases

Popularity18

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity31

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~6 days

Total

3

Last Release

74d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/2412177?v=4)[Joachim Løvgaard](/maintainers/loevgaard)[@loevgaard](https://github.com/loevgaard)

---

Top Contributors

[![loevgaard](https://avatars.githubusercontent.com/u/2412177?v=4)](https://github.com/loevgaard "loevgaard (46 commits)")

---

Tags

phpsyliussylius-pluginsymfonyqr codeqrsyliussylius-plugin

###  Code Quality

TestsBehat

### Embed Badge

![Health badge](/badges/setono-sylius-qr-code-plugin/health.svg)

```
[![Health](https://phpackages.com/badges/setono-sylius-qr-code-plugin/health.svg)](https://phpackages.com/packages/setono-sylius-qr-code-plugin)
```

###  Alternatives

[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k5.9M754](/packages/sylius-sylius)[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.9M400](/packages/easycorp-easyadmin-bundle)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M215](/packages/sulu-sulu)[oro/platform

Business Application Platform (BAP)

645143.5k116](/packages/oro-platform)[contao/core-bundle

Contao Open Source CMS

1231.6M2.8k](/packages/contao-core-bundle)[open-dxp/opendxp

Content &amp; Product Management Framework (CMS/PIM)

9421.6k64](/packages/open-dxp-opendxp)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
