From ed5826128e314b0bff17cac2ee7753ac1923c6e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Fri, 25 Sep 2026 16:15:02 +0200 Subject: [PATCH] feat: put the access codes behind a feature flag --- .env | 11 ++ .env.dev | 3 + .env.test | 4 + composer.json | 1 + composer.lock | 135 +++++++++++++++++- config/bundles.php | 1 + config/packages/flagception.yaml | 10 ++ config/reference.php | 59 ++++++++ src/Controller/AccessCode/IndexController.php | 2 + .../Admin/AccessCode/CreateController.php | 2 + .../Admin/AccessCode/DeleteController.php | 2 + .../Admin/AccessCode/EditController.php | 2 + .../Admin/AccessCode/IndexController.php | 2 + symfony.lock | 12 ++ templates/dashboard/index.html.twig | 19 +-- 15 files changed, 256 insertions(+), 9 deletions(-) create mode 100644 config/packages/flagception.yaml diff --git a/.env b/.env index 51dea81..dc01332 100644 --- a/.env +++ b/.env @@ -77,3 +77,14 @@ EP_OAUTH2_URL_ACCESS_TOKEN=https://myep.ddev.site/token EP_OAUTH2_URL_USERINFO=https://myep.ddev.site/api/userinfo EP_OAUTH2_SCOPES=email,profile,roles ###< app/myep-oauth2 ### + +###> app/features ### +# Feature flags, read by config/packages/flagception.yaml. Off here and switched on per +# deployment in .env.local, so a feature reaches production when its environment says so rather +# than when it is merged. +# +# Door, lift and barrier codes under /access-codes and their admin table. Off until the codes +# have actually been entered: an empty list of codes is worse than no menu entry, because it +# reads as "there are none" to somebody standing at the door. +FEATURE_ACCESS_CODES=false +###< app/features ### diff --git a/.env.dev b/.env.dev index a5084c0..0a05452 100644 --- a/.env.dev +++ b/.env.dev @@ -8,3 +8,6 @@ APP_SECRET=44660e8fcd421376ebe0f7a7d55e179c # ddev serves the application under this name; anything else is refused. SYMFONY_TRUSTED_HOSTS='^ephub\.ddev\.site$' + +# On for local development, so a fresh checkout sees the feature the branch is about. +FEATURE_ACCESS_CODES=true diff --git a/.env.test b/.env.test index 3ae2840..17d38a6 100644 --- a/.env.test +++ b/.env.test @@ -3,3 +3,7 @@ KERNEL_CLASS='App\Kernel' # Fixed and committed so the test suite is reproducible, and never served: .env.test is excluded # from the deploy rsync and production takes APP_SECRET from its own uncommitted .env.local. APP_SECRET='$ecretf0rt3st' + +# The access-code suite tests the feature, not the gate in front of it, so it runs with the flag +# on. +FEATURE_ACCESS_CODES=true diff --git a/composer.json b/composer.json index aa0c3ad..cd8268e 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "doctrine/doctrine-bundle": "^3.3", "doctrine/doctrine-migrations-bundle": "^4.0", "doctrine/orm": "^3.7", + "flagception/flagception-bundle": "^6.0", "knplabs/knp-paginator-bundle": "^6.10", "league/oauth2-client": "^2.9", "phpdocumentor/reflection-docblock": "^6.0", diff --git a/composer.lock b/composer.lock index 24eca64..bb99691 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5651480dee3b63391faefc9646fe4a95", + "content-hash": "2aef6c416eb2231831a8f94f753f2aa3", "packages": [ { "name": "composer/semver", @@ -1269,6 +1269,139 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "flagception/flagception", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/playox/flagception-sdk.git", + "reference": "6934e73ea1ab43611a43bf85922349a83561177b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/playox/flagception-sdk/zipball/6934e73ea1ab43611a43bf85922349a83561177b", + "reference": "6934e73ea1ab43611a43bf85922349a83561177b", + "shasum": "" + }, + "require": { + "php": "^7.2||^8.0", + "psr/cache": "^1.0||^2.0||^3.0", + "symfony/expression-language": ">=2.7" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.4.3", + "phpunit/phpunit": "*", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "flagception/database-activator": "Fetch feature flags from database." + }, + "type": "lib", + "autoload": { + "psr-4": { + "Flagception\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michel Chowanski", + "email": "michel.chowanski@office-partner.de" + } + ], + "description": "Feature toggle on steroids.", + "keywords": [ + "feature", + "feature-flags", + "feature-toggle", + "flagception", + "flags", + "rollout", + "testing", + "toggle" + ], + "support": { + "issues": "https://github.com/playox/flagception-sdk/issues", + "source": "https://github.com/playox/flagception-sdk/tree/2.0.0" + }, + "time": "2025-02-26T16:11:43+00:00" + }, + { + "name": "flagception/flagception-bundle", + "version": "6.1.1", + "source": { + "type": "git", + "url": "https://github.com/playox/flagception-bundle.git", + "reference": "48f8fd0c8386aa3f4e66ba7882e24dc3ffcf8997" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/playox/flagception-bundle/zipball/48f8fd0c8386aa3f4e66ba7882e24dc3ffcf8997", + "reference": "48f8fd0c8386aa3f4e66ba7882e24dc3ffcf8997", + "shasum": "" + }, + "require": { + "flagception/flagception": "^2.0", + "php": "^8.0", + "symfony/config": "^4.4 | ^5.0 | ^6.0 | ^7.0 | ^8.0", + "symfony/dependency-injection": "^4.4 | ^5.0 | ^6.0 | ^7.0 | ^8.0", + "symfony/http-kernel": "^4.4 | ^5.0 | ^6.0 | ^7.0 | ^8.0", + "symfony/yaml": "^4.4 | ^5.0 | ^6.0 | ^7.0 | ^8.0", + "twig/twig": "^1.18|^2.0|^3.0" + }, + "require-dev": { + "flagception/database-activator": "^2.0", + "php-coveralls/php-coveralls": "^2.0", + "squizlabs/php_codesniffer": "^3.3.1", + "symfony/phpunit-bridge": "^5.0 | ^6.0 | ^7.0 | ^8.0", + "symfony/twig-bridge": "^4.4 | ^5.0 | ^6.0 | ^7.0 | ^8.0" + }, + "suggest": { + "flagception/database-activator": "Fetch feature flags from a database." + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "url": "https://github.com/playox/flagception-sdk", + "name": "flagception/flagception" + } + }, + "autoload": { + "psr-4": { + "Flagception\\Bundle\\FlagceptionBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michel Chowanski", + "email": "michel.chowanski@bestit-online.de" + } + ], + "description": "Feature toggle bundle on steroids.", + "keywords": [ + "bundle", + "feature", + "feature-flags", + "feature-toggle", + "flagception", + "flags", + "rollout", + "symfony", + "toggle" + ], + "support": { + "issues": "https://github.com/playox/flagception-bundle/issues", + "source": "https://github.com/playox/flagception-bundle/tree/6.1.1" + }, + "time": "2026-06-01T10:02:14+00:00" + }, { "name": "guzzlehttp/guzzle", "version": "7.15.5", diff --git a/config/bundles.php b/config/bundles.php index 8f204de..76477b7 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -14,4 +14,5 @@ return [ Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Symfony\UX\Icons\UXIconsBundle::class => ['all' => true], Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true], + Flagception\Bundle\FlagceptionBundle\FlagceptionBundle::class => ['all' => true], ]; diff --git a/config/packages/flagception.yaml b/config/packages/flagception.yaml new file mode 100644 index 0000000..2951d65 --- /dev/null +++ b/config/packages/flagception.yaml @@ -0,0 +1,10 @@ +# See full configuration at https://github.com/bestit/flagception-bundle +# +# Features are off unless an environment says otherwise: a deployment that knows nothing about a +# flag gets the state the feature had before it existed. Switching one on is an .env change, not +# a release. +flagception: + features: + access_codes: + default: false + env: FEATURE_ACCESS_CODES diff --git a/config/reference.php b/config/reference.php index 17bad17..85fb803 100644 --- a/config/reference.php +++ b/config/reference.php @@ -1513,6 +1513,61 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * convert_exception?: bool|Param, // Default: false * remove_first_page_param?: bool|Param, // Default: false * } + * @psalm-type FlagceptionConfig = array{ + * features?: array, + * routing_metadata?: array{ + * enable?: bool|Param, // Default: true + * }, + * activators?: array{ + * array?: array{ + * enable?: bool|Param, // Default: true + * priority?: int|Param, // Default: 255 + * }, + * constraint?: array{ + * enable?: bool|Param, // Default: true + * priority?: int|Param, // Default: 210 + * }, + * environment?: array{ + * enable?: bool|Param, // Default: true + * priority?: int|Param, // Default: 230 + * }, + * cookie?: array{ + * enable?: bool|Param, // Default: false + * priority?: int|Param, // Default: 200 + * name?: scalar|Param|null, // Default: "flagception" + * separator?: scalar|Param|null, // Default: "," + * mode?: "whitelist"|"blacklist"|Param, // Default: "whitelist" + * }, + * database?: array{ + * enable?: bool|Param, // Default: false + * priority?: int|Param, // Default: 220 + * url?: scalar|Param|null, // Connection string for the database + * dbal?: scalar|Param|null, // Service with dbal instance + * credentials?: array{ + * dbname?: scalar|Param|null, + * user?: scalar|Param|null, + * password?: scalar|Param|null, + * host?: scalar|Param|null, + * driver?: scalar|Param|null, + * }, + * options?: array{ + * db_table?: scalar|Param|null, // Default: "flagception_features" + * db_column_feature?: scalar|Param|null, // Default: "feature" + * db_column_state?: scalar|Param|null, // Default: "state" + * }, + * cache?: array{ + * enable?: bool|Param, // Default: false + * pool?: scalar|Param|null, // Default: "cache.app" + * lifetime?: int|Param, // Default: 3600 + * }, + * }, + * }, + * } * @psalm-type ConfigType = array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -1527,6 +1582,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * monolog?: MonologConfig, * ux_icons?: UxIconsConfig, * knp_paginator?: KnpPaginatorConfig, + * flagception?: FlagceptionConfig, * "when@dev"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -1544,6 +1600,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * maker?: MakerConfig, * ux_icons?: UxIconsConfig, * knp_paginator?: KnpPaginatorConfig, + * flagception?: FlagceptionConfig, * }, * "when@prod"?: array{ * imports?: ImportsConfig, @@ -1559,6 +1616,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * monolog?: MonologConfig, * ux_icons?: UxIconsConfig, * knp_paginator?: KnpPaginatorConfig, + * flagception?: FlagceptionConfig, * }, * "when@test"?: array{ * imports?: ImportsConfig, @@ -1575,6 +1633,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * monolog?: MonologConfig, * ux_icons?: UxIconsConfig, * knp_paginator?: KnpPaginatorConfig, + * flagception?: FlagceptionConfig, * }, * ...Alle Buchungen verwalten.

- -
- {{ ux_icon('lucide:key-round', { class: 'h-6 w-6 shrink-0 text-primary' }) }} - Zugangscodes -
-

Codes für Türen, Aufzug und Schranke.

-
+ {# The route 404s while the feature is off, so the card has to go with it. #} + {% if feature('access_codes') %} + +
+ {{ ux_icon('lucide:key-round', { class: 'h-6 w-6 shrink-0 text-primary' }) }} + Zugangscodes +
+

Codes für Türen, Aufzug und Schranke.

+
+ {% endif %} {# The header is deliberately not navigation, so the dashboard is the only place an