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); +}