Sell digital products in Sylius with uploaded files, external URLs, download limits, and post-payment delivery.
This plugin adds digital product support to Sylius 2.x.
It lets you:
- mark product variants as digital
- attach multiple files to a variant
- scope files per channel
- use built-in file types: uploaded files and external URLs
- configure download limits and availability windows
- send download links automatically after payment
- let customers download files from the storefront order area
- resend download emails from the admin panel
- upload large files in chunks
For uploaded files, the plugin copies the original product file into an order-specific storage when payment is completed. This keeps customer downloads independent from later catalog changes.
- PHP 8.2+
- Symfony 6.4 or 7.4
- Sylius 2.x
- League Flysystem Bundle 3.x
- Node.js 20+ for building Sylius Standard frontend assets
composer require jkindly/sylius-digital-product-pluginAdd the plugin bundle to config/bundles.php if it is not registered automatically:
<?php
return [
Jkindly\SyliusDigitalProductPlugin\SyliusDigitalProductPlugin::class => ['all' => true],
];sylius_digital_product_admin:
resource: "@SyliusDigitalProductPlugin/config/routes/admin.yaml"
prefix: /admin
sylius_digital_product_shop:
resource: "@SyliusDigitalProductPlugin/config/routes/shop.yaml"The plugin expects your application models to implement its interfaces and use its traits.
Your product variant model should:
- implement
Jkindly\SyliusDigitalProductPlugin\Entity\DigitalProductVariantInterface - use
Jkindly\SyliusDigitalProductPlugin\Entity\Trait\DigitalProductFilesAwareTrait - use
Jkindly\SyliusDigitalProductPlugin\Entity\Trait\DigitalProductVariantSettingsAwareTrait - add the matching Doctrine relations for:
DigitalProductVariantSettingsDigitalProductFile
Example:
<?php
declare(strict_types=1);
namespace App\Entity\Product;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Sylius\Component\Core\Model\ProductVariant as BaseProductVariant;
use Jkindly\SyliusDigitalProductPlugin\Entity\DigitalProductFile;
use Jkindly\SyliusDigitalProductPlugin\Entity\DigitalProductVariantInterface;
use Jkindly\SyliusDigitalProductPlugin\Entity\DigitalProductVariantSettings;
use Jkindly\SyliusDigitalProductPlugin\Entity\DigitalProductVariantSettingsInterface;
use Jkindly\SyliusDigitalProductPlugin\Entity\Trait\DigitalProductFilesAwareTrait;
use Jkindly\SyliusDigitalProductPlugin\Entity\Trait\DigitalProductVariantSettingsAwareTrait;
#[ORM\Entity]
#[ORM\Table(name: 'sylius_product_variant')]
class ProductVariant extends BaseProductVariant implements DigitalProductVariantInterface
{
use DigitalProductFilesAwareTrait;
use DigitalProductVariantSettingsAwareTrait;
#[ORM\OneToOne(targetEntity: DigitalProductVariantSettings::class, mappedBy: 'productVariant', cascade: ['persist', 'remove'], orphanRemoval: true)]
protected ?DigitalProductVariantSettingsInterface $digitalProductVariantSettings = null;
#[ORM\OneToMany(targetEntity: DigitalProductFile::class, mappedBy: 'productVariant', cascade: ['persist', 'remove'], orphanRemoval: true)]
protected Collection $files;
public function __construct()
{
parent::__construct();
$this->initializeFilesCollection();
}
}Your channel model should:
- implement
Jkindly\SyliusDigitalProductPlugin\Entity\DigitalProductChannelInterface - use
Jkindly\SyliusDigitalProductPlugin\Entity\Trait\DigitalProductFileChannelSettingsAwareTrait - add the relation for
DigitalProductChannelSettings
Your order model should:
- implement
Jkindly\SyliusDigitalProductPlugin\Entity\DigitalProductOrderInterface - use
Jkindly\SyliusDigitalProductPlugin\Entity\Trait\DigitalProductOrderAwareTrait
Your order item model should:
- implement
Jkindly\SyliusDigitalProductPlugin\Entity\DigitalProductOrderItemInterface - use
Jkindly\SyliusDigitalProductPlugin\Entity\Trait\DigitalProductFilesAwareTrait - add the relation for
DigitalProductOrderItemFile
The test application in tests/TestApplication/src/Entity/ shows the full working setup.
Example:
sylius_product:
resources:
product_variant:
classes:
model: App\Entity\Product\ProductVariant
sylius_channel:
resources:
channel:
classes:
model: App\Entity\Channel\Channel
sylius_order:
resources:
order:
classes:
model: App\Entity\Order\Order
order_item:
classes:
model: App\Entity\Order\OrderItemThe plugin prepends its Doctrine migrations automatically. Run:
bin/console doctrine:migrations:migrateThe bundle automatically registers:
- Sylius Mailer configuration for the digital download email
- Flysystem storages for product files, order files, and upload chunks
- Twig hooks for admin and shop UI integration
- serializer and validator mapping
You only need extra configuration if you want to override the defaults.
Root key:
sylius_digital_product:Available options:
sylius_digital_product:
uploaded_file:
delete_from_storage_on_remove: false
chunk_size: 5242880
product_files_path: null
order_files_path: null
chunks_path: nullDefault storage directories:
var/uploads/product_filesvar/uploads/order_filesvar/uploads/tmp/chunks
The plugin provides two file types out of the box:
uploaded_file
A physical file stored through Flysystem.external_url
A redirect to a remote URL.
Each file type has its own:
- DTO
- form type
- validator
- serializer
- provider
- response generator
- channel forms expose default digital-product settings
- product and variant forms expose digital settings and file collections
- uploaded files can be sent directly or through chunked upload
- admins can download uploaded files for preview
When the workflow.sylius_order_payment.completed.pay event is triggered, the plugin:
- creates
DigitalProductOrderItemFilerecords for every digital file in the order - copies uploaded files into order-specific storage
- calculates download limits and expiration dates
- dispatches a message that sends the digital download email
Customers receive download links after payment and can also access files from the order view.
The public download route uses a UUID token:
/download/{uuid}
Before returning the response, the plugin:
- checks whether the file exists for the order
- verifies the download limit
- verifies the availability window
- increments the download count
- returns either a streamed file download or a redirect, depending on file type
Guest customers are supported because the UUID acts as the download token.
The admin order page includes an action for resending the digital download email after the order has been paid.
Large uploads can leave temporary chunk directories behind. Use:
bin/console sylius:digital-product:cleanup-chunksOptions:
--hours=24to remove only old chunks--forceto remove all chunk directories
The file type system is extensible. To add a custom type, create and register:
- a DTO implementing
FileDtoInterface - a form type extending
AbstractFileType - a data transformer for DTO <-> array conversion
- a serializer for the configuration payload
- a response generator
- a provider implementing
FileProviderInterface
Register the provider, serializer, and response generator with the plugin tags defined in config/services/.
composer install
vendor/bin/console doctrine:database:create
vendor/bin/console doctrine:migrations:migrate -n
vendor/bin/console sylius:fixtures:load -n
(cd vendor/sylius/test-application && yarn install)
(cd vendor/sylius/test-application && yarn build)
vendor/bin/console assets:installvendor/bin/phpunit
vendor/bin/behat
vendor/bin/phpstan analyse -c phpstan.neon -l max src
vendor/bin/ecs checkFor JavaScript Behat scenarios, start a browser driver and the Symfony test server first, as in the test application workflow already used in this repository.
This plugin is released under the MIT License.