Overview
The DevExchange is your chance to catch up with old friends and meet new people, while discussing the topics that matter to you.
The sessions are planned to run from 16:00 - 18:00 on the pre-event day (Thursday 16th) as per the official agenda.
None, just turn up!
Tables will be split into the topics suggested. Join Hyvä Slack and use the #hyva-dev-paradise channel to suggest topics prior to the event
These workshop sessions are designed to allow developers to get hands-on experience with various Hyvä focused topics.
Each session topic will:
If none of the sessions are of interest to you, you are also welcome to utilise the workshop space during this time to:
The sessions are planned to run from 15:00 - 16:40 on the main event day (Friday 17th) as per the official agenda, but will be broken down as follows:
Each topic will run throughout the entire workshop, but there is a changeover point halfway if you want to switch to another topic.
Note:
Note: many of these are a WIP and will remain so until the event.
Admin Dashboard
Get hands-on with the Admin Dashboard by learning the key concepts of our framework and building your own widget.
An introduction of what the admin dashboard is and how to make widgets. With a focus on existing display types.
Learn to build a Hyva Admin Dashboard Widget.
app/code/Paradise/OrdersByStatus/
├── registration.php
├── etc/
│ ├── module.xml
│ └── adminhtml/
│ └── hyva_dashboard_widget.xml
└── Model/
└── Widget/
└── OrdersByStatus.php
Both framework modules are required. Hyva_AdminDashboardFramework provides the base classes; Hyva_AdminDashboardWidgets provides the dashboard UI that renders them.
<module name="Paradise_OrdersByStatus">
<sequence>
<module name="Hyva_AdminDashboardFramework"/>
<module name="Hyva_AdminDashboardWidgets"/>
<module name="Magento_Sales"/>
</sequence>
</module>
This tells the dashboard the widget exists, what it's called, how it looks, and what display type to render.
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Hyva_AdminDashboardFramework:etc/adminhtml/hyva_dashboard_widget.xsd">
<widget id="orders_by_status">
<title>Orders by Status</title>
<class>Paradise\OrdersByStatus\Model\Widget\OrdersByStatus</class>
<display_type>bar_chart</display_type>
<min_height>6</min_height>
<min_width>1</min_width>
<icon>chart-bar</icon>
<trailing_action>
<label>View All Orders</label>
<route>sales/order</route>
</trailing_action>
</widget>
</config>
The id here must match the protected string $id in your PHP class.
Provides a user-friendly name for the widget. | |
Defines which PHP class is responsible for implementing the behaviour of the widget. | |
Determines which template is used to display the widget once the admin user has added it to their dashboard. In this case we use bar_chart | |
Specify the minimum number or rows and columns a widget occupies. | |
Assigns an icon to the widget that is visible in the new widget popup. | |
Allows developer to define a number of key value to the footer section of a widget |
Extend AbstractBarChart and implement two methods:
namespace Paradise\OrdersByStatus\Model\Widget;
use Hyva\AdminDashboardFramework\Model\Widget\AbstractBarChart;
use Hyva\AdminDashboardFramework\Model\WidgetAuth;
use Hyva\AdminDashboardFramework\Model\WidgetConfig;
use Hyva\AdminDashboardFramework\Model\WidgetInstance\WidgetInstanceInterface;
use Magento\Framework\DB\Sql\Expression;
use Magento\Sales\Model\ResourceModel\Order\CollectionFactory as OrderCollectionFactory;
class YourWidget extends AbstractBarChart
{
protected string $id = 'orders_by_status'; // must match widget XML id
public function __construct(
WidgetAuth $widgetAuth,
WidgetConfig $widgetConfig,
protected readonly OrderCollectionFactory $orderCollectionFactory,
string $id = '',
array $data = [],
) {
parent::__construct($widgetAuth, $widgetConfig, $id, $data);
}
Widget properties refer to a set of input options users can use to configure and customize their widget. There are two properties:
Here we add the store view property to allow users to filter between storeviews.
public function getConfigurableProperties(): array
{
return array_merge(
parent::getConfigurableProperties(),
[
'store_ids' => [
'label' => __('Store Views'),
'input' => [
'type' => 'scope', // Hyvä-specific: renders a store view picker
'attributes' => [
'multiple' => true,
'size' => '10',
'required' => true,
],
],
],
],
);
}
Here we define an order count to allow users to choose how many orders they want to include
public function getDisplayProperties(): array
{
return array_merge(
parent::getDisplayProperties(),
[
'order_count' => [
'label' => __('Number of Orders to Include'),
'input' => [
'type' => 'text',
'subtype' => 'number',
‘'attributes' => [
‘'value' => 30,
This function is responsible for providing widget data. In this case we use
$widgetInstance->getPropertyValue(self::KEY_CONFIGURABLE_PROPERTIES, 'store_ids'); and ($widgetInstance->getPropertyValue(self::KEY_DISPLAY_PROPERTIES, 'order_count') ?? 30); to filter and limit the recent order. Then group them.
public function getDisplayData(WidgetInstanceInterface $widgetInstance): array
{
$storeIds = $widgetInstance->getPropertyValue(self::KEY_CONFIGURABLE_PROPERTIES, 'store_ids');
$orderCount = (int) ($widgetInstance->getPropertyValue(self::KEY_DISPLAY_PROPERTIES, 'order_count') ?? 30);
$recentOrders = $this->orderCollectionFactory->create();
$recentOrders->addFieldToFilter('store_id', ['in' => $storeIds]);
$recentOrders->getSelect() ->reset('columns')
->columns(['status'])
->order('created_at DESC')
->limit($orderCount);
// Outer query: group the subset by status and count
$connection = $recentOrders->getConnection();
$outerSelect = $connection->select()
->from(
['recent_orders' => $recentOrders->getSelect()],
['status', 'count' => new Expression('COUNT(*)')]
)
->group('status');
$rows = $connection->fetchAll($outerSelect);
// Return data in the shape AbstractBarChart expects (ApexCharts format)
return [
'series' => [['name' => (string) __('Orders'), 'data' => array_column($rows, 'count')]],
'xaxis' => ['categories' => array_column($rows, 'status')],
];
}
}
The bar_chart and line_chart display types both use the same formatting of data but the pie_chart is slightly different. See if you can convert the widget to use the pie_chart display type.
Resources:
Top tip:
Try to create a widget that shows the latest newsletter subscribers.
Resources:
Hyvä CMS
Get hands-on with Hyvä CMS by learning the key concepts of the framework and building your own reusable, modern components. This session focuses on practical implementation, helping you go from understanding the structure to confidently creating and customising components.
A practical introduction to Hyvä CMS, combined with a deeper dive into how components work under the hood. We’ll explore how data, templates, and configuration come together, with a focus on building flexible, reusable UI components using Hyvä’s modern stack (TailwindCSS, Alpine.js, and Magento).
You will learn on how to develop a custom component
This guide can be followed in 2 different ways:
Create an app/code module (e.g. Vendor_Example)
app/
└── code/
└── Vendor/
└── Example/
├── registration.php
├── Observer/
│ └── RegisterModuleForHyvaConfig.php
├── etc/
│ ├── module.xml
│ ├── frontend/
│ │ └── events.xml
│ └── hyva_cms/
│ └── components.json
└── view/
└── frontend/
├── templates/
│ └── elements/
│ ├── stl.phtml
│ └── stl-single.phtml
└── tailwind/
└── tailwind.config.js
Or setup from script:
curl -sL https://raw.githubusercontent.com/callanzimmermann/workshop-module/refs/heads/main/setup.sh | bash
{
"stl": {
"label": "Shop the Look",
"description": "Editable component for shop the look, must be used for a large container",
"template": "Vendor_Example::elements/stl.phtml",
"children": true,
"content": {
"text": {
"type": "richtext",
"label": "Text"
},
"image": {
"type": "image",
"label": "Image"
},
"classes": {
"type": "text",
"label": "Classes"
}
}
},
"stl_single": {
"label": "STL Child",
"description": "Editable component for shop the look, must be used within shop the look component",
"template": "Vendor_Example::elements/stl-single.phtml",
"content": {
"x_axis": {
"type": "number",
"label": "X Axis"
},
"y_axis": {
"type": "number",
"label": "Y Axis"
}
}
}
}
Stl.phtml:
<?php
declare(strict_types=1);
use Hyva\CmsLiveviewEditor\Block\Element;
use Hyva\Theme\Model\ViewModelRegistry;
use Magento\Framework\Escaper;
/** @var Element $block */
/** @var Escaper $escaper */
/** @var ViewModelRegistry $viewModels*/
$children = $block->getChildren();
$image = $block->getImage() ?? [];
$imageSrc = @$image['src'] ?? '';
?>
<div
class="relative min-h-[450px] flex items-center justify-center overflow-hidden <?= $escaper->escapeHtmlAttr($block->getClasses()) ?>"
data-liveview-element="stl"
<?= /** @noEscape */ $block->getEditorAttrs() ?>
<?= /** @noEscape */ $block->renderBlockId() ?>
>
<img src="<?= $block->getImagePath($imageSrc) ?>"
class="object-cover w-full absolute -z-10"
<?= /** @noEscape */ $block->getEditorAttrs('image') ?> />
<div <?= /** @noEscape */ $block->getEditorAttrs('text') ?>
class="text-3xl font-bold text-white">
<?= $block->renderRichText($block->getText()) ?>
</div>
<?php foreach ($children ?: [] as $elementData): ?>
<?= $block->createChildHtml($elementData, 'stl_child') ?>
<?php endforeach; ?>
</div>
Slt-single.phtml:
<?php
declare(strict_types=1);
use Hyva\CmsLiveviewEditor\Block\Element;
use Hyva\Theme\Model\ViewModelRegistry;
use Magento\Framework\Escaper;
/** @var Element $block */
/** @var Escaper $escaper */
/** @var ViewModelRegistry $viewModels*/
?>
<div
class="size-4 rounded-full bg-red-500 absolute"
data-liveview-element="stl-single"
style="top:<?= $block->getYAxis() ?>%;left:<?= $block->getXAxis() ?>%"
<?= /** @noEscape */ $block->getEditorAttrs() ?>
<?= /** @noEscape */ $block->renderBlockId() ?>
></div>
CSP & Security
Get hands-on with writing CSP-compliant code by debugging and resolving a pre-prepared issue.
Copy of https://github.com/friends-of-hyva/magento2-paradise-csp-workshop-meta/blob/main/SESSION-GUIDE.md
You write Hyva. You know Alpine. Everything works great — until someone enables strict Content Security Policy and half your JavaScript goes silent. The console fills up. The page looks fine. Nothing works. You have no idea why.
Welcome to Paradise.
This hands-on workshop walks you through exactly why that happens and how to fix it. You'll work with intentionally broken code, fix it yourself, and come out the other side knowing every Alpine pattern that trips up CSP and how to handle each one. By the end, you'll be able to look at any Hyva component and know whether it's CSP-safe — and migrate it if it isn't.
PCI-DSS 4.0 made strict CSP a compliance requirement for payment pages as of April 2025. If your store handles payments, this is no longer optional. The good news: Hyva is built for this. You just need to know the patterns.
Eight Alpine patterns that break under strict CSP — what each one is, why it breaks, and how to fix it:
Plus: the special considerations for Hyva Checkout components (scripts must move out of Magewire templates entirely).
Please have the following ready before the session starts. A broken environment wastes everyone's time — including yours.
Don't have Hyva Checkout? No problem — you can still have a great time. Install the frontend package on its own and everything in /paradise/ works without it:
composer require friends-of-hyva/magento2-paradise-csp-workshop-frontend
The session has two tracks — pick the one that fits where you are.
The Guided Tour (recommended if you're new to CSP): work through the workshop modules in order. Broken code, clear comments pointing at the violations, a quiz, and immediate feedback when you get a fix right. Everything is prepared.
Bring Your Own Code: already familiar with CSP and want practical help migrating a real project? Bring a Hyva module or theme you're working on. There will be time for hands-on migration with help available.
The guided tour works well as preparation for the hands-on track — it gives you the vocabulary and patterns, so the time spent on your own code is spent on actual migration rather than the basics.
Tailwind v4 Migration
Learn how to migrate your theme to Tailwind v4 (the right way!) using the automated upgrade-helper tool to handle the heavy lifting.
Tailwind v4 is a total rewrite designed for the modern web. It replaces the heavy JavaScript configuration with a CSS-native configuration.
In this workshop, we’ll move through the actual migration of a Hyvä theme. You will use the automated migration script which transforms your tailwind.config.js into the new @theme variables and handles the npm cleanup automatically.
Your job is to verify the output, fix pathing issues in your imports, and manually restore your custom styles from the backup created by the tool.
Please have the following ready. A broken environment wastes everyone's time.
composer require --dev hyva-themes/upgrade-helper-tools:dev-main
We'll execute the script provided by the upgrade-helper. This tool updates your package.json, installs Tailwind v4, cleans up node_modules, and automatically migrates your tailwind.config.js into your CSS file.
Note: Ensure you are in the Magento root directory when running this command.
# Example for a theme located at app/design/frontend/Vendor/theme
./vendor/bin/update-to-tailwind-v4.js app/design/frontend/Vendor/theme
The tool moves your config to the @theme block in tailwind-source.css. We will verify the output, ensuring brand tokens are correctly mapped and any complex logic from your old JS config has translated correctly.
We'll run the first v4 build. This is where most errors appear:
The migration script generates a tailwind-deprecated-report.md with all of the deprecated and removed classes. We will:
The migration tool creates a backup of your old web/tailwind folder and starts tailwind-source.css from a clean slate. Your custom classes will not be moved automatically.
With this update, you get support for using Design Tokens with Tailwind v4. If time permits, we will explore options based on the Hyvä Design Token Docs. We will use the Simple Tokens format, as it doesn't require design tools like Figma and the syntax is very similar to what you know from the tailwind.config.js.
Checkout Integration
Get hands-on with checkout integration(s) and learn how to create an integration with an example API
You know all about Hyvä Theme and you have a client who has a shop with Hyvä Checkout with B2B functionality where B2B customers can buy on purchase orders. However these orders need to be approved or within spending limits according to the rules.
Welcome to Paradise.
This workshop will introduce you to checkout integrations and how to go about them
We will cover various aspects of the Hyvä Checkout process:
Be sure to have the following ready before the session starts:
The Session starts with a small introduction and then we will get started. If you have code and you have questions we will be happy to help.