diff --git a/.env.test b/.env.test
new file mode 100644
index 0000000..9e7162f
--- /dev/null
+++ b/.env.test
@@ -0,0 +1,6 @@
+# define your env variables for the test env here
+KERNEL_CLASS='App\Kernel'
+APP_SECRET='$ecretf0rt3st'
+SYMFONY_DEPRECATIONS_HELPER=999999
+PANTHER_APP_ENV=panther
+PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml
index 3711f76..da0de28 100644
--- a/.github/workflows/pr.yaml
+++ b/.github/workflows/pr.yaml
@@ -290,3 +290,17 @@ jobs:
docker network create frontend
docker compose run --rm --user root phpfpm composer install --no-scripts
docker compose run --rm --user root phpfpm bin/console biomejs:ci .
+
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ name: Run tests
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Run tests
+ run: |
+ docker network create frontend
+ docker compose run --rm --user root phpfpm composer install --no-scripts
+ docker compose run --rm --user root phpfpm bin/phpunit
diff --git a/.gitignore b/.gitignore
index 71192e8..8ea87c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,8 @@
/public/assets/
/assets/vendor/
###< symfony/asset-mapper ###
+
+###> symfony/phpunit-bridge ###
+.phpunit.result.cache
+/phpunit.xml
+###< symfony/phpunit-bridge ###
diff --git a/Taskfile.yml b/Taskfile.yml
index 503c480..0a63711 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -148,3 +148,9 @@ tasks:
cmds:
- task composer -- update-api-spec
silent: true
+
+ test:
+ desc: "Run tests"
+ cmds:
+ - task compose -- exec phpfpm bin/phpunit {{.CLI_ARGS}}
+ silent: true
diff --git a/bin/phpunit b/bin/phpunit
new file mode 100755
index 0000000..692bacc
--- /dev/null
+++ b/bin/phpunit
@@ -0,0 +1,23 @@
+#!/usr/bin/env php
+= 80000) {
+ require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
+ } else {
+ define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
+ require PHPUNIT_COMPOSER_INSTALL;
+ PHPUnit\TextUI\Command::main();
+ }
+} else {
+ if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
+ echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
+ exit(1);
+ }
+
+ require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
+}
diff --git a/composer.json b/composer.json
index b1dc29c..b83f4ba 100644
--- a/composer.json
+++ b/composer.json
@@ -52,8 +52,11 @@
"friendsofphp/php-cs-fixer": "^3.20",
"hautelook/alice-bundle": "^2.12",
"kocal/biome-js-bundle": "^1.3",
+ "symfony/browser-kit": "7.2.*",
+ "symfony/css-selector": "7.2.*",
"symfony/debug-bundle": "~7.2.0",
"symfony/maker-bundle": "*",
+ "symfony/phpunit-bridge": "^7.2",
"symfony/stopwatch": "~7.2.0",
"symfony/web-profiler-bundle": "~7.2.0",
"vincentlanglet/twig-cs-fixer": "^3.3",
@@ -84,12 +87,15 @@
}
},
"config": {
+ "//": "See https://github.com/api-platform/api-platform/issues/2437#issuecomment-1540620564 for details on why prepend-autoloader is set",
"allow-plugins": {
"ergebnis/composer-normalize": true,
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
+ "optimize-autoloader": true,
+ "prepend-autoloader": false,
"sort-packages": true
},
"extra": {
diff --git a/composer.lock b/composer.lock
index a5e2c17..7ddd74f 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": "dacda89ecd6218395778c1a4327c84ac",
+ "content-hash": "361ae6436338b139c17ec6223c8b1189",
"packages": [
{
"name": "amphp/amp",
@@ -168,16 +168,16 @@
},
{
"name": "api-platform/core",
- "version": "v3.4.8",
+ "version": "v3.4.9",
"source": {
"type": "git",
"url": "https://github.com/api-platform/core.git",
- "reference": "985a9a0408cfc31721adbe43c6ae38d9c3a8c88f"
+ "reference": "535bbf57a7fc00ba0f8d05261050d90b3cbad987"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/core/zipball/985a9a0408cfc31721adbe43c6ae38d9c3a8c88f",
- "reference": "985a9a0408cfc31721adbe43c6ae38d9c3a8c88f",
+ "url": "https://api.github.com/repos/api-platform/core/zipball/535bbf57a7fc00ba0f8d05261050d90b3cbad987",
+ "reference": "535bbf57a7fc00ba0f8d05261050d90b3cbad987",
"shasum": ""
},
"require": {
@@ -383,9 +383,9 @@
],
"support": {
"issues": "https://github.com/api-platform/core/issues",
- "source": "https://github.com/api-platform/core/tree/v3.4.8"
+ "source": "https://github.com/api-platform/core/tree/v3.4.9"
},
- "time": "2024-12-06T11:11:33+00:00"
+ "time": "2024-12-13T14:29:05+00:00"
},
{
"name": "behat/transliterator",
@@ -1849,16 +1849,16 @@
},
{
"name": "doctrine/orm",
- "version": "2.20.0",
+ "version": "2.20.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/orm.git",
- "reference": "8ed6c2234aba019f9737a6bcc9516438e62da27c"
+ "reference": "e3cabade99ebccc6ba078884c1c5f250866a494e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/orm/zipball/8ed6c2234aba019f9737a6bcc9516438e62da27c",
- "reference": "8ed6c2234aba019f9737a6bcc9516438e62da27c",
+ "url": "https://api.github.com/repos/doctrine/orm/zipball/e3cabade99ebccc6ba078884c1c5f250866a494e",
+ "reference": "e3cabade99ebccc6ba078884c1c5f250866a494e",
"shasum": ""
},
"require": {
@@ -1888,15 +1888,14 @@
"doctrine/coding-standard": "^9.0.2 || ^12.0",
"phpbench/phpbench": "^0.16.10 || ^1.0",
"phpstan/extension-installer": "~1.1.0 || ^1.4",
- "phpstan/phpstan": "~1.4.10 || 1.12.6",
- "phpstan/phpstan-deprecation-rules": "^1",
+ "phpstan/phpstan": "~1.4.10 || 2.0.3",
+ "phpstan/phpstan-deprecation-rules": "^1 || ^2",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.7.2",
"symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0",
"symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0",
- "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
- "vimeo/psalm": "4.30.0 || 5.24.0"
+ "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"suggest": {
"ext-dom": "Provides support for XSD validation for XML mapping files",
@@ -1946,9 +1945,9 @@
],
"support": {
"issues": "https://github.com/doctrine/orm/issues",
- "source": "https://github.com/doctrine/orm/tree/2.20.0"
+ "source": "https://github.com/doctrine/orm/tree/2.20.1"
},
- "time": "2024-10-11T11:47:24+00:00"
+ "time": "2024-12-19T06:48:36+00:00"
},
{
"name": "doctrine/persistence",
@@ -2651,9 +2650,9 @@
"type": "symfony-bundle",
"extra": {
"symfony": {
- "allow-contrib": false,
+ "docker": false,
"require": "~6.4|~7.0",
- "docker": false
+ "allow-contrib": false
}
},
"autoload": {
@@ -3860,16 +3859,16 @@
},
{
"name": "spatie/array-to-xml",
- "version": "3.3.0",
+ "version": "3.4.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/array-to-xml.git",
- "reference": "f56b220fe2db1ade4c88098d83413ebdfc3bf876"
+ "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/f56b220fe2db1ade4c88098d83413ebdfc3bf876",
- "reference": "f56b220fe2db1ade4c88098d83413ebdfc3bf876",
+ "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67",
+ "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67",
"shasum": ""
},
"require": {
@@ -3912,7 +3911,7 @@
"xml"
],
"support": {
- "source": "https://github.com/spatie/array-to-xml/tree/3.3.0"
+ "source": "https://github.com/spatie/array-to-xml/tree/3.4.0"
},
"funding": [
{
@@ -3924,7 +3923,7 @@
"type": "github"
}
],
- "time": "2024-05-01T10:20:27+00:00"
+ "time": "2024-12-16T12:45:15+00:00"
},
{
"name": "stof/doctrine-extensions-bundle",
@@ -6654,8 +6653,8 @@
"type": "library",
"extra": {
"thanks": {
- "name": "symfony/polyfill",
- "url": "https://github.com/symfony/polyfill"
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
}
},
"autoload": {
@@ -10939,6 +10938,73 @@
},
"time": "2024-11-28T04:54:44+00:00"
},
+ {
+ "name": "masterminds/html5",
+ "version": "2.9.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Masterminds/html5-php.git",
+ "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6",
+ "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Masterminds\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Matt Butcher",
+ "email": "technosophos@gmail.com"
+ },
+ {
+ "name": "Matt Farina",
+ "email": "matt@mattfarina.com"
+ },
+ {
+ "name": "Asmir Mustafic",
+ "email": "goetas@gmail.com"
+ }
+ ],
+ "description": "An HTML5 parser and serializer.",
+ "homepage": "http://masterminds.github.io/html5-php",
+ "keywords": [
+ "HTML5",
+ "dom",
+ "html",
+ "parser",
+ "querypath",
+ "serializer",
+ "xml"
+ ],
+ "support": {
+ "issues": "https://github.com/Masterminds/html5-php/issues",
+ "source": "https://github.com/Masterminds/html5-php/tree/2.9.0"
+ },
+ "time": "2024-03-31T07:05:07+00:00"
+ },
{
"name": "myclabs/deep-copy",
"version": "1.12.1",
@@ -11843,6 +11909,139 @@
],
"time": "2024-07-03T05:10:34+00:00"
},
+ {
+ "name": "symfony/browser-kit",
+ "version": "v7.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/browser-kit.git",
+ "reference": "8d64d17e198082f8f198d023a6b634e7b5fdda94"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/browser-kit/zipball/8d64d17e198082f8f198d023a6b634e7b5fdda94",
+ "reference": "8d64d17e198082f8f198d023a6b634e7b5fdda94",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/dom-crawler": "^6.4|^7.0"
+ },
+ "require-dev": {
+ "symfony/css-selector": "^6.4|^7.0",
+ "symfony/http-client": "^6.4|^7.0",
+ "symfony/mime": "^6.4|^7.0",
+ "symfony/process": "^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\BrowserKit\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/browser-kit/tree/v7.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-10-25T15:15:23+00:00"
+ },
+ {
+ "name": "symfony/css-selector",
+ "version": "v7.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/css-selector.git",
+ "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2",
+ "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\CssSelector\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Jean-François Simon",
+ "email": "jeanfrancois.simon@sensiolabs.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Converts CSS selectors to XPath expressions",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/css-selector/tree/v7.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:21:43+00:00"
+ },
{
"name": "symfony/debug-bundle",
"version": "v7.2.0",
@@ -11917,6 +12116,73 @@
],
"time": "2024-09-25T14:21:43+00:00"
},
+ {
+ "name": "symfony/dom-crawler",
+ "version": "v7.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/dom-crawler.git",
+ "reference": "b176e1f1f550ef44c94eb971bf92488de08f7c6b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/b176e1f1f550ef44c94eb971bf92488de08f7c6b",
+ "reference": "b176e1f1f550ef44c94eb971bf92488de08f7c6b",
+ "shasum": ""
+ },
+ "require": {
+ "masterminds/html5": "^2.6",
+ "php": ">=8.2",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "require-dev": {
+ "symfony/css-selector": "^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\DomCrawler\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Eases DOM navigation for HTML and XML documents",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/dom-crawler/tree/v7.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-11-13T16:15:23+00:00"
+ },
{
"name": "symfony/maker-bundle",
"version": "v1.61.0",
@@ -12009,6 +12275,88 @@
],
"time": "2024-08-29T22:50:23+00:00"
},
+ {
+ "name": "symfony/phpunit-bridge",
+ "version": "v7.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/phpunit-bridge.git",
+ "reference": "2bbde92ab25a0e2c88160857af7be9db5da0d145"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/2bbde92ab25a0e2c88160857af7be9db5da0d145",
+ "reference": "2bbde92ab25a0e2c88160857af7be9db5da0d145",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<7.5|9.1.2"
+ },
+ "require-dev": {
+ "symfony/deprecation-contracts": "^2.5|^3.0",
+ "symfony/error-handler": "^5.4|^6.4|^7.0",
+ "symfony/polyfill-php81": "^1.27"
+ },
+ "bin": [
+ "bin/simple-phpunit"
+ ],
+ "type": "symfony-bridge",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/sebastianbergmann/phpunit",
+ "name": "phpunit/phpunit"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Bridge\\PhpUnit\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/",
+ "/bin/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides utilities for PHPUnit, especially user deprecation notices management",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/phpunit-bridge/tree/v7.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-11-13T16:15:23+00:00"
+ },
{
"name": "symfony/process",
"version": "v7.2.0",
@@ -12256,16 +12604,16 @@
},
{
"name": "vincentlanglet/twig-cs-fixer",
- "version": "3.4.0",
+ "version": "3.5.0",
"source": {
"type": "git",
"url": "https://github.com/VincentLanglet/Twig-CS-Fixer.git",
- "reference": "a193004602d7a9b1c17408eb8b8a1632532a28a7"
+ "reference": "f81af33e48c384be7e0e3689f02e6e712fa68beb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/VincentLanglet/Twig-CS-Fixer/zipball/a193004602d7a9b1c17408eb8b8a1632532a28a7",
- "reference": "a193004602d7a9b1c17408eb8b8a1632532a28a7",
+ "url": "https://api.github.com/repos/VincentLanglet/Twig-CS-Fixer/zipball/f81af33e48c384be7e0e3689f02e6e712fa68beb",
+ "reference": "f81af33e48c384be7e0e3689f02e6e712fa68beb",
"shasum": ""
},
"require": {
@@ -12324,7 +12672,7 @@
"homepage": "https://github.com/VincentLanglet/Twig-CS-Fixer",
"support": {
"issues": "https://github.com/VincentLanglet/Twig-CS-Fixer/issues",
- "source": "https://github.com/VincentLanglet/Twig-CS-Fixer/tree/3.4.0"
+ "source": "https://github.com/VincentLanglet/Twig-CS-Fixer/tree/3.5.0"
},
"funding": [
{
@@ -12332,7 +12680,7 @@
"type": "github"
}
],
- "time": "2024-12-04T18:37:59+00:00"
+ "time": "2024-12-13T16:55:11+00:00"
},
{
"name": "weirdan/doctrine-psalm-plugin",
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..c76a655
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tests
+
+
+
+
+
+ src
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Admin/Field/ValueWithUnitField.php b/src/Admin/Field/ValueWithUnitField.php
index 5bea65f..f998e39 100644
--- a/src/Admin/Field/ValueWithUnitField.php
+++ b/src/Admin/Field/ValueWithUnitField.php
@@ -3,6 +3,7 @@
namespace App\Admin\Field;
use App\Form\ValueWithUnitType;
+use App\Service\ValueWithUnitHelper;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
use Symfony\Contracts\Translation\TranslatableInterface;
@@ -19,6 +20,7 @@ public static function new(string $propertyName, $label = null): self
return (new self())
->setProperty($propertyName)
->setLabel($label)
+ ->setScale(0)
// this template is used in 'index' and 'detail' pages
->setTemplatePath('admin/field/value_with_unit.html.twig')
@@ -28,4 +30,14 @@ public static function new(string $propertyName, $label = null): self
->setFormType(ValueWithUnitType::class)
->addCssClass('field-value-with-unit');
}
+
+ public function setUnits(array $units): self
+ {
+ return $this->setFormTypeOption(ValueWithUnitHelper::OPTION_UNITS, $units);
+ }
+
+ public function setScale(int $scale): self
+ {
+ return $this->setFormTypeOption(ValueWithUnitHelper::OPTION_SCALE, $scale);
+ }
}
diff --git a/src/Controller/Admin/PointOfInterestCrudController.php b/src/Controller/Admin/PointOfInterestCrudController.php
index 0d9077a..fb0a1ab 100644
--- a/src/Controller/Admin/PointOfInterestCrudController.php
+++ b/src/Controller/Admin/PointOfInterestCrudController.php
@@ -8,8 +8,8 @@
use App\Entity\Role;
use App\Entity\Route;
use App\Field\VichImageField;
-use App\Form\ValueWithUnitType;
use App\Service\EasyAdminHelper;
+use App\Service\ValueWithUnitHelper;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
@@ -94,11 +94,11 @@ public function configureFields(string $pageName): iterable
->setVirtual(true)->setColumns(12);
yield ValueWithUnitField::new('proximityToUnlock', new TranslatableMessage('Proximity to unlock', [], 'admin'))
- ->setFormTypeOption('units', [
+ ->setUnits([
'm' => [
- ValueWithUnitType::OPTION_LABEL => new TranslatableMessage('meter', [], 'admin'),
- ValueWithUnitType::OPTION_SCALE => 1,
- ValueWithUnitType::OPTION_LOCALIZED_UNIT => new TranslatableMessage('unit.m', [], 'admin'),
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => new TranslatableMessage('meter', [], 'admin'),
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => new TranslatableMessage('unit.m', [], 'admin'),
],
])
->setHelp(new TranslatableMessage('The proximity that allows unlocking this point of interest.', [], 'admin'))->setColumns(12);
diff --git a/src/Controller/Admin/RouteCrudController.php b/src/Controller/Admin/RouteCrudController.php
index a354261..acba9e4 100644
--- a/src/Controller/Admin/RouteCrudController.php
+++ b/src/Controller/Admin/RouteCrudController.php
@@ -6,9 +6,9 @@
use App\Entity\Role;
use App\Entity\Route;
use App\Field\VichImageField;
-use App\Form\ValueWithUnitType;
use App\Service\AppManager;
use App\Service\EasyAdminHelper;
+use App\Service\ValueWithUnitHelper;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
@@ -80,32 +80,33 @@ public function configureFields(string $pageName): iterable
}
yield ValueWithUnitField::new('distance', new TranslatableMessage('Distance', [], 'admin'))
- ->setFormTypeOption('units', [
+ ->setScale(1)
+ ->setUnits([
'km' => [
- ValueWithUnitType::OPTION_LABEL => new TranslatableMessage('kilometer', [], 'admin'),
- ValueWithUnitType::OPTION_SCALE => 1000,
- ValueWithUnitType::OPTION_LOCALIZED_UNIT => new TranslatableMessage('unit.km', [], 'admin'),
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => new TranslatableMessage('kilometer', [], 'admin'),
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1000,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => new TranslatableMessage('unit.km', [], 'admin'),
],
'm' => [
- ValueWithUnitType::OPTION_LABEL => new TranslatableMessage('meter', [], 'admin'),
- ValueWithUnitType::OPTION_SCALE => 1,
- ValueWithUnitType::OPTION_LOCALIZED_UNIT => new TranslatableMessage('unit.m', [], 'admin'),
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => new TranslatableMessage('meter', [], 'admin'),
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => new TranslatableMessage('unit.m', [], 'admin'),
],
])
->setColumns(6)
->setHelp(new TranslatableMessage('The total distance of the route with all points of interests included.', [], 'admin'));
yield ValueWithUnitField::new('totalDuration', new TranslatableMessage('Total duration', [], 'admin'))
- ->setFormTypeOption('units', [
+ ->setUnits([
'hour' => [
- ValueWithUnitType::OPTION_LABEL => new TranslatableMessage('hours', [], 'admin'),
- ValueWithUnitType::OPTION_SCALE => 60 * 60,
- ValueWithUnitType::OPTION_LOCALIZED_UNIT => new TranslatableMessage('unit.hour', [], 'admin'),
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => new TranslatableMessage('hours', [], 'admin'),
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 60 * 60,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => new TranslatableMessage('unit.hour', [], 'admin'),
],
'minute' => [
- ValueWithUnitType::OPTION_LABEL => new TranslatableMessage('minutes', [], 'admin'),
- ValueWithUnitType::OPTION_SCALE => 60,
- ValueWithUnitType::OPTION_LOCALIZED_UNIT => new TranslatableMessage('unit.minute', [], 'admin'),
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => new TranslatableMessage('minutes', [], 'admin'),
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 60,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => new TranslatableMessage('unit.minute', [], 'admin'),
],
])
->setColumns(6)
diff --git a/src/Form/ValueWithUnitType.php b/src/Form/ValueWithUnitType.php
index d8bb7b6..5ad9e54 100644
--- a/src/Form/ValueWithUnitType.php
+++ b/src/Form/ValueWithUnitType.php
@@ -2,6 +2,7 @@
namespace App\Form;
+use App\Service\ValueWithUnitHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
@@ -9,11 +10,7 @@
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
-use Symfony\Component\Translation\LocaleSwitcher;
use Symfony\Component\Translation\TranslatableMessage;
-use Symfony\Component\Validator\Constraints\Positive;
-use Symfony\Component\Validator\Validation;
-use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @extends AbstractType
@@ -23,124 +20,62 @@ final class ValueWithUnitType extends AbstractType
public const string FIELD_VALUE = 'value';
public const string FIELD_UNIT = 'unit';
- public const string OPTION_LABEL = 'label';
- public const string OPTION_SCALE = 'scale';
- public const string OPTION_LOCALIZED_UNIT = 'localized_unit';
-
- private const int SCALE = 1;
-
- private \NumberFormatter $numberFormatter;
-
public function __construct(
- private readonly TranslatorInterface $translator,
- LocaleSwitcher $localeSwitcher,
+ private ValueWithUnitHelper $helper,
) {
- $this->numberFormatter = new \NumberFormatter($localeSwitcher->getLocale(), \NumberFormatter::DECIMAL);
- $this->numberFormatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, self::SCALE);
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
- $units = array_keys($options['units']);
- $unitChoices = array_combine($units, $units);
+ $helper = $this->helper->withOptions($options);
+
+ $units = $helper->getUnits();
+ $unitKeys = array_keys($units);
+ $unitChoices = array_combine($unitKeys, $unitKeys);
$builder
->add(self::FIELD_VALUE, NumberType::class, [
- 'scale' => self::SCALE,
+ 'scale' => $helper->getScale(),
])
->add(self::FIELD_UNIT, ChoiceType::class, [
'choices' => $unitChoices,
- 'choice_label' => function ($choice, string $key, mixed $value) use ($options): TranslatableMessage|string {
- return $options['units'][$key]['label'] ?? $key;
+ 'choice_label' => function ($choice, string $key, mixed $value) use ($units): TranslatableMessage|string {
+ return $units[$key][ValueWithUnitHelper::OPTION_UNIT_LABEL] ?? $key;
},
]);
$builder
->addModelTransformer(new CallbackTransformer(
- fn ($value) => $this->transform($value, $options),
- fn ($value) => $this->reverseTransform($value, $options),
- ))
- ;
+ fn (mixed $value) => $this->transform($value, $helper),
+ fn (mixed $value) => $this->reverseTransform($value, $helper),
+ ));
}
- public function transform(?int $value, array $options): array
+ public function transform(?int $value, ValueWithUnitHelper $helper): array
{
try {
- return $this->getMatchingUnit($value, $options);
- } catch (\Exception $exception) {
+ return $helper->getMatchingUnit($value);
+ } catch (\Exception) {
throw new TransformationFailedException(invalidMessage: 'Error transforming value: {value}.', invalidMessageParameters: ['value' => $value, /* @todo Make this work! */ 'translation_domain' => 'admin']);
}
}
- public function reverseTransform(array $values, array $options): int
+ public function reverseTransform(array $values, ValueWithUnitHelper $helper): int
{
[self::FIELD_VALUE => $value, self::FIELD_UNIT => $unit] = $values;
- $units = $this->getUnits($options);
+ $units = $helper->getUnits();
$info = $units[$unit] ?? null;
if (null === $info) {
throw new TransformationFailedException(invalidMessage: 'Invalid unit: {unit}.', invalidMessageParameters: ['unit' => $unit, /* @todo Make this work! */ 'translation_domain' => 'admin']);
}
- return $value * $info[self::OPTION_SCALE];
+ return $value * $info[ValueWithUnitHelper::OPTION_UNIT_FACTOR];
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
- ->setRequired('units')
- ->setAllowedTypes('units', 'array')
- ->setDefault('units', function (OptionsResolver $resolver): void {
- $resolver
- ->setPrototype(true)
- ->setDefault(self::OPTION_SCALE, 1)
- ->setAllowedTypes(self::OPTION_SCALE, 'int')
- ->setAllowedValues(self::OPTION_SCALE, Validation::createIsValidCallable(
- new Positive(),
- ))
- ->setRequired(self::OPTION_LABEL)
- ->setAllowedTypes(self::OPTION_LABEL, ['string', TranslatableMessage::class])
- ->setRequired(self::OPTION_LOCALIZED_UNIT);
- })
- // Make units required.
- ->setAllowedValues('units', static fn (array $value) => !empty($value))
- ;
- }
-
- /**
- * Get units sorted descending by scale.
- */
- private function getUnits(array $options): array
- {
- $units = $options['units'];
-
- uasort($units, static fn ($a, $b) => -($a[self::OPTION_SCALE] <=> $b[self::OPTION_SCALE]));
-
- return $units;
- }
-
- public function getMatchingUnit(?int $value, array $options): array
- {
- $units = $this->getUnits($options);
- foreach ($units as $unit => $info) {
- $scale = $info[self::OPTION_SCALE];
- if ($value >= $scale || array_key_last($units) === $unit) {
- return [
- self::FIELD_VALUE => null === $value ? null : ($scale > 1 ? $value / $scale : $value),
- self::FIELD_UNIT => $unit,
- self::OPTION_LOCALIZED_UNIT => $info[self::OPTION_LOCALIZED_UNIT],
- ];
- }
- }
-
- throw new \RuntimeException('This should never be called.');
- }
-
- public function getFormattedValue(int $value, array $options): string
- {
- $unit = $this->getMatchingUnit($value, $options);
-
- // @todo There must be a better way to do this!
- return sprintf('%s %s', $this->numberFormatter->format($unit['value']),
- $unit[self::OPTION_LOCALIZED_UNIT]->trans($this->translator));
+ ->setDefault(ValueWithUnitHelper::OPTION_UNITS, [])
+ ->setDefault(ValueWithUnitHelper::OPTION_SCALE, 0);
}
}
diff --git a/src/Service/ValueWithUnitHelper.php b/src/Service/ValueWithUnitHelper.php
new file mode 100644
index 0000000..7c32ba2
--- /dev/null
+++ b/src/Service/ValueWithUnitHelper.php
@@ -0,0 +1,126 @@
+ in_array($key, [ValueWithUnitHelper::OPTION_UNITS, ValueWithUnitHelper::OPTION_SCALE]), ARRAY_FILTER_USE_KEY);
+ $key = sha1(json_encode($options, JSON_THROW_ON_ERROR));
+ if (!isset(self::$clones[$key])) {
+ $clone = clone $this;
+ $clone->options = $this->resolveOptions($options);
+ $clone->numberFormatter = new \NumberFormatter($this->translator->getLocale(), \NumberFormatter::DECIMAL);
+ $clone->numberFormatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $clone->options[self::OPTION_SCALE]);
+ $clone->numberFormatter->setSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL, '');
+ self::$clones[$key] = $clone;
+ }
+
+ return self::$clones[$key];
+ }
+
+ public function getScale(): int
+ {
+ return $this->options[self::OPTION_SCALE] ?? 1;
+ }
+
+ /**
+ * Get units sorted descending by factor.
+ */
+ public function getUnits(): array
+ {
+ $units = $this->options['units'];
+
+ uasort($units, static fn ($a, $b) => -($a[self::OPTION_UNIT_FACTOR] <=> $b[self::OPTION_UNIT_FACTOR]));
+
+ return $units;
+ }
+
+ public function getMatchingUnit(?int $value): array
+ {
+ $units = $this->getUnits();
+ foreach ($units as $unit => $info) {
+ // Make sure that factor is positive.
+ $factor = max(1, $info[self::OPTION_UNIT_FACTOR]);
+
+ // $isMatch = 0 === $value % $factor;
+ $truncatedValue = (float) number_format($value / $factor, $this->getScale(), '.', '');
+ $isMatch = (float) ($value / $factor) === $truncatedValue;
+
+ if (($value >= $factor && $isMatch) || array_key_last($units) === $unit) {
+ return $info + [
+ self::FIELD_VALUE => null === $value ? null : $truncatedValue,
+ self::FIELD_UNIT => $unit,
+ ];
+ }
+ }
+
+ throw new \RuntimeException('This should never be called.');
+ }
+
+ public function getFormattedValue(int $value): string
+ {
+ $unit = $this->getMatchingUnit($value);
+
+ $localizedUnit = $unit[self::OPTION_UNIT_LOCALIZED_UNIT] ?? $unit[self::FIELD_UNIT];
+ if ($localizedUnit instanceof TranslatableMessage) {
+ $localizedUnit = $localizedUnit->trans($this->translator);
+ }
+
+ // @todo There must be a better way to do this!
+ return sprintf('%s %s', $this->numberFormatter->format($unit['value']), $localizedUnit);
+ }
+
+ private function resolveOptions(array $options): array
+ {
+ return (new OptionsResolver())
+ ->setRequired(self::OPTION_UNITS)
+ ->setAllowedTypes(self::OPTION_UNITS, 'array')
+ ->setDefault(self::OPTION_UNITS, function (OptionsResolver $resolver): void {
+ $resolver
+ ->setPrototype(true)
+ ->setDefault(self::OPTION_UNIT_FACTOR, 1)
+ ->setAllowedTypes(self::OPTION_UNIT_FACTOR, 'int')
+ ->setAllowedValues(self::OPTION_UNIT_FACTOR, Validation::createIsValidCallable(
+ new Positive(),
+ ))
+ ->setRequired(self::OPTION_UNIT_LABEL)
+ ->setAllowedTypes(self::OPTION_UNIT_LABEL, ['string', TranslatableMessage::class])
+ ->setDefault(self::OPTION_UNIT_LOCALIZED_UNIT, null)
+ ->setAllowedTypes(self::OPTION_UNIT_LOCALIZED_UNIT, ['null', 'string', TranslatableMessage::class]);
+ })
+ // Make units required.
+ ->setAllowedValues(self::OPTION_UNITS, static fn (array $value) => !empty($value))
+ ->setRequired(self::OPTION_SCALE)
+ ->setAllowedTypes(self::OPTION_SCALE, 'int')
+ ->setDefault(self::OPTION_SCALE, 0)
+ ->resolve($options);
+ }
+}
diff --git a/src/Twig/Runtime/AppExtensionRuntime.php b/src/Twig/Runtime/AppExtensionRuntime.php
index f854714..9e32fc5 100644
--- a/src/Twig/Runtime/AppExtensionRuntime.php
+++ b/src/Twig/Runtime/AppExtensionRuntime.php
@@ -6,6 +6,7 @@
use App\Entity\PointOfInterest;
use App\Form\ValueWithUnitType;
use App\Service\MediaProcessorInterface;
+use App\Service\ValueWithUnitHelper;
use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
use Twig\Extension\RuntimeExtensionInterface;
@@ -13,7 +14,7 @@ class AppExtensionRuntime implements RuntimeExtensionInterface
{
public function __construct(
private readonly MediaProcessorInterface $mediaProcessor,
- private readonly ValueWithUnitType $valueWithUnitType,
+ private readonly ValueWithUnitHelper $valueWithUnitHelper,
) {
}
@@ -44,6 +45,6 @@ public function formatValueWithUnit(FieldDto $field): string
throw new \InvalidArgumentException(sprintf("Field's form type must be %s. Found %s.", ValueWithUnitType::class, $field->getFormType() ?? ''));
}
- return $this->valueWithUnitType->getFormattedValue($field->getValue(), $field->getFormTypeOptions());
+ return $this->valueWithUnitHelper->withOptions($field->getFormTypeOptions())->getFormattedValue($field->getValue());
}
}
diff --git a/symfony.lock b/symfony.lock
index 4cf24c4..531bd43 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -82,7 +82,7 @@
]
},
"hautelook/alice-bundle": {
- "version": "2.12",
+ "version": "2.13",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@@ -111,7 +111,7 @@
]
},
"nelmio/alice": {
- "version": "3.12",
+ "version": "3.13",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@@ -261,6 +261,21 @@
"config/packages/monolog.yaml"
]
},
+ "symfony/phpunit-bridge": {
+ "version": "7.2",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.3",
+ "ref": "a411a0480041243d97382cac7984f7dce7813c08"
+ },
+ "files": [
+ ".env.test",
+ "bin/phpunit",
+ "phpunit.xml.dist",
+ "tests/bootstrap.php"
+ ]
+ },
"symfony/routing": {
"version": "6.3",
"recipe": {
@@ -374,7 +389,7 @@
]
},
"theofidry/alice-data-fixtures": {
- "version": "1.6",
+ "version": "1.8",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
diff --git a/tests/Service/ValueWithUnitHelperTest.php b/tests/Service/ValueWithUnitHelperTest.php
new file mode 100644
index 0000000..10a0626
--- /dev/null
+++ b/tests/Service/ValueWithUnitHelperTest.php
@@ -0,0 +1,350 @@
+helper = $container->get(ValueWithUnitHelper::class);
+ $container->get(LocaleSwitcher::class)->setLocale('en');
+ }
+
+ public function testGetScale(): void
+ {
+ $helper = $this->helper->withOptions([
+ ValueWithUnitHelper::OPTION_UNITS => ['test' => [ValueWithUnitHelper::OPTION_UNIT_LABEL => 'test']],
+ ValueWithUnitHelper::OPTION_SCALE => 2,
+ ]);
+ $expected = 2;
+ $actual = $helper->getScale();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testGetUnits(): void
+ {
+ $helper = $this->helper->withOptions([
+ ValueWithUnitHelper::OPTION_UNITS => ['test' => [ValueWithUnitHelper::OPTION_UNIT_LABEL => 'test']],
+ ]);
+ $expected = [
+ 'test' => [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'test',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => null,
+ ],
+ ];
+ $actual = $helper->getUnits();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ /**
+ * @dataProvider getMatchingUnitProvider
+ */
+ public function testGetMatchingUnit(array $options, int $value, array $expected): void
+ {
+ $helper = $this->helper->withOptions($options);
+ $actual = $helper->getMatchingUnit($value);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public static function getMatchingUnitProvider(): iterable
+ {
+ $options = [
+ ValueWithUnitHelper::OPTION_UNITS => [
+ 'test' => [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'test',
+ ],
+ ],
+ ];
+
+ yield [
+ $options,
+ 87,
+ [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'test',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => null,
+ ValueWithUnitHelper::FIELD_VALUE => 87,
+ ValueWithUnitHelper::FIELD_UNIT => 'test',
+ ],
+ ];
+
+ $options = [
+ ValueWithUnitHelper::OPTION_UNITS => [
+ 'm' => [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'meter',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1,
+ ],
+ 'km' => [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'kilometer',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1000,
+ ],
+ ],
+ ];
+
+ yield [
+ $options,
+ 87,
+ [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'meter',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => null,
+ ValueWithUnitHelper::FIELD_VALUE => 87,
+ ValueWithUnitHelper::FIELD_UNIT => 'm',
+ ],
+ ];
+
+ yield [
+ $options,
+ 1234,
+ [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'meter',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => null,
+ ValueWithUnitHelper::FIELD_VALUE => 1234,
+ ValueWithUnitHelper::FIELD_UNIT => 'm',
+ ],
+ ];
+
+ $options[ValueWithUnitHelper::OPTION_SCALE] = 1;
+ yield [
+ $options,
+ 87,
+ [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'meter',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => null,
+ ValueWithUnitHelper::FIELD_VALUE => 87,
+ ValueWithUnitHelper::FIELD_UNIT => 'm',
+ ],
+ ];
+
+ yield [
+ $options,
+ 1234,
+ [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'meter',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => null,
+ ValueWithUnitHelper::FIELD_VALUE => 1234,
+ ValueWithUnitHelper::FIELD_UNIT => 'm',
+ ],
+ ];
+
+ yield [
+ $options,
+ 2000,
+ [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'kilometer',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1000,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => null,
+ ValueWithUnitHelper::FIELD_VALUE => 2.0,
+ ValueWithUnitHelper::FIELD_UNIT => 'km',
+ ],
+ ];
+
+ yield [
+ $options,
+ 2100,
+ [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'kilometer',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1000,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => null,
+ ValueWithUnitHelper::FIELD_VALUE => 2.1,
+ ValueWithUnitHelper::FIELD_UNIT => 'km',
+ ],
+ ];
+
+ $options = [
+ ValueWithUnitHelper::OPTION_UNITS => [
+ 'hour' => [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'hours',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 60 * 60,
+ ],
+ 'minute' => [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'minutes',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 60,
+ ],
+ ],
+ ];
+
+ yield [
+ $options,
+ 9000,
+ [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'minutes',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 60,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => null,
+ ValueWithUnitHelper::FIELD_VALUE => 150,
+ ValueWithUnitHelper::FIELD_UNIT => 'minute',
+ ],
+ ];
+
+ yield [
+ $options,
+ 10800,
+ [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'hours',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 60 * 60,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => null,
+ ValueWithUnitHelper::FIELD_VALUE => 3,
+ ValueWithUnitHelper::FIELD_UNIT => 'hour',
+ ],
+ ];
+
+ $options[ValueWithUnitHelper::OPTION_SCALE] = 1;
+
+ yield [
+ $options,
+ 9000,
+ [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'hours',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 60 * 60,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => null,
+ ValueWithUnitHelper::FIELD_VALUE => 2.5,
+ ValueWithUnitHelper::FIELD_UNIT => 'hour',
+ ],
+ ];
+
+ yield [
+ $options,
+ 10800,
+ [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'hours',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 60 * 60,
+ ValueWithUnitHelper::OPTION_UNIT_LOCALIZED_UNIT => null,
+ ValueWithUnitHelper::FIELD_VALUE => 3,
+ ValueWithUnitHelper::FIELD_UNIT => 'hour',
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider getFormattedValueProvider
+ */
+ public function testGetFormattedValue(array $options, int $value, string $expected): void
+ {
+ $helper = $this->helper->withOptions($options);
+ $actual = $helper->getFormattedValue($value);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public static function getFormattedValueProvider(): iterable
+ {
+ $options = [
+ ValueWithUnitHelper::OPTION_UNITS => [
+ 'test' => [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'test',
+ ],
+ ],
+ ];
+
+ yield [
+ $options,
+ 87,
+ '87 test',
+ ];
+
+ $options = [
+ ValueWithUnitHelper::OPTION_UNITS => [
+ 'm' => [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'meter',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1,
+ ],
+ 'km' => [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'kilometer',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 1000,
+ ],
+ ],
+ ];
+
+ yield [
+ $options,
+ 87,
+ '87 m',
+ ];
+
+ yield [
+ $options,
+ 1234,
+ '1234 m',
+ ];
+
+ $options[ValueWithUnitHelper::OPTION_SCALE] = 1;
+ yield [
+ $options,
+ 87,
+ '87.0 m',
+ ];
+
+ yield [
+ $options,
+ 1234,
+ '1234.0 m',
+ ];
+
+ yield [
+ $options,
+ 2000,
+ '2.0 km',
+ ];
+
+ yield [
+ $options,
+ 2100,
+ '2.1 km',
+ ];
+
+ $options = [
+ ValueWithUnitHelper::OPTION_UNITS => [
+ 'hour' => [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'hours',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 60 * 60,
+ ],
+ 'minute' => [
+ ValueWithUnitHelper::OPTION_UNIT_LABEL => 'minutes',
+ ValueWithUnitHelper::OPTION_UNIT_FACTOR => 60,
+ ],
+ ],
+ ];
+
+ yield [
+ $options,
+ 9000,
+ '150 minute',
+ ];
+
+ yield [
+ $options,
+ 10800,
+ '3 hour',
+ ];
+
+ $options[ValueWithUnitHelper::OPTION_SCALE] = 1;
+ yield [
+ $options,
+ 9000,
+ '2.5 hour',
+ ];
+
+ yield [
+ $options,
+ 10800,
+ '3.0 hour',
+ ];
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..47a5855
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,13 @@
+bootEnv(dirname(__DIR__).'/.env');
+}
+
+if ($_SERVER['APP_DEBUG']) {
+ umask(0000);
+}