Skip to content

Commit 247fdfc

Browse files
github-actions[bot]shopwareBot
andauthored
[create-pull-request] automated change (#1298)
Co-authored-by: shopwareBot <example@example.com>
1 parent c3d3015 commit 247fdfc

File tree

2 files changed

+221
-41
lines changed

2 files changed

+221
-41
lines changed

resources/references/adr/2023-02-01-app-script-product-pricing.md

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,53 +15,57 @@ You can find the original version [here](https://github.com/shopware/shopware/bl
1515
## Context
1616
We want to provide the opportunity to manipulate the price of a product inside the cart and within the store.
1717
For the cart manipulation we already have a hook integrated which allows accessing and manipulating the cart.
18-
Right now we are not allowing to manipulate prices directly just over creating discounts or new price objects and add them as new line items into the cart.
18+
Right now we are not allowing to manipulate prices directly but just creating discounts or new price objects and add them as new line items into the cart.
1919

20-
But there are different business cases which requires a directly price manipulation like `get an sample of the product for free`
20+
However, there are different business cases which require a direct price manipulation like `get a sample of the product for free`
2121

2222
The following code can be used for manipulating the prices in the product-pricing hook:
2323

2424
```php
25-
{# allow resetting product prices #}
26-
{% do product.calculatedCheapestPrice.reset %}
27-
{% do product.calculatedPrices.reset %}
28-
{# not allowed to RESET the default price otherwise it is not more valid
2925

30-
{# get control of the default price calculation #}
31-
{% set price = services.prices.create({
32-
'default': { 'gross': 20, 'net': 20 },
33-
'USD': { 'gross': 15, 'net': 15 },
34-
'EUR': { 'gross': 10, 'net': 10 }
35-
}) %}
36-
37-
{# directly changes the price to a fix value #}
38-
{% do product.calculatedPrice.change(price) %}
39-
40-
{# manipulate the price and subtract the provided price object #}
41-
{% do product.calculatedPrice.minus(price) %}
42-
43-
{# manipulate the price and add the provided price object #}
44-
{% do product.calculatedPrice.plus(price) %}
45-
46-
{# the following examples show how to deal with percentage manipulation #}
47-
{% do product.calculatedPrice.discount(10) %}
48-
{% do product.calculatedPrice.surcharge(10) %}
49-
50-
{# get control of graduated prices #}
51-
{% do product.calculatedPrices.reset %}
52-
{% do product.calculatedPrices.change([
53-
{ to: 20, price: services.prices.create({ 'default': { 'gross': 15, 'net': 15} }) },
54-
{ to: 30, price: services.prices.create({ 'default': { 'gross': 10, 'net': 10} }) },
55-
{ to: null, price: services.prices.create({ 'default': { 'gross': 5, 'net': 5} }) },
56-
]) %}
57-
58-
{# after hook => walk through prices and fix "from/to" values #}
59-
60-
{% do product.calculatedCheapestPrice.change(price) %}
61-
{% do product.calculatedCheapestPrice.minus(price) %}
62-
{% do product.calculatedCheapestPrice.plus(price) %}
63-
{% do product.calculatedCheapestPrice.discount(10) %}
64-
{% do product.calculatedCheapestPrice.surcharge(10) %}
26+
{% foreach hook.products as product %}
27+
{# allow resetting product prices #}
28+
{% do product.calculatedCheapestPrice.reset %}
29+
{% do product.calculatedPrices.reset %}
30+
{# not allowed to RESET the default price otherwise it is not more valid
31+
32+
{# get control of the default price calculation #}
33+
{% set price = services.prices.create({
34+
'default': { 'gross': 20, 'net': 20 },
35+
'USD': { 'gross': 15, 'net': 15 },
36+
'EUR': { 'gross': 10, 'net': 10 }
37+
}) %}
38+
39+
{# directly changes the price to a fix value #}
40+
{% do product.calculatedPrice.change(price) %}
41+
42+
{# manipulate the price and subtract the provided price object #}
43+
{% do product.calculatedPrice.minus(price) %}
44+
45+
{# manipulate the price and add the provided price object #}
46+
{% do product.calculatedPrice.plus(price) %}
47+
48+
{# the following examples show how to deal with percentage manipulation #}
49+
{% do product.calculatedPrice.discount(10) %}
50+
{% do product.calculatedPrice.surcharge(10) %}
51+
52+
{# get control of graduated prices #}
53+
{% do product.calculatedPrices.reset %}
54+
{% do product.calculatedPrices.change([
55+
{ to: 20, price: services.prices.create({ 'default': { 'gross': 15, 'net': 15} }) },
56+
{ to: 30, price: services.prices.create({ 'default': { 'gross': 10, 'net': 10} }) },
57+
{ to: null, price: services.prices.create({ 'default': { 'gross': 5, 'net': 5} }) },
58+
]) %}
59+
60+
{# after hook => walk through prices and fix "from/to" values #}
61+
62+
{% do product.calculatedCheapestPrice.change(price) %}
63+
{% do product.calculatedCheapestPrice.minus(price) %}
64+
{% do product.calculatedCheapestPrice.plus(price) %}
65+
{% do product.calculatedCheapestPrice.discount(10) %}
66+
{% do product.calculatedCheapestPrice.surcharge(10) %}
67+
68+
{% endforeach %}
6569
```
6670

6771
The following code can be used to manipulate the prices of a product inside the cart:
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
---
2+
title: Make feature flags toggleable on demand
3+
date: 2023-11-29
4+
area: core, administration, storefront
5+
tags: [core, feature, experimental]
6+
---
7+
8+
# Make feature flags toggleable on demand
9+
10+
::: info
11+
This document represents an architecture decision record (ADR) and has been mirrored from the ADR section in our Shopware 6 repository.
12+
You can find the original version [here](https://github.com/shopware/shopware/blob/trunk/adr/2023-11-29-toggle-feature-flag-on-demand.md)
13+
:::
14+
15+
## Context
16+
17+
Feature flags are a great way to enable/disable features in the application. However currently, they are not toggleable on demand. This means that if you want to enable a feature flag, you need to change the environment variables and restart the application. This is not ideal for a production environment.
18+
19+
## Decision
20+
21+
### Store feature flags in the database
22+
23+
The available features are currently stored in the `feature.yaml` static file and toggleable via environment variables. We want to provide a way, that we can toggle this feature flags also via database and provide an UI for the shop merchant.
24+
25+
#### Example feature flag configuration in `app_config`
26+
27+
| key | value |
28+
|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
29+
| feature.flags | <pre>{ <br/> EXAMPLE_FEATURE_1:{ name: EXAMPLE_FEATURE_1, default: true, active: true<br/> EXAMPLE_FEATURE_2:{ name: EXAMPLE_FEATURE_2, default: true, active: false<br/>}</pre> |
30+
31+
All activated feature flags should be registered on `Framework::boot` via `FeatureFlagRegistry::register`:
32+
33+
```php
34+
class Framework extends Bundle
35+
public function boot(): void
36+
{
37+
...
38+
$featureFlagRegistry = $this->container->get(FeatureFlagRegistry::class);
39+
$featureFlagRegistry->register();
40+
}
41+
```
42+
43+
`FeatureFlagRegistry::registry`: in this public method, we merge the static feature flags from `feature.yaml` with the stored feature flags from the database, we then activate the feature flags which are marked as active.
44+
45+
```php
46+
class FeatureFlagRegistry
47+
{
48+
public function registry(): void
49+
{
50+
$static = $this->featureFlags;
51+
$stored = $this->keyValueStorage->get(self::STORAGE_KEY, []);
52+
53+
if (!empty($stored) && \is_string($stored)) {
54+
$stored = \json_decode($stored, true, 512, \JSON_THROW_ON_ERROR);
55+
}
56+
57+
// Major feature flags cannot be toggled with stored flags
58+
$stored = array_filter($stored, static function (array $flag) {
59+
return !\array_key_exists('major', $flag) || !$flag['major'];
60+
});
61+
62+
$flags = array_merge($static, $stored);
63+
64+
Feature::registerFeatures($flags);
65+
}
66+
}
67+
```
68+
69+
### Toggle feature flags on demand
70+
71+
We introduce new admin APIs so we can either activate/deactivate the feature flags.
72+
**Note:** We should only allow toggling feature flags which is not major.
73+
74+
#### Admin API
75+
76+
```php
77+
class FeatureFlagController extends AbstractController
78+
{
79+
#[Route("/api/_action/feature-flag/enable/{feature}", name="api.action.feature-flag.toggle", methods={"POST"})]
80+
public function enable(string $feature, Request $request): JsonResponse
81+
{
82+
$this->featureFlagRegistry->enable($feature);
83+
84+
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
85+
}
86+
87+
#[Route("/api/_action/feature-flag/disable/{feature}", name="api.action.feature-flag.toggle", methods={"POST"})]
88+
public function disable(string $feature, Request $request): JsonResponse
89+
{
90+
$this->featureFlagRegistry->disable($feature);
91+
92+
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
93+
}
94+
95+
#[Route("/api/_action/feature-flag", name="api.action.feature-flag.load", methods={"GET"})]
96+
public function load(Request $request): JsonResponse
97+
{
98+
$featureFlags = Feature::getRegisteredFeatures();
99+
100+
return new JsonResponse($featureFlags);
101+
}
102+
}
103+
```
104+
105+
`FeatureFlagRegistry::enable` & `disable` methods: in these public methods, we enable feature flags and store the new state in the database. We also dispatch an event `BeforeFeatureFlagToggleEvent` before toggling the feature flag and `FeatureFlagToggledEvent` after toggling the feature flag. This is helpful for plugins to listen to these events and do some actions before/after toggling the feature flag
106+
107+
```php
108+
class FeatureFlagRegistry
109+
{
110+
private function enable(string $feature, bool $active): void
111+
{
112+
$registeredFlags = Feature::getRegisteredFeatures();
113+
114+
if (!array_key_exists($feature, $registeredFlags)) {
115+
return;
116+
}
117+
118+
if ($registeredFlags[$feature]['major'] === 'true') {
119+
// cannot toggle major feature flags
120+
return;
121+
}
122+
123+
$registeredFlags[$feature] = [
124+
'active' => $active, // mark the flag as activated or deactivated
125+
'static' => array_key_exists($feature, $this->staticFlags), // check if the flag is static
126+
...$registeredFlags[$feature],
127+
];
128+
129+
$this->dispatcher->dispatch(new BeforeFeatureFlagToggleEvent($feature, $active));
130+
131+
$this->keyValueStorage->set(self::STORAGE_KEY, $registeredFlags);
132+
Feature::toggle($feature, $active);
133+
134+
$this->dispatcher->dispatch(new FeatureFlagToggledEvent($feature, $active));
135+
}
136+
}
137+
```
138+
139+
#### CLI
140+
141+
We can also toggle the feature flags via CLI
142+
143+
```script
144+
// to enable the feature FEATURE_EXAMPLE
145+
bin/console feature:enable FEATURE_EXAMPLE
146+
147+
// to disable the feature FEATURE_EXAMPLE
148+
bin/console feature:disable FEATURE_EXAMPLE
149+
150+
// to list all registered feature flags
151+
bin/console feature:list
152+
```
153+
154+
## Consequences
155+
156+
### Ecosystem
157+
158+
- Before this, Feature flag system was mostly considered as an internal dev-only tool, it's used to hide major breaks or performance boost.
159+
- Now it elevates to be a place where we can introduce new features and hide them behind feature flags. This will allow us to delivery new features even at experimental/beta phase and try them in production on demand without affecting the shop merchants
160+
- But this should not be abused, we could only use the toggle for experimental/beta features and not for major features
161+
162+
### Commercial plans
163+
164+
- For commercial licenses, each license's feature should be treated as a feature flag. This way, we can enable/disable features for each license if it's available in the license
165+
166+
### Shop merchants
167+
168+
- For shop merchants, they can use the new toggle feature flags API to enable/disable features on demand, this will override the environment variables if the feature flag is available in the database. We can also add a new admin module or an app to allow shop merchants to toggle feature flags on demand or list all available feature flags via new admin APIs
169+
170+
### Developers
171+
172+
- For internal devs, they can utilize the tool to quickly delivery new experimental/beta features. However, it's important that this should not be a tool to reach deadlines or release "crap". We should still follow standards and guidelines.
173+
- External plugins can also add their own feature flags by adding them to the `feature.flags` key in the key value storage (e.g. `app_config` table if using the default key value storage)
174+
- Feature flags can be toggled via CLI using `bin/console feature:enable <feature>` or `bin/console feature:disable <feature>` this is helpful for testing purposes and for CI/CD pipelines
175+
- We can also add a new CLI command to list all available feature flags and their status using `bin/console feature:list`
176+
- When a feature flag is toggled at run time, we dispatch an event `BeforeFeatureFlagToggleEvent` before toggling the feature flag and `FeatureFlagToggledEvent` after toggling the feature flag. This is helpful for plugins to listen to these events and do some actions before/after toggling the feature flag

0 commit comments

Comments
 (0)