diff --git a/.env b/.env index 3e8e3aa..3e68a82 100644 --- a/.env +++ b/.env @@ -29,7 +29,7 @@ APP_SECRET= DATABASE_URL="postgresql://app:!ChangeMe!@database.app-sf.orb.local:5432/app?serverVersion=16&charset=utf8" ###< doctrine/doctrine-bundle ### ###> symfony/mailer ### -# MAILER_DSN=null://null +MAILER_DSN=null://null ###< symfony/mailer ### REDIS_URI=redis://redis.app-sf.orb.local:6379 diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 1cc4cbb..22a1e06 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -7,6 +7,10 @@ on: permissions: contents: read +env: + # Used for PHPStan to initialize the Symfony check + APP_SECRET: TIRKxo3IEcWIr0x4EM6D + jobs: lint: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 6aac60d..226372d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,11 +14,6 @@ .phpunit.result.cache ###< phpunit/phpunit ### -###> symfony/phpunit-bridge ### -.phpunit.result.cache -/phpunit.xml -###< symfony/phpunit-bridge ### - ###> symfony/asset-mapper ### /public/assets/ /assets/vendor/ diff --git a/.idea/app-sf.iml b/.idea/app-sf.iml index cd909c3..1397076 100644 --- a/.idea/app-sf.iml +++ b/.idea/app-sf.iml @@ -189,6 +189,11 @@ + + + + + diff --git a/.idea/php.xml b/.idea/php.xml index 77b9297..13fd26b 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -204,6 +204,16 @@ + + + + + + + + + + diff --git a/assets/app/bootstrap.ts b/assets/app/bootstrap.ts index bda9fb5..e6e1e20 100644 --- a/assets/app/bootstrap.ts +++ b/assets/app/bootstrap.ts @@ -1,5 +1,3 @@ import { startStimulusApp } from "@symfony/stimulus-bundle"; startStimulusApp(); -// register any custom, 3rd party controllers here -// app.register('some_controller_name', SomeImportedController); diff --git a/assets/app/index.ts b/assets/app/index.ts index b998955..34fface 100644 --- a/assets/app/index.ts +++ b/assets/app/index.ts @@ -8,7 +8,7 @@ document.addEventListener("turbo:load", () => { const tooltipTriggerList = document.querySelectorAll("[data-bs-toggle=\"tooltip\"]"); const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); - // Destroy tooltips on navigating to a new page + // Destroy tooltips when navigating to a new page document.addEventListener("turbo:before-visit", () => { for (const tooltip of tooltipList) { tooltip.dispose(); diff --git a/assets/controllers/challenge_comment_controller.ts b/assets/controllers/challenge_comment_controller.ts index f251c68..2a776f0 100644 --- a/assets/controllers/challenge_comment_controller.ts +++ b/assets/controllers/challenge_comment_controller.ts @@ -11,7 +11,7 @@ export default class extends Controller { } async connect() { - const $commentBlock = this.element.querySelector(".challenge-comments__comment"); + const $commentBlock = this.element.querySelector(".app-challenge-comment__main"); if (!$commentBlock) { console.warn("No comment element found. Cannot fade in."); return; @@ -22,7 +22,7 @@ export default class extends Controller { $commentBlock.classList.remove("opacity-0"); }, 20 /* ms */); - const $confirmModal = this.element.querySelector(".challenge-comments__deletion_confirm"); + const $confirmModal = this.element.querySelector(".app-challenge-comment__deletion_confirm"); if ($confirmModal) { this.#modal = new bootstrap.Modal($confirmModal); } diff --git a/assets/styles/app.scss b/assets/styles/app.scss index 31e2e6e..dbf1e79 100644 --- a/assets/styles/app.scss +++ b/assets/styles/app.scss @@ -90,7 +90,7 @@ ul.credit { } } -.question-card { +.app-question-card { @extend .card; overflow: hidden; min-height: 14em; @@ -135,7 +135,7 @@ ul.credit { @extend .d-flex, .gap-3, .justify-content-start, .align-items-center; } - .question-card__pass-rate { + &__pass-rate { &[data-pass-rate~="high"] { @extend .text-success; } @@ -148,8 +148,7 @@ ul.credit { } } -/* ChallengeHeader */ -.challenge-header { +.app-challenge-header { &__title { font-weight: bold; } @@ -184,11 +183,11 @@ ul.credit { } } -.challenge-result-event-presenter { +.app-challenge-result-event-presenter { table-layout: fixed; } -.challenge-primary { +.app-challenge-primary { min-height: 55vh; } @@ -330,8 +329,8 @@ ul.credit { } } -.challenge-comments { - &__comment { +.app-challenge-comment { + &__main { transition: opacity 300ms; } } diff --git a/composer.json b/composer.json index 97290f5..42e62ac 100644 --- a/composer.json +++ b/composer.json @@ -71,7 +71,8 @@ "allow-plugins": { "php-http/discovery": true, "symfony/flex": true, - "symfony/runtime": true + "symfony/runtime": true, + "phpstan/extension-installer": true }, "sort-packages": true }, @@ -129,7 +130,12 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "dev-master", + "phpstan/extension-installer": "1.4.x-dev", "phpstan/phpstan": "2.0.x-dev", + "phpstan/phpstan-doctrine": "2.0.x-dev", + "phpstan/phpstan-phpunit": "2.0.x-dev", + "phpstan/phpstan-strict-rules": "2.0.x-dev", + "phpstan/phpstan-symfony": "2.0.x-dev", "phpunit/phpunit": "^10", "symfony/browser-kit": "7.2.*", "symfony/css-selector": "7.2.*", diff --git a/composer.lock b/composer.lock index aed6f3b..5177e06 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": "f5b342163804ecec59659cf6ea34c91b", + "content-hash": "2b3fb4708f599b2570d89c0e421ce016", "packages": [ { "name": "composer/semver", @@ -351,12 +351,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "b6eb756a5d017130eddd4e6c2b0f36f97b4018c0" + "reference": "8188dd6d7f012a0faf4340284f52b52213ba0bd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/b6eb756a5d017130eddd4e6c2b0f36f97b4018c0", - "reference": "b6eb756a5d017130eddd4e6c2b0f36f97b4018c0", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/8188dd6d7f012a0faf4340284f52b52213ba0bd4", + "reference": "8188dd6d7f012a0faf4340284f52b52213ba0bd4", "shasum": "" }, "require": { @@ -456,7 +456,7 @@ "type": "tidelift" } ], - "time": "2024-09-30T09:22:15+00:00" + "time": "2024-10-02T22:21:41+00:00" }, { "name": "doctrine/deprecations", @@ -1166,12 +1166,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "5724e6279ef2a4258084c0cec97670b2822d7a83" + "reference": "434b7cee2a798e1ef401e5aa2fd3fec19833c6fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/5724e6279ef2a4258084c0cec97670b2822d7a83", - "reference": "5724e6279ef2a4258084c0cec97670b2822d7a83", + "url": "https://api.github.com/repos/doctrine/orm/zipball/434b7cee2a798e1ef401e5aa2fd3fec19833c6fb", + "reference": "434b7cee2a798e1ef401e5aa2fd3fec19833c6fb", "shasum": "" }, "require": { @@ -1246,7 +1246,7 @@ "issues": "https://github.com/doctrine/orm/issues", "source": "https://github.com/doctrine/orm/tree/3.3.x" }, - "time": "2024-09-05T20:03:08+00:00" + "time": "2024-10-04T16:53:50+00:00" }, { "name": "doctrine/persistence", @@ -1900,12 +1900,12 @@ "source": { "type": "git", "url": "https://github.com/meilisearch/meilisearch-symfony.git", - "reference": "a6ee705c2bf4023a42d2ceb37a3b54b6db2cc6d1" + "reference": "0e2d7d86b061d4709f3245e5bb84dfd1e08b27c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/meilisearch/meilisearch-symfony/zipball/a6ee705c2bf4023a42d2ceb37a3b54b6db2cc6d1", - "reference": "a6ee705c2bf4023a42d2ceb37a3b54b6db2cc6d1", + "url": "https://api.github.com/repos/meilisearch/meilisearch-symfony/zipball/0e2d7d86b061d4709f3245e5bb84dfd1e08b27c4", + "reference": "0e2d7d86b061d4709f3245e5bb84dfd1e08b27c4", "shasum": "" }, "require": { @@ -1971,7 +1971,7 @@ "issues": "https://github.com/meilisearch/meilisearch-symfony/issues", "source": "https://github.com/meilisearch/meilisearch-symfony/tree/main" }, - "time": "2024-10-02T07:27:06+00:00" + "time": "2024-10-04T20:00:35+00:00" }, { "name": "monolog/monolog", @@ -2081,12 +2081,12 @@ "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "9ff5667e3e734fad63a9da031c63f6a7935c3bf4" + "reference": "9522dad6211c4d995a01a9ac529da88d0b0ba7b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/9ff5667e3e734fad63a9da031c63f6a7935c3bf4", - "reference": "9ff5667e3e734fad63a9da031c63f6a7935c3bf4", + "url": "https://api.github.com/repos/nette/schema/zipball/9522dad6211c4d995a01a9ac529da88d0b0ba7b5", + "reference": "9522dad6211c4d995a01a9ac529da88d0b0ba7b5", "shasum": "" }, "require": { @@ -2133,9 +2133,9 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.3" + "source": "https://github.com/nette/schema/tree/v1.3.1" }, - "time": "2024-06-18T20:26:58+00:00" + "time": "2024-10-05T03:01:50+00:00" }, { "name": "nette/utils", @@ -3281,12 +3281,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "97e0e91f2165f7432e8f0da38475352e0f35b8a9" + "reference": "5e0bc576c566c05b1d483abbcadaa86967027661" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/97e0e91f2165f7432e8f0da38475352e0f35b8a9", - "reference": "97e0e91f2165f7432e8f0da38475352e0f35b8a9", + "url": "https://api.github.com/repos/symfony/cache/zipball/5e0bc576c566c05b1d483abbcadaa86967027661", + "reference": "5e0bc576c566c05b1d483abbcadaa86967027661", "shasum": "" }, "require": { @@ -3371,7 +3371,7 @@ "type": "tidelift" } ], - "time": "2024-10-01T08:32:35+00:00" + "time": "2024-10-02T06:33:23+00:00" }, { "name": "symfony/cache-contracts", @@ -3605,12 +3605,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c8b54045867f124cf19b7c905d107e73a2a1df8b" + "reference": "270fd988c707d8d3aeae056de564130d3cd1b2ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c8b54045867f124cf19b7c905d107e73a2a1df8b", - "reference": "c8b54045867f124cf19b7c905d107e73a2a1df8b", + "url": "https://api.github.com/repos/symfony/console/zipball/270fd988c707d8d3aeae056de564130d3cd1b2ec", + "reference": "270fd988c707d8d3aeae056de564130d3cd1b2ec", "shasum": "" }, "require": { @@ -3690,7 +3690,7 @@ "type": "tidelift" } ], - "time": "2024-09-28T12:51:41+00:00" + "time": "2024-10-04T09:00:10+00:00" }, { "name": "symfony/dependency-injection", @@ -3698,12 +3698,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "36478e3dc21ff77d325bd881a98c7d2b25c5b920" + "reference": "92db4a73acf0b4f430c2942122207b06a62318dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/36478e3dc21ff77d325bd881a98c7d2b25c5b920", - "reference": "36478e3dc21ff77d325bd881a98c7d2b25c5b920", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/92db4a73acf0b4f430c2942122207b06a62318dd", + "reference": "92db4a73acf0b4f430c2942122207b06a62318dd", "shasum": "" }, "require": { @@ -3770,7 +3770,7 @@ "type": "tidelift" } ], - "time": "2024-09-28T12:51:41+00:00" + "time": "2024-10-02T06:33:23+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3955,12 +3955,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/doctrine-messenger.git", - "reference": "2f764486d0dd59b1aea3d81014a0113a8c950d6b" + "reference": "82ede3b917c68a172832cd4ab3d236bf630dd8e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-messenger/zipball/2f764486d0dd59b1aea3d81014a0113a8c950d6b", - "reference": "2f764486d0dd59b1aea3d81014a0113a8c950d6b", + "url": "https://api.github.com/repos/symfony/doctrine-messenger/zipball/82ede3b917c68a172832cd4ab3d236bf630dd8e0", + "reference": "82ede3b917c68a172832cd4ab3d236bf630dd8e0", "shasum": "" }, "require": { @@ -4019,7 +4019,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2024-10-02T06:33:23+00:00" }, { "name": "symfony/dotenv", @@ -4626,12 +4626,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "7e09e75c5fcdc8bbf769303557b54f1c3be34960" + "reference": "a8f57a0a7a3b8ae1bea4032541df9e76414a12ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/7e09e75c5fcdc8bbf769303557b54f1c3be34960", - "reference": "7e09e75c5fcdc8bbf769303557b54f1c3be34960", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/a8f57a0a7a3b8ae1bea4032541df9e76414a12ad", + "reference": "a8f57a0a7a3b8ae1bea4032541df9e76414a12ad", "shasum": "" }, "require": { @@ -4768,7 +4768,7 @@ "type": "tidelift" } ], - "time": "2024-10-02T08:47:28+00:00" + "time": "2024-10-04T09:59:42+00:00" }, { "name": "symfony/http-client", @@ -6561,12 +6561,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "2fecd4200c9097f697566a30a7238e8479ecec69" + "reference": "f8c1e96378984c4ae72caa2e304728999d2fd580" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/2fecd4200c9097f697566a30a7238e8479ecec69", - "reference": "2fecd4200c9097f697566a30a7238e8479ecec69", + "url": "https://api.github.com/repos/symfony/property-info/zipball/f8c1e96378984c4ae72caa2e304728999d2fd580", + "reference": "f8c1e96378984c4ae72caa2e304728999d2fd580", "shasum": "" }, "require": { @@ -6637,7 +6637,7 @@ "type": "tidelift" } ], - "time": "2024-10-02T08:47:28+00:00" + "time": "2024-10-03T09:19:10+00:00" }, { "name": "symfony/routing", @@ -8255,12 +8255,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/ux-turbo.git", - "reference": "f96a697dc773ab6cc33e400ff0a933aec40335c8" + "reference": "577f34a6d5994778a5a6723686816bac123af16b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/f96a697dc773ab6cc33e400ff0a933aec40335c8", - "reference": "f96a697dc773ab6cc33e400ff0a933aec40335c8", + "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/577f34a6d5994778a5a6723686816bac123af16b", + "reference": "577f34a6d5994778a5a6723686816bac123af16b", "shasum": "" }, "require": { @@ -8275,21 +8275,22 @@ "doctrine/doctrine-bundle": "^2.4.3", "doctrine/orm": "^2.8 | 3.0", "phpstan/phpstan": "^1.10", + "symfony/asset-mapper": "^6.4|^7.0", "symfony/debug-bundle": "^5.4|^6.0|^7.0", "symfony/expression-language": "^5.4|^6.0|^7.0", "symfony/form": "^5.4|^6.0|^7.0", - "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", "symfony/mercure-bundle": "^0.3.7", "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/panther": "^1.0|^2.0", + "symfony/panther": "^2.1", "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", "symfony/process": "^5.4|6.3.*|^7.0", "symfony/property-access": "^5.4|^6.0|^7.0", "symfony/security-core": "^5.4|^6.0|^7.0", "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/twig-bundle": "^5.4|^6.0|^7.0", - "symfony/web-profiler-bundle": "^5.4|^6.0|^7.0", - "symfony/webpack-encore-bundle": "^2.1.1" + "symfony/twig-bundle": "^6.4|^7.0", + "symfony/ux-twig-component": "^2.21", + "symfony/web-profiler-bundle": "^5.4|^6.0|^7.0" }, "default-branch": true, "type": "symfony-bundle", @@ -8345,7 +8346,7 @@ "type": "tidelift" } ], - "time": "2024-09-28T16:15:30+00:00" + "time": "2024-10-03T07:25:39+00:00" }, { "name": "symfony/ux-twig-component", @@ -8437,12 +8438,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "21789543f8d40023c31875228f17bfae079a90cb" + "reference": "651feaea32097823591fe700a19e537dc78a1ba5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/21789543f8d40023c31875228f17bfae079a90cb", - "reference": "21789543f8d40023c31875228f17bfae079a90cb", + "url": "https://api.github.com/repos/symfony/validator/zipball/651feaea32097823591fe700a19e537dc78a1ba5", + "reference": "651feaea32097823591fe700a19e537dc78a1ba5", "shasum": "" }, "require": { @@ -8526,7 +8527,7 @@ "type": "tidelift" } ], - "time": "2024-09-28T12:51:41+00:00" + "time": "2024-10-02T06:33:23+00:00" }, { "name": "symfony/var-dumper", @@ -9018,12 +9019,12 @@ "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "69c2202081de3215283c258c6d8dc29ebe30f10b" + "reference": "5ac03a440cb1a3b3b8b1e08f692e42443e62a382" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/69c2202081de3215283c258c6d8dc29ebe30f10b", - "reference": "69c2202081de3215283c258c6d8dc29ebe30f10b", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/5ac03a440cb1a3b3b8b1e08f692e42443e62a382", + "reference": "5ac03a440cb1a3b3b8b1e08f692e42443e62a382", "shasum": "" }, "require": { @@ -9090,7 +9091,7 @@ "type": "tidelift" } ], - "time": "2024-10-02T14:18:34+00:00" + "time": "2024-10-04T19:01:30+00:00" } ], "packages-dev": [ @@ -9419,12 +9420,12 @@ "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "c808dec27628cf82afff3af7bbdd4fceb2712eae" + "reference": "46abfa65896913c15e15de97c17095184ecc4dbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/c808dec27628cf82afff3af7bbdd4fceb2712eae", - "reference": "c808dec27628cf82afff3af7bbdd4fceb2712eae", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/46abfa65896913c15e15de97c17095184ecc4dbc", + "reference": "46abfa65896913c15e15de97c17095184ecc4dbc", "shasum": "" }, "require": { @@ -9515,7 +9516,7 @@ "type": "github" } ], - "time": "2024-09-30T20:27:03+00:00" + "time": "2024-10-02T19:43:05+00:00" }, { "name": "masterminds/html5", @@ -9822,18 +9823,67 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpstan/extension-installer", + "version": "1.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "e7bd2bf9662c96368303a284be3f2079e204b341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/e7bd2bf9662c96368303a284be3f2079e204b341", + "reference": "e7bd2bf9662c96368303a284be3f2079e204b341", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.12.0 || ^2.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2.0", + "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + }, + "default-branch": true, + "type": "composer-plugin", + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/extension-installer/issues", + "source": "https://github.com/phpstan/extension-installer/tree/1.4.x" + }, + "time": "2024-09-06T14:27:20+00:00" + }, { "name": "phpstan/phpstan", "version": "2.0.x-dev", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "877e5ce1dcc74b8d70d2ddf168faf2d89a139489" + "reference": "393f358af9e2434ad695e000dd33ef2870a761d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/877e5ce1dcc74b8d70d2ddf168faf2d89a139489", - "reference": "877e5ce1dcc74b8d70d2ddf168faf2d89a139489", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/393f358af9e2434ad695e000dd33ef2870a761d5", + "reference": "393f358af9e2434ad695e000dd33ef2870a761d5", "shasum": "" }, "require": { @@ -9879,7 +9929,252 @@ "type": "github" } ], - "time": "2024-10-02T14:23:51+00:00" + "time": "2024-10-04T12:10:09+00:00" + }, + { + "name": "phpstan/phpstan-doctrine", + "version": "2.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-doctrine.git", + "reference": "4f513337c20586277cdc76871eb482b614396bf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/4f513337c20586277cdc76871eb482b614396bf2", + "reference": "4f513337c20586277cdc76871eb482b614396bf2", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" + }, + "conflict": { + "doctrine/collections": "<1.0", + "doctrine/common": "<2.7", + "doctrine/mongodb-odm": "<1.2", + "doctrine/orm": "<2.5", + "doctrine/persistence": "<1.3" + }, + "require-dev": { + "cache/array-adapter": "^1.1", + "composer/semver": "^3.3.2", + "cweagans/composer-patches": "^1.7.3", + "doctrine/annotations": "^2.0", + "doctrine/collections": "^1.6 || ^2.1", + "doctrine/common": "^2.7 || ^3.0", + "doctrine/dbal": "^3.3.8", + "doctrine/lexer": "^2.0 || ^3.0", + "doctrine/mongodb-odm": "^2.4.3", + "doctrine/orm": "^2.16.0", + "doctrine/persistence": "^2.2.1 || ^3.2", + "gedmo/doctrine-extensions": "^3.8", + "nesbot/carbon": "^2.49", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6.20", + "ramsey/uuid": "^4.2", + "symfony/cache": "^5.4" + }, + "default-branch": true, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Doctrine extensions for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-doctrine/issues", + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.x" + }, + "time": "2024-10-04T11:41:54+00:00" + }, + { + "name": "phpstan/phpstan-phpunit", + "version": "2.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-phpunit.git", + "reference": "09e2d3b470bdda02824c626735153dfd962e3f29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/09e2d3b470bdda02824c626735153dfd962e3f29", + "reference": "09e2d3b470bdda02824c626735153dfd962e3f29", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" + }, + "conflict": { + "phpunit/phpunit": "<7.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "default-branch": true, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPUnit extensions and rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-phpunit/issues", + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.x" + }, + "time": "2024-09-24T16:07:03+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "2.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "5d50bde7ed256a94e50e1466a105f8d53fc5ed3a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/5d50bde7ed256a94e50e1466a105f8d53fc5ed3a", + "reference": "5d50bde7ed256a94e50e1466a105f8d53fc5ed3a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "default-branch": true, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.x" + }, + "time": "2024-09-30T19:49:16+00:00" + }, + { + "name": "phpstan/phpstan-symfony", + "version": "2.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-symfony.git", + "reference": "d1e08acebbde9d8f1af925fd247742f40448e32b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/d1e08acebbde9d8f1af925fd247742f40448e32b", + "reference": "d1e08acebbde9d8f1af925fd247742f40448e32b", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" + }, + "conflict": { + "symfony/framework-bundle": "<3.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "psr/container": "1.0 || 1.1.1", + "symfony/config": "^5.4 || ^6.1", + "symfony/console": "^5.4 || ^6.1", + "symfony/dependency-injection": "^5.4 || ^6.1", + "symfony/form": "^5.4 || ^6.1", + "symfony/framework-bundle": "^5.4 || ^6.1", + "symfony/http-foundation": "^5.4 || ^6.1", + "symfony/messenger": "^5.4", + "symfony/polyfill-php80": "^1.24", + "symfony/serializer": "^5.4", + "symfony/service-contracts": "^2.2.0" + }, + "default-branch": true, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukáš Unger", + "email": "looky.msc@gmail.com", + "homepage": "https://lookyman.net" + } + ], + "description": "Symfony Framework extensions and rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-symfony/issues", + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.x" + }, + "time": "2024-10-04T11:54:52+00:00" }, { "name": "phpunit/php-code-coverage", @@ -10210,12 +10505,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f2f936db01e1112ca5372421305f8ef622358d7d" + "reference": "50d62bd541d80b4ad94ea97b9f56d1e2233e86e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f2f936db01e1112ca5372421305f8ef622358d7d", - "reference": "f2f936db01e1112ca5372421305f8ef622358d7d", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/50d62bd541d80b4ad94ea97b9f56d1e2233e86e4", + "reference": "50d62bd541d80b4ad94ea97b9f56d1e2233e86e4", "shasum": "" }, "require": { @@ -10303,7 +10598,7 @@ "type": "tidelift" } ], - "time": "2024-09-29T06:51:19+00:00" + "time": "2024-10-04T14:41:31+00:00" }, { "name": "react/cache", @@ -12496,7 +12791,12 @@ "symfonycasts/sass-bundle": 20, "twbs/bootstrap": 20, "friendsofphp/php-cs-fixer": 20, + "phpstan/extension-installer": 20, "phpstan/phpstan": 20, + "phpstan/phpstan-doctrine": 20, + "phpstan/phpstan-phpunit": 20, + "phpstan/phpstan-strict-rules": 20, + "phpstan/phpstan-symfony": 20, "vincentlanglet/twig-cs-fixer": 20 }, "prefer-stable": false, diff --git a/config/packages/sensiolabs_typescript.yaml b/config/packages/sensiolabs_typescript.yaml new file mode 100644 index 0000000..0eff5b3 --- /dev/null +++ b/config/packages/sensiolabs_typescript.yaml @@ -0,0 +1,2 @@ +sensiolabs_typescript: + swc_version: v1.7.14 diff --git a/devenv.lock b/devenv.lock index 31a3a59..3fc4054 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,10 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1727779147, + "lastModified": 1728112965, "owner": "cachix", "repo": "devenv", - "rev": "eb86c60fc04a8be6a544b2647cc0ca8dc611533e", + "rev": "cc562bf2bd351b475ceffb50c2056e73d0f639ce", "type": "github" }, "original": { @@ -53,10 +53,10 @@ }, "nixpkgs": { "locked": { - "lastModified": 1727777050, + "lastModified": 1728031656, "owner": "nixos", "repo": "nixpkgs", - "rev": "d78d09350ac7dfe503cf48cbc59764aef4157b9a", + "rev": "eeeb90a1dd3c9bea3afdbc76fd34d0fb2a727c7a", "type": "github" }, "original": { @@ -68,10 +68,10 @@ }, "nixpkgs-stable": { "locked": { - "lastModified": 1727672256, + "lastModified": 1727907660, "owner": "NixOS", "repo": "nixpkgs", - "rev": "1719f27dd95fd4206afb9cec9f415b539978827e", + "rev": "5966581aa04be7eff830b9e1457d56dc70a0b798", "type": "github" }, "original": { @@ -91,10 +91,10 @@ "nixpkgs-stable": "nixpkgs-stable" }, "locked": { - "lastModified": 1727854478, + "lastModified": 1728092656, "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "5f58871c9657b5fc0a7f65670fe2ba99c26c1d79", + "rev": "1211305a5b237771e13fcca0c51e60ad47326a9a", "type": "github" }, "original": { diff --git a/devenv.nix b/devenv.nix index bea19cb..59b9994 100644 --- a/devenv.nix +++ b/devenv.nix @@ -14,6 +14,7 @@ languages.php.version = "8.3"; languages.php.extensions = [ "apcu" "intl" "opcache" "zip" "redis" "pdo_pgsql" "sysvsem" "xdebug" ]; languages.php.disableExtensions = [ "soap" ]; + languages.php.ini = builtins.readFile ./frankenphp/conf.d/10-app.ini; languages.javascript.enable = true; languages.javascript.corepack.enable = true; diff --git a/frankenphp/conf.d/10-app.ini b/frankenphp/conf.d/10-app.ini index e73d9c6..21a279a 100644 --- a/frankenphp/conf.d/10-app.ini +++ b/frankenphp/conf.d/10-app.ini @@ -3,6 +3,7 @@ date.timezone = Asia/Taipei apc.enable_cli = 1 session.use_strict_mode = 1 zend.detect_unicode = 0 +memory_limit = 512M ; https://symfony.com/doc/current/performance.html realpath_cache_size = 4096K diff --git a/importmap.php b/importmap.php index dfa00db..a0a5bf4 100644 --- a/importmap.php +++ b/importmap.php @@ -83,7 +83,7 @@ 'version' => '2.2.8', ], '@lezer/common' => [ - 'version' => '1.2.1', + 'version' => '1.2.2', ], 'crelt' => [ 'version' => '1.0.6', diff --git a/package.json b/package.json index 08ef100..c856224 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,12 @@ }, "packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1", "devDependencies": { - "@eslint/js": "^9.11.1", + "@eslint/js": "^9.12.0", "@hotwired/stimulus": "^3.2.2", "@types/bootstrap": "^5.2.10", "bootstrap": "^5.3.3", "dprint": "^0.47.2", - "eslint": "^9.11.1", + "eslint": "^9.12.0", "globals": "^15.10.0", "typescript-eslint": "^8.8.0" } diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 3ba9219..2469fa7 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -6,3 +6,5 @@ parameters: - public/ - src/ - tests/ + symfony: + consoleApplicationLoader: tests/console-application.php diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4490b90..e8cad7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,14 +15,14 @@ importers: version: 3.2.2(@hotwired/stimulus@3.2.2) codemirror: specifier: ^6.0.1 - version: 6.0.1(@lezer/common@1.2.1) + version: 6.0.1(@lezer/common@1.2.2) typescript: specifier: ^5.6.2 version: 5.6.2 devDependencies: "@eslint/js": - specifier: ^9.11.1 - version: 9.11.1 + specifier: ^9.12.0 + version: 9.12.0 "@hotwired/stimulus": specifier: ^3.2.2 version: 3.2.2 @@ -36,14 +36,14 @@ importers: specifier: ^0.47.2 version: 0.47.2 eslint: - specifier: ^9.11.1 - version: 9.11.1 + specifier: ^9.12.0 + version: 9.12.0 globals: specifier: ^15.10.0 version: 15.10.0 typescript-eslint: specifier: ^8.8.0 - version: 8.8.0(eslint@9.11.1)(typescript@5.6.2) + version: 8.8.0(eslint@9.12.0)(typescript@5.6.2) packages: "@codemirror/autocomplete@6.18.1": @@ -179,9 +179,9 @@ packages: } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - "@eslint/js@9.11.1": + "@eslint/js@9.12.0": resolution: { - integrity: sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==, + integrity: sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } @@ -209,21 +209,33 @@ packages: integrity: sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==, } + "@humanfs/core@0.19.0": + resolution: { + integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==, + } + engines: { node: ">=18.18.0" } + + "@humanfs/node@0.16.5": + resolution: { + integrity: sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==, + } + engines: { node: ">=18.18.0" } + "@humanwhocodes/module-importer@1.0.1": resolution: { integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, } engines: { node: ">=12.22" } - "@humanwhocodes/retry@0.3.0": + "@humanwhocodes/retry@0.3.1": resolution: { - integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==, + integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==, } engines: { node: ">=18.18" } - "@lezer/common@1.2.1": + "@lezer/common@1.2.2": resolution: { - integrity: sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==, + integrity: sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw==, } "@lezer/highlight@1.2.1": @@ -386,12 +398,6 @@ packages: integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==, } - ansi-regex@5.0.1: - resolution: { - integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, - } - engines: { node: ">=8" } - ansi-styles@4.3.0: resolution: { integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, @@ -532,9 +538,9 @@ packages: } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - eslint@9.11.1: + eslint@9.12.0: resolution: { - integrity: sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==, + integrity: sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } hasBin: true @@ -700,12 +706,6 @@ packages: } engines: { node: ">=0.12.0" } - is-path-inside@3.0.3: - resolution: { - integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==, - } - engines: { node: ">=8" } - isexe@2.0.0: resolution: { integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, @@ -901,12 +901,6 @@ packages: } engines: { node: ">=8" } - strip-ansi@6.0.1: - resolution: { - integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, - } - engines: { node: ">=8" } - strip-json-comments@3.1.1: resolution: { integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, @@ -997,26 +991,26 @@ packages: engines: { node: ">=10" } snapshots: - "@codemirror/autocomplete@6.18.1(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1)(@lezer/common@1.2.1)": + "@codemirror/autocomplete@6.18.1(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1)(@lezer/common@1.2.2)": dependencies: "@codemirror/language": 6.10.3 "@codemirror/state": 6.4.1 "@codemirror/view": 6.34.1 - "@lezer/common": 1.2.1 + "@lezer/common": 1.2.2 "@codemirror/commands@6.6.2": dependencies: "@codemirror/language": 6.10.3 "@codemirror/state": 6.4.1 "@codemirror/view": 6.34.1 - "@lezer/common": 1.2.1 + "@lezer/common": 1.2.2 "@codemirror/lang-sql@6.8.0(@codemirror/view@6.34.1)": dependencies: - "@codemirror/autocomplete": 6.18.1(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1)(@lezer/common@1.2.1) + "@codemirror/autocomplete": 6.18.1(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1)(@lezer/common@1.2.2) "@codemirror/language": 6.10.3 "@codemirror/state": 6.4.1 - "@lezer/common": 1.2.1 + "@lezer/common": 1.2.2 "@lezer/highlight": 1.2.1 "@lezer/lr": 1.4.2 transitivePeerDependencies: @@ -1026,7 +1020,7 @@ snapshots: dependencies: "@codemirror/state": 6.4.1 "@codemirror/view": 6.34.1 - "@lezer/common": 1.2.1 + "@lezer/common": 1.2.2 "@lezer/highlight": 1.2.1 "@lezer/lr": 1.4.2 style-mod: 4.1.2 @@ -1075,9 +1069,9 @@ snapshots: "@dprint/win32-x64@0.47.2": optional: true - "@eslint-community/eslint-utils@4.4.0(eslint@9.11.1)": + "@eslint-community/eslint-utils@4.4.0(eslint@9.12.0)": dependencies: - eslint: 9.11.1 + eslint: 9.12.0 eslint-visitor-keys: 3.4.3 "@eslint-community/regexpp@4.11.1": {} @@ -1106,7 +1100,7 @@ snapshots: transitivePeerDependencies: - supports-color - "@eslint/js@9.11.1": {} + "@eslint/js@9.12.0": {} "@eslint/object-schema@2.1.4": {} @@ -1120,19 +1114,26 @@ snapshots: "@hotwired/stimulus@3.2.2": {} + "@humanfs/core@0.19.0": {} + + "@humanfs/node@0.16.5": + dependencies: + "@humanfs/core": 0.19.0 + "@humanwhocodes/retry": 0.3.1 + "@humanwhocodes/module-importer@1.0.1": {} - "@humanwhocodes/retry@0.3.0": {} + "@humanwhocodes/retry@0.3.1": {} - "@lezer/common@1.2.1": {} + "@lezer/common@1.2.2": {} "@lezer/highlight@1.2.1": dependencies: - "@lezer/common": 1.2.1 + "@lezer/common": 1.2.2 "@lezer/lr@1.4.2": dependencies: - "@lezer/common": 1.2.1 + "@lezer/common": 1.2.2 "@nodelib/fs.scandir@2.1.5": dependencies: @@ -1167,15 +1168,15 @@ snapshots: "@types/webpack-env@1.18.5": {} - "@typescript-eslint/eslint-plugin@8.8.0(@typescript-eslint/parser@8.8.0(eslint@9.11.1)(typescript@5.6.2))(eslint@9.11.1)(typescript@5.6.2)": + "@typescript-eslint/eslint-plugin@8.8.0(@typescript-eslint/parser@8.8.0(eslint@9.12.0)(typescript@5.6.2))(eslint@9.12.0)(typescript@5.6.2)": dependencies: "@eslint-community/regexpp": 4.11.1 - "@typescript-eslint/parser": 8.8.0(eslint@9.11.1)(typescript@5.6.2) + "@typescript-eslint/parser": 8.8.0(eslint@9.12.0)(typescript@5.6.2) "@typescript-eslint/scope-manager": 8.8.0 - "@typescript-eslint/type-utils": 8.8.0(eslint@9.11.1)(typescript@5.6.2) - "@typescript-eslint/utils": 8.8.0(eslint@9.11.1)(typescript@5.6.2) + "@typescript-eslint/type-utils": 8.8.0(eslint@9.12.0)(typescript@5.6.2) + "@typescript-eslint/utils": 8.8.0(eslint@9.12.0)(typescript@5.6.2) "@typescript-eslint/visitor-keys": 8.8.0 - eslint: 9.11.1 + eslint: 9.12.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -1185,14 +1186,14 @@ snapshots: transitivePeerDependencies: - supports-color - "@typescript-eslint/parser@8.8.0(eslint@9.11.1)(typescript@5.6.2)": + "@typescript-eslint/parser@8.8.0(eslint@9.12.0)(typescript@5.6.2)": dependencies: "@typescript-eslint/scope-manager": 8.8.0 "@typescript-eslint/types": 8.8.0 "@typescript-eslint/typescript-estree": 8.8.0(typescript@5.6.2) "@typescript-eslint/visitor-keys": 8.8.0 debug: 4.3.7 - eslint: 9.11.1 + eslint: 9.12.0 optionalDependencies: typescript: 5.6.2 transitivePeerDependencies: @@ -1203,10 +1204,10 @@ snapshots: "@typescript-eslint/types": 8.8.0 "@typescript-eslint/visitor-keys": 8.8.0 - "@typescript-eslint/type-utils@8.8.0(eslint@9.11.1)(typescript@5.6.2)": + "@typescript-eslint/type-utils@8.8.0(eslint@9.12.0)(typescript@5.6.2)": dependencies: "@typescript-eslint/typescript-estree": 8.8.0(typescript@5.6.2) - "@typescript-eslint/utils": 8.8.0(eslint@9.11.1)(typescript@5.6.2) + "@typescript-eslint/utils": 8.8.0(eslint@9.12.0)(typescript@5.6.2) debug: 4.3.7 ts-api-utils: 1.3.0(typescript@5.6.2) optionalDependencies: @@ -1232,13 +1233,13 @@ snapshots: transitivePeerDependencies: - supports-color - "@typescript-eslint/utils@8.8.0(eslint@9.11.1)(typescript@5.6.2)": + "@typescript-eslint/utils@8.8.0(eslint@9.12.0)(typescript@5.6.2)": dependencies: - "@eslint-community/eslint-utils": 4.4.0(eslint@9.11.1) + "@eslint-community/eslint-utils": 4.4.0(eslint@9.12.0) "@typescript-eslint/scope-manager": 8.8.0 "@typescript-eslint/types": 8.8.0 "@typescript-eslint/typescript-estree": 8.8.0(typescript@5.6.2) - eslint: 9.11.1 + eslint: 9.12.0 transitivePeerDependencies: - supports-color - typescript @@ -1265,8 +1266,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ansi-regex@5.0.1: {} - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -1301,9 +1300,9 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - codemirror@6.0.1(@lezer/common@1.2.1): + codemirror@6.0.1(@lezer/common@1.2.2): dependencies: - "@codemirror/autocomplete": 6.18.1(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1)(@lezer/common@1.2.1) + "@codemirror/autocomplete": 6.18.1(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1)(@lezer/common@1.2.2) "@codemirror/commands": 6.6.2 "@codemirror/language": 6.10.3 "@codemirror/lint": 6.8.2 @@ -1359,18 +1358,18 @@ snapshots: eslint-visitor-keys@4.1.0: {} - eslint@9.11.1: + eslint@9.12.0: dependencies: - "@eslint-community/eslint-utils": 4.4.0(eslint@9.11.1) + "@eslint-community/eslint-utils": 4.4.0(eslint@9.12.0) "@eslint-community/regexpp": 4.11.1 "@eslint/config-array": 0.18.0 "@eslint/core": 0.6.0 "@eslint/eslintrc": 3.1.0 - "@eslint/js": 9.11.1 + "@eslint/js": 9.12.0 "@eslint/plugin-kit": 0.2.0 + "@humanfs/node": 0.16.5 "@humanwhocodes/module-importer": 1.0.1 - "@humanwhocodes/retry": 0.3.0 - "@nodelib/fs.walk": 1.2.8 + "@humanwhocodes/retry": 0.3.1 "@types/estree": 1.0.6 "@types/json-schema": 7.0.15 ajv: 6.12.6 @@ -1390,13 +1389,11 @@ snapshots: ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 - strip-ansi: 6.0.1 text-table: 0.2.0 transitivePeerDependencies: - supports-color @@ -1490,8 +1487,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - isexe@2.0.0: {} js-yaml@4.1.0: @@ -1601,10 +1596,6 @@ snapshots: shebang-regex@3.0.0: {} - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - strip-json-comments@3.1.1: {} style-mod@4.1.2: {} @@ -1627,11 +1618,11 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.8.0(eslint@9.11.1)(typescript@5.6.2): + typescript-eslint@8.8.0(eslint@9.12.0)(typescript@5.6.2): dependencies: - "@typescript-eslint/eslint-plugin": 8.8.0(@typescript-eslint/parser@8.8.0(eslint@9.11.1)(typescript@5.6.2))(eslint@9.11.1)(typescript@5.6.2) - "@typescript-eslint/parser": 8.8.0(eslint@9.11.1)(typescript@5.6.2) - "@typescript-eslint/utils": 8.8.0(eslint@9.11.1)(typescript@5.6.2) + "@typescript-eslint/eslint-plugin": 8.8.0(@typescript-eslint/parser@8.8.0(eslint@9.12.0)(typescript@5.6.2))(eslint@9.12.0)(typescript@5.6.2) + "@typescript-eslint/parser": 8.8.0(eslint@9.12.0)(typescript@5.6.2) + "@typescript-eslint/utils": 8.8.0(eslint@9.12.0)(typescript@5.6.2) optionalDependencies: typescript: 5.6.2 transitivePeerDependencies: diff --git a/src/Command/CreateUserCommand.php b/src/Command/CreateUserCommand.php index f09ceec..52832c4 100644 --- a/src/Command/CreateUserCommand.php +++ b/src/Command/CreateUserCommand.php @@ -36,77 +36,29 @@ protected function configure(): void $this->addOption('roles', 'r', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The roles of this account. Can specify multiple times.'); } - public function getNameArg(InputInterface $input): string + protected function execute(InputInterface $input, OutputInterface $output): int { - $name = $input->getArgument('name'); - if (!\is_string($name)) { - throw new \InvalidArgumentException('The name must be a string.'); - } - if (empty($name)) { - throw new \InvalidArgumentException('The name cannot be empty.'); - } + $io = new SymfonyStyle($input, $output); - return $name; - } + /** + * @var string $name + */ + $name = $input->getArgument('name'); - public function getEmailArg(InputInterface $input): string - { + /** + * @var string $email + */ $email = $input->getArgument('email'); - if (!\is_string($email)) { - throw new \InvalidArgumentException('The email must be a string.'); - } - if (empty($email)) { - throw new \InvalidArgumentException('The email cannot be empty.'); - } - - return $email; - } - public function getPasswordArg(InputInterface $input): string - { + /** + * @var string $password + */ $password = $input->getOption('password'); - if (!\is_string($password)) { - throw new \InvalidArgumentException('The password must be a string.'); - } - if (empty($password)) { - throw new \InvalidArgumentException('The password cannot be empty.'); - } - - return $password; - } - - /** - * @return list - */ - public function getRolesOpt(InputInterface $input): array - { - $roles = $input->getOption('roles'); - - \assert(\is_array($roles) && array_is_list($roles), 'The roles must be an array.'); /** - * @var list $rolesCasted + * @var list $roles */ - $rolesCasted = []; - - foreach ($roles as $role) { - if (!\is_string($role)) { - throw new \InvalidArgumentException('The roles must be strings.'); - } - $rolesCasted[] = $role; - } - - return $rolesCasted; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - - $name = $this->getNameArg($input); - $email = $this->getEmailArg($input); - $password = $this->getPasswordArg($input); - $roles = $this->getRolesOpt($input); + $roles = $input->getOption('roles'); $user = (new User())->setName($name)->setEmail($email)->setRoles($roles); $hashedPassword = $this->passwordHasher->hashPassword($user, $password); @@ -115,7 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->entityManager->persist($user); $this->entityManager->flush(); - $io->success("Created a user {$email} with password: {$password}"); + $io->success("Created a user $email with password: $password"); return Command::SUCCESS; } diff --git a/src/Command/ExportCommand.php b/src/Command/ExportCommand.php index add2f59..7e1fc38 100644 --- a/src/Command/ExportCommand.php +++ b/src/Command/ExportCommand.php @@ -4,6 +4,7 @@ namespace App\Command; +use App\Entity\ExportDto\ExportedDataDto; use App\Entity\ExportDto\QuestionDto; use App\Entity\ExportDto\SchemaDto; use App\Repository\QuestionRepository; @@ -14,6 +15,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Serializer\SerializerInterface; #[AsCommand( name: 'app:export-schema', @@ -24,6 +26,7 @@ class ExportCommand extends Command public function __construct( private readonly SchemaRepository $schemaRepository, private readonly QuestionRepository $questionRepository, + private readonly SerializerInterface $serializer, ) { parent::__construct(); } @@ -33,19 +36,16 @@ protected function configure(): void $this->addArgument('filename', InputArgument::REQUIRED, 'The JSON filename to export the schema and questions.'); } - /** - * @throws \JsonException - */ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); + /** + * @var string $filename + */ $filename = $input->getArgument('filename'); - if (!\is_string($filename)) { - $io->error('The filename must be a string.'); - return Command::FAILURE; - } + $exportedData = new ExportedDataDto(); /** * @var array $schemas @@ -55,9 +55,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->info("Exporting schema {$schema->getId()}…"); $schemas[$schema->getId()] = SchemaDto::fromEntity($schema); } + $exportedData->setSchemas($schemas); /** - * @var QuestionDto[] $questions + * @var list $questions */ $questions = []; foreach ($this->questionRepository->findBy( @@ -67,21 +68,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->info("Exporting question {$question->getId()}…"); $questions[] = QuestionDto::fromEntity($question); } + $exportedData->setQuestions($questions); $io->info('Exporting schema and questions…'); - $f = fopen($filename, 'w'); - if (!$f) { - $io->error("Cannot open $filename for writing."); + $serialized = $this->serializer->serialize($exportedData, 'json', [ + 'json_encode_options' => \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE, + ]); + + if (false === file_put_contents($filename, $serialized)) { + $io->error("Cannot write to the file $filename."); return Command::FAILURE; } - fwrite($f, json_encode([ - 'schemas' => $schemas, - 'questions' => $questions, - ], \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR)); - fclose($f); - $io->success("Exported schema and questions to $filename."); return Command::SUCCESS; diff --git a/src/Command/ImportCommand.php b/src/Command/ImportCommand.php index ede00a8..00a8194 100644 --- a/src/Command/ImportCommand.php +++ b/src/Command/ImportCommand.php @@ -4,10 +4,8 @@ namespace App\Command; -use App\Entity\ExportDto\QuestionDto; -use App\Entity\ExportDto\SchemaDto; +use App\Entity\ExportDto\ExportedDataDto; use App\Entity\Schema; -use App\Repository\SchemaRepository; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -15,6 +13,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Serializer\SerializerInterface; #[AsCommand( name: 'app:import-schema', @@ -24,6 +23,7 @@ class ImportCommand extends Command { public function __construct( private readonly EntityManagerInterface $entityManager, + private readonly SerializerInterface $serializer, ) { parent::__construct(); } @@ -33,19 +33,14 @@ protected function configure(): void $this->addArgument('filename', InputArgument::REQUIRED, 'The JSON filename to import the schema and questions.'); } - /** - * @throws \JsonException - */ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); + /** + * @var string $filename + */ $filename = $input->getArgument('filename'); - if (!\is_string($filename)) { - $io->error('The filename must be a string.'); - - return Command::FAILURE; - } $io->info('Unmarshaling schema and questions…'); $content = file_get_contents($filename); @@ -55,71 +50,32 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $data = json_decode($content, flags: \JSON_THROW_ON_ERROR); - - if (!\is_object($data)) { - $io->error('The data must be an object.'); - - return Command::FAILURE; - } - if (!isset($data->schemas) || !isset($data->questions)) { - $io->error('The schemas and questions must be set.'); - - return Command::FAILURE; - } - if (!\is_object($data->schemas)) { - $io->error('The schemas must be an object.'); - - return Command::FAILURE; - } - if (!\is_array($data->questions)) { - $io->error('The questions must be an array.'); - - return Command::FAILURE; - } - - try { - $this->entityManager->wrapInTransaction(function (EntityManagerInterface $em) use ($io, $data): void { - $schemaRepository = $em->getRepository(Schema::class); - \assert($schemaRepository instanceof SchemaRepository); - - $io->info('Importing schema…'); - foreach ((array) $data->schemas as $schema) { - if (!\is_object($schema)) { - throw new \InvalidArgumentException('The schema must be an object.'); - } + $exportedData = $this->serializer->deserialize($content, ExportedDataDto::class, 'json'); - $dto = SchemaDto::fromJsonObject($schema); + $this->entityManager->wrapInTransaction(function (EntityManagerInterface $em) use ($io, $exportedData): void { + $schemaRepository = $em->getRepository(Schema::class); - if ($schemaRepository->find($dto->id)) { - $io->info("Schema {$dto->id} already exists, skipping…"); - continue; - } - - $io->info("Importing schema {$dto->id}…"); - $em->persist($dto->toEntity()); + $io->info('Importing schema…'); + foreach ($exportedData->getSchemas() as $schema) { + $existingSchema = $schemaRepository->find($schema->getId()); + if (null !== $existingSchema) { + $io->info("Schema {$schema->getId()} already exists, skipping…"); + continue; } - $io->info('Importing questions…'); - foreach ($data->questions as $question) { - if (!\is_object($question)) { - throw new \InvalidArgumentException('The question must be an object.'); - } - - $dto = QuestionDto::fromJsonObject($question); - - $io->info("Importing question {$dto->title}…"); + $io->info("Importing schema {$schema->getId()}…"); + $em->persist($schema->toEntity()); + } - $em->persist($dto->toEntity($schemaRepository)); - } + $io->info('Importing questions…'); + foreach ($exportedData->getQuestions() as $question) { + $io->info("Importing question {$question->getTitle()}…"); - $em->flush(); - }); - } catch (\InvalidArgumentException $e) { - $io->error($e->getMessage()); + $em->persist($question->toEntity($schemaRepository)); + } - return Command::FAILURE; - } + $em->flush(); + }); $io->success("Imported schema and questions from $filename."); diff --git a/src/Controller/ChallengeController.php b/src/Controller/ChallengeController.php index f4143ae..f61a514 100644 --- a/src/Controller/ChallengeController.php +++ b/src/Controller/ChallengeController.php @@ -42,7 +42,7 @@ public function solution_video( } $solutionVideo = $question->getSolutionVideo(); - if (!$solutionVideo) { + if (null === $solutionVideo) { throw $this->createNotFoundException('There is no solution video for this question.'); } diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index 09f8a25..bf7b826 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -13,7 +13,7 @@ class HomeController extends AbstractController #[Route('/', name: 'app_home')] public function index(): Response { - if (!$this->getUser()) { + if (null === $this->getUser()) { return $this->redirectToRoute('app_login'); } diff --git a/src/Controller/OverviewCardsController.php b/src/Controller/OverviewCardsController.php index b3d6925..a2c374b 100644 --- a/src/Controller/OverviewCardsController.php +++ b/src/Controller/OverviewCardsController.php @@ -9,6 +9,7 @@ use App\Repository\QuestionRepository; use App\Repository\SolutionEventRepository; use App\Service\PointCalculationService; +use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -34,8 +35,16 @@ class OverviewCardsController extends AbstractController public function points( #[CurrentUser] User $user, PointCalculationService $pointCalculationService, + LoggerInterface $logger, ): Response { - $points = $pointCalculationService->calculate($user); + try { + $points = $pointCalculationService->calculate($user); + } catch (\Throwable) { + $logger->warning('Failed to calculate the points for the user.', [ + 'user' => $user->getId(), + ]); + $points = 0; + } return $this->render('overview/cards/points.html.twig', [ 'points' => $points, diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php index e399f7d..e007028 100644 --- a/src/Controller/ProfileController.php +++ b/src/Controller/ProfileController.php @@ -35,12 +35,6 @@ public function editPassword( Request $request, #[CurrentUser] User $user, ): Response { - // If it is not opened from Turbo, we redirect to the profile page - $frameId = $request->headers->get('Turbo-Frame'); - if (!$frameId) { - return $this->redirectToRoute('app_profile'); - } - $passwordChangeModel = new PasswordChangeModel(); $passwordChangeForm = $formFactory->createBuilder(PasswordChangeFormType::class, $passwordChangeModel) ->setAction($this->generateUrl('app_profile_edit_password')) @@ -72,12 +66,6 @@ public function editUsername( Request $request, #[CurrentUser] User $user, ): Response { - // If it is not opened from Turbo, we redirect to the profile page - $frameId = $request->headers->get('Turbo-Frame'); - if (!$frameId) { - return $this->redirectToRoute('app_profile'); - } - $usernameChangeForm = $formFactory->createBuilder(NameChangeFormType::class, $user) ->setAction($this->generateUrl('app_profile_edit_username')) ->getForm(); diff --git a/src/Entity/BaseEvent.php b/src/Entity/BaseEvent.php index 490234e..c39c91c 100644 --- a/src/Entity/BaseEvent.php +++ b/src/Entity/BaseEvent.php @@ -18,8 +18,8 @@ abstract class BaseEvent #[ORM\Column(type: 'ulid', unique: true)] protected ?Ulid $id = null; - #[ORM\Column(name: 'created_at')] - private ?\DateTimeImmutable $createdAt = null; + #[ORM\Column] + private \DateTimeImmutable $createdAt; public function getId(): ?Ulid { @@ -29,9 +29,9 @@ public function getId(): ?Ulid /** * Get the created at date and time of the solution event. * - * @return ?\DateTimeImmutable The parsed DateTime object + * @return \DateTimeImmutable The parsed DateTime object */ - public function getCreatedAt(): ?\DateTimeImmutable + public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; } diff --git a/src/Entity/Comment.php b/src/Entity/Comment.php index 01f4d3e..4b005b3 100644 --- a/src/Entity/Comment.php +++ b/src/Entity/Comment.php @@ -24,15 +24,15 @@ class Comment #[ORM\ManyToOne(inversedBy: 'comments')] #[ORM\JoinColumn(nullable: false)] - private ?User $commenter = null; + private User $commenter; #[ORM\ManyToOne(inversedBy: 'comments')] #[ORM\JoinColumn(nullable: false)] - private ?Question $question = null; + private Question $question; #[ORM\Column(type: Types::TEXT)] #[NotBlank] - private ?string $content = null; + private string $content; /** * @var Collection @@ -50,31 +50,31 @@ public function getId(): ?int return $this->id; } - public function getCommenter(): ?User + public function getCommenter(): User { return $this->commenter; } - public function setCommenter(?User $commenter): static + public function setCommenter(User $commenter): static { $this->commenter = $commenter; return $this; } - public function getQuestion(): ?Question + public function getQuestion(): Question { return $this->question; } - public function setQuestion(?Question $question): static + public function setQuestion(Question $question): static { $this->question = $question; return $this; } - public function getContent(): ?string + public function getContent(): string { return $this->content; } @@ -107,9 +107,9 @@ public function addCommentLikeEvent(CommentLikeEvent $commentLikeEvent): static public function removeCommentLikeEvent(CommentLikeEvent $commentLikeEvent): static { if ($this->commentLikeEvents->removeElement($commentLikeEvent)) { - // set the owning side to null (unless already changed) + // set the owning side to a default class (unless already changed) if ($commentLikeEvent->getComment() === $this) { - $commentLikeEvent->setComment(null); + $commentLikeEvent->setComment(new self()); } } diff --git a/src/Entity/CommentLikeEvent.php b/src/Entity/CommentLikeEvent.php index e7222b4..0ed4100 100644 --- a/src/Entity/CommentLikeEvent.php +++ b/src/Entity/CommentLikeEvent.php @@ -12,30 +12,30 @@ class CommentLikeEvent extends BaseEvent { #[ORM\ManyToOne(inversedBy: 'commentLikeEvents')] #[ORM\JoinColumn(nullable: false)] - private ?User $liker = null; + private User $liker; #[ORM\ManyToOne(inversedBy: 'commentLikeEvents')] #[ORM\JoinColumn(nullable: false)] - private ?Comment $comment = null; + private Comment $comment; - public function getLiker(): ?User + public function getLiker(): User { return $this->liker; } - public function setLiker(?User $liker): static + public function setLiker(User $liker): static { $this->liker = $liker; return $this; } - public function getComment(): ?Comment + public function getComment(): Comment { return $this->comment; } - public function setComment(?Comment $comment): static + public function setComment(Comment $comment): static { $this->comment = $comment; diff --git a/src/Entity/ExportDto/ExportedDataDto.php b/src/Entity/ExportDto/ExportedDataDto.php new file mode 100644 index 0000000..9cb2fd0 --- /dev/null +++ b/src/Entity/ExportDto/ExportedDataDto.php @@ -0,0 +1,54 @@ + + */ + private array $schemas; + + /** + * @var list + */ + private array $questions; + + /** + * @return array + */ + public function getSchemas(): array + { + return $this->schemas; + } + + /** + * @param array $schemas + */ + public function setSchemas(array $schemas): self + { + $this->schemas = $schemas; + + return $this; + } + + /** + * @return list + */ + public function getQuestions(): array + { + return $this->questions; + } + + /** + * @param list $questions + */ + public function setQuestions(array $questions): self + { + $this->questions = $questions; + + return $this; + } +} diff --git a/src/Entity/ExportDto/Importable.php b/src/Entity/ExportDto/Importable.php deleted file mode 100644 index 01b39bc..0000000 --- a/src/Entity/ExportDto/Importable.php +++ /dev/null @@ -1,10 +0,0 @@ -schemaId; } - public static function fromEntity(Question $question): self + public function setSchemaId(string $schemaId): self { - return new self( - schemaId: $question->getSchema()?->getId(), - type: $question->getType(), - difficulty: $question->getDifficulty(), - title: $question->getTitle(), - description: $question->getDescription(), - answer: $question->getAnswer(), - solutionVideo: $question->getSolutionVideo(), - ); + $this->schemaId = $schemaId; + + return $this; } - public function toEntity(SchemaRepository $schemaRepository): Question + public function getType(): string { - $schema = $schemaRepository->find($this->schemaId); + return $this->type; + } - return (new Question()) - ->setSchema($schema) - ->setType($this->type) - ->setDifficulty($this->difficulty) - ->setTitle($this->title) - ->setDescription($this->description) - ->setAnswer($this->answer) - ->setSolutionVideo($this->solutionVideo); + public function setType(string $type): self + { + $this->type = $type; + + return $this; } - /** - * @throws \InvalidArgumentException - */ - public static function fromJsonObject(object $json): self + public function getDifficulty(): QuestionDifficulty { - /** @var \stdClass $json */ - $json = clone $json; + return $this->difficulty; + } - if (!isset($json->schemaId)) { - throw new \InvalidArgumentException('schemaId is required'); - } - if (!\is_string($json->schemaId)) { - throw new \InvalidArgumentException('schemaId must be of type string'); - } + public function setDifficulty(QuestionDifficulty $difficulty): self + { + $this->difficulty = $difficulty; - if (!isset($json->type)) { - throw new \InvalidArgumentException('type is required'); - } - if (!\is_string($json->type)) { - throw new \InvalidArgumentException('type must be of type string'); - } + return $this; + } - if (!isset($json->difficulty)) { - throw new \InvalidArgumentException('difficulty is required'); - } - if (!\is_string($json->difficulty)) { - throw new \InvalidArgumentException('difficulty must be of type string'); - } + public function getTitle(): string + { + return $this->title; + } - if (!isset($json->title)) { - throw new \InvalidArgumentException('title is required'); - } - if (!\is_string($json->title)) { - throw new \InvalidArgumentException('title must be of type string'); - } + public function setTitle(string $title): self + { + $this->title = $title; - if (!isset($json->answer)) { - throw new \InvalidArgumentException('answer is required'); - } - if (!\is_string($json->answer)) { - throw new \InvalidArgumentException('answer must be of type string'); - } + return $this; + } - if (!isset($json->description)) { - $json->description = null; - } - if (!\is_string($json->description) && null !== $json->description) { - throw new \InvalidArgumentException('description must be of type string'); - } + public function getDescription(): ?string + { + return $this->description; + } - if (!isset($json->solutionVideo)) { - $json->solutionVideo = null; - } - if (!\is_string($json->solutionVideo) && null !== $json->solutionVideo) { - throw new \InvalidArgumentException('solutionVideo must be of type string'); + public function setDescription(?string $description): self + { + $this->description = $description; + + return $this; + } + + public function getAnswer(): string + { + return $this->answer; + } + + public function setAnswer(string $answer): self + { + $this->answer = $answer; + + return $this; + } + + public function getSolutionVideo(): ?string + { + return $this->solutionVideo; + } + + public function setSolutionVideo(?string $solutionVideo): self + { + $this->solutionVideo = $solutionVideo; + + return $this; + } + + public static function fromEntity(Question $question): self + { + return (new self()) + ->setSchemaId($question->getSchema()->getId()) + ->setType($question->getType()) + ->setDifficulty($question->getDifficulty()) + ->setTitle($question->getTitle()) + ->setDescription($question->getDescription()) + ->setAnswer($question->getAnswer()) + ->setSolutionVideo($question->getSolutionVideo()); + } + + public function toEntity(SchemaRepository $schemaRepository): Question + { + $schema = $schemaRepository->find($this->schemaId); + if (null === $schema) { + throw new \RuntimeException("Schema $this->schemaId not found"); } - return new self( - schemaId: $json->schemaId, - type: $json->type, - difficulty: QuestionDifficulty::fromString($json->difficulty), - title: $json->title, - description: $json->description, - answer: $json->answer, - solutionVideo: $json->solutionVideo, - ); + return (new Question()) + ->setSchema($schema) + ->setType($this->type) + ->setDifficulty($this->difficulty) + ->setTitle($this->title) + ->setDescription($this->description) + ->setAnswer($this->answer) + ->setSolutionVideo($this->solutionVideo); } } diff --git a/src/Entity/ExportDto/SchemaDto.php b/src/Entity/ExportDto/SchemaDto.php index 2d397d3..03c7781 100644 --- a/src/Entity/ExportDto/SchemaDto.php +++ b/src/Entity/ExportDto/SchemaDto.php @@ -6,24 +6,68 @@ use App\Entity\Schema; -readonly class SchemaDto implements Importable +class SchemaDto { - public function __construct( - public string $id, - public ?string $picture, - public ?string $description, - public string $schema, - ) { + private string $id; + private ?string $picture = null; + private ?string $description = null; + private string $schema; + + public function getId(): string + { + return $this->id; + } + + public function setId(string $id): self + { + $this->id = $id; + + return $this; + } + + public function getPicture(): ?string + { + return $this->picture; + } + + public function setPicture(?string $picture): self + { + $this->picture = $picture; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): self + { + $this->description = $description; + + return $this; + } + + public function getSchema(): string + { + return $this->schema; + } + + public function setSchema(string $schema): self + { + $this->schema = $schema; + + return $this; } public static function fromEntity(Schema $schema): self { - return new self( - id: $schema->getId(), - picture: $schema->getPicture(), - description: $schema->getDescription(), - schema: $schema->getSchema(), - ); + return (new self()) + ->setId($schema->getId()) + ->setPicture($schema->getPicture()) + ->setDescription($schema->getDescription()) + ->setSchema($schema->getSchema()); } public function toEntity(): Schema @@ -34,48 +78,4 @@ public function toEntity(): Schema ->setDescription($this->description) ->setSchema($this->schema); } - - /** - * @throws \InvalidArgumentException - */ - public static function fromJsonObject(object $json): self - { - /** @var \stdClass $json */ - $json = clone $json; - - if (!isset($json->id)) { - throw new \InvalidArgumentException('The id must be set.'); - } - if (!\is_string($json->id)) { - throw new \InvalidArgumentException('The id must be a string.'); - } - - if (!isset($json->picture)) { - $json->picture = null; - } - if (!\is_string($json->picture) && null !== $json->picture) { - throw new \InvalidArgumentException('The picture must be a string.'); - } - - if (!isset($json->description)) { - $json->description = null; - } - if (!\is_string($json->description) && null !== $json->description) { - throw new \InvalidArgumentException('The description must be a string.'); - } - - if (!isset($json->schema)) { - throw new \InvalidArgumentException('The schema must be set.'); - } - if (!\is_string($json->schema)) { - throw new \InvalidArgumentException('The schema must be a string.'); - } - - return new self( - id: $json->id, - picture: $json->picture, - description: $json->description, - schema: $json->schema, - ); - } } diff --git a/src/Entity/Form/PasswordChangeModel.php b/src/Entity/Form/PasswordChangeModel.php index 5e634c3..65720a0 100644 --- a/src/Entity/Form/PasswordChangeModel.php +++ b/src/Entity/Form/PasswordChangeModel.php @@ -9,9 +9,6 @@ use Symfony\Component\Security\Core\Validator\Constraints as SecurityAssert; use Symfony\Component\Validator\Constraints as Assert; -/** - * The plain old PHP object for the change password form. - */ class PasswordChangeModel { #[SecurityAssert\UserPassword] diff --git a/src/Entity/Group.php b/src/Entity/Group.php index 133d6f1..ac9df46 100644 --- a/src/Entity/Group.php +++ b/src/Entity/Group.php @@ -23,7 +23,7 @@ class Group private ?int $id = null; #[ORM\Column(length: 255)] - private ?string $name = null; + private string $name; #[ORM\Column(length: 255, nullable: true)] private ?string $description = null; @@ -36,7 +36,7 @@ class Group public function __toString(): string { - return "{$this->name}"; + return $this->name; } public function __construct() @@ -56,7 +56,7 @@ public function setId(?int $id): static return $this; } - public function getName(): ?string + public function getName(): string { return $this->name; } @@ -101,7 +101,7 @@ public function addUser(User $user): static public function removeUser(User $user): static { if ($this->users->removeElement($user)) { - // set the owning side to null (unless already changed) + // set the owning side to a default class (unless already changed) if ($user->getGroup() === $this) { $user->setGroup(null); } diff --git a/src/Entity/HintOpenEvent.php b/src/Entity/HintOpenEvent.php index c0fe977..a3f33f2 100644 --- a/src/Entity/HintOpenEvent.php +++ b/src/Entity/HintOpenEvent.php @@ -13,31 +13,31 @@ class HintOpenEvent extends BaseEvent { #[ORM\ManyToOne(inversedBy: 'hintOpenEvents')] #[ORM\JoinColumn(nullable: false)] - private ?User $opener = null; + private User $opener; #[ORM\ManyToOne(inversedBy: 'hintOpenEvents')] #[ORM\JoinColumn(nullable: false)] - private ?Question $question = null; + private Question $question; #[ORM\Column(type: Types::TEXT)] - private ?string $response = null; + private string $response; #[ORM\Column(type: Types::TEXT)] - private ?string $query = null; + private string $query; - public function getOpener(): ?User + public function getOpener(): User { return $this->opener; } - public function setOpener(?User $opener): static + public function setOpener(User $opener): static { $this->opener = $opener; return $this; } - public function getResponse(): ?string + public function getResponse(): string { return $this->response; } @@ -49,7 +49,7 @@ public function setResponse(string $response): static return $this; } - public function getQuery(): ?string + public function getQuery(): string { return $this->query; } @@ -61,12 +61,12 @@ public function setQuery(string $query): static return $this; } - public function getQuestion(): ?Question + public function getQuestion(): Question { return $this->question; } - public function setQuestion(?Question $question): static + public function setQuestion(Question $question): static { $this->question = $question; diff --git a/src/Entity/LoginEvent.php b/src/Entity/LoginEvent.php index a006274..5ca4c66 100644 --- a/src/Entity/LoginEvent.php +++ b/src/Entity/LoginEvent.php @@ -12,14 +12,14 @@ class LoginEvent extends BaseEvent { #[ORM\ManyToOne(inversedBy: 'loginEvents')] #[ORM\JoinColumn(nullable: false)] - private ?User $account = null; + private User $account; - public function getAccount(): ?User + public function getAccount(): User { return $this->account; } - public function setAccount(?User $account): static + public function setAccount(User $account): static { $this->account = $account; diff --git a/src/Entity/Question.php b/src/Entity/Question.php index 3474022..8a6b041 100644 --- a/src/Entity/Question.php +++ b/src/Entity/Question.php @@ -25,22 +25,22 @@ class Question #[ORM\ManyToOne(inversedBy: 'questions')] #[ORM\JoinColumn(nullable: false)] - private ?Schema $schema = null; + private Schema $schema; #[ORM\Column(length: 255)] - private ?string $type = null; + private string $type; #[ORM\Column(enumType: QuestionDifficulty::class)] - private ?QuestionDifficulty $difficulty = null; + private QuestionDifficulty $difficulty; #[ORM\Column(length: 255)] - private ?string $title = null; + private string $title; #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $description = null; #[ORM\Column(type: Types::TEXT)] - private ?string $answer = null; + private string $answer; #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $solution_video = null; @@ -79,7 +79,7 @@ public function __construct() public function __toString(): string { - return "#{$this->id}: {$this->title}"; + return "#$this->id: $this->title"; } #[Groups(['searchable'])] @@ -88,12 +88,12 @@ public function getId(): int return (int) $this->id; } - public function getSchema(): ?Schema + public function getSchema(): Schema { return $this->schema; } - public function setSchema(?Schema $schema): static + public function setSchema(Schema $schema): static { $this->schema = $schema; @@ -103,7 +103,7 @@ public function setSchema(?Schema $schema): static #[Groups(['searchable'])] public function getType(): string { - return (string) $this->type; + return $this->type; } public function setType(string $type): static @@ -129,7 +129,7 @@ public function setDifficulty(QuestionDifficulty $difficulty): static #[Groups(['searchable'])] public function getTitle(): string { - return (string) $this->title; + return $this->title; } public function setTitle(string $title): static @@ -154,7 +154,7 @@ public function setDescription(?string $description): static public function getAnswer(): string { - return (string) $this->answer; + return $this->answer; } public function setAnswer(string $answer): static @@ -197,9 +197,9 @@ public function addSolutionEvent(SolutionEvent $solutionEvent): static public function removeSolutionEvent(SolutionEvent $solutionEvent): static { if ($this->solutionEvents->removeElement($solutionEvent)) { - // set the owning side to null (unless already changed) + // set the owning side to a default class (unless already changed) if ($solutionEvent->getQuestion() === $this) { - $solutionEvent->setQuestion(null); + $solutionEvent->setQuestion(new self()); } } @@ -227,9 +227,9 @@ public function addSolutionVideoEvent(SolutionVideoEvent $solutionVideoEvent): s public function removeSolutionVideoEvent(SolutionVideoEvent $solutionVideoEvent): static { if ($this->solutionVideoEvents->removeElement($solutionVideoEvent)) { - // set the owning side to null (unless already changed) + // set the owning side to a default class (unless already changed) if ($solutionVideoEvent->getQuestion() === $this) { - $solutionVideoEvent->setQuestion(null); + $solutionVideoEvent->setQuestion(new self()); } } @@ -257,9 +257,9 @@ public function addComment(Comment $comment): static public function removeComment(Comment $comment): static { if ($this->comments->removeElement($comment)) { - // set the owning side to null (unless already changed) + // set the owning side to a default class (unless already changed) if ($comment->getQuestion() === $this) { - $comment->setQuestion(null); + $comment->setQuestion(new self()); } } @@ -287,9 +287,9 @@ public function addHintOpenEvent(HintOpenEvent $hintOpenEvent): static public function removeHintOpenEvent(HintOpenEvent $hintOpenEvent): static { if ($this->hintOpenEvents->removeElement($hintOpenEvent)) { - // set the owning side to null (unless already changed) + // set the owning side to a default class (unless already changed) if ($hintOpenEvent->getQuestion() === $this) { - $hintOpenEvent->setQuestion(null); + $hintOpenEvent->setQuestion(new self()); } } diff --git a/src/Entity/Schema.php b/src/Entity/Schema.php index 8cd0ab9..47dcb38 100644 --- a/src/Entity/Schema.php +++ b/src/Entity/Schema.php @@ -19,7 +19,7 @@ class Schema #[ORM\Id] #[ORM\Column(type: Types::STRING, length: 255)] - private ?string $id = null; + private string $id; #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $picture = null; @@ -28,7 +28,7 @@ class Schema private ?string $description = null; #[ORM\Column(type: Types::TEXT)] - private ?string $schema = null; + private string $schema; /** * @var Collection @@ -48,7 +48,7 @@ public function __toString(): string public function getId(): string { - return (string) $this->id; + return $this->id; } public function setId(string $id): static @@ -84,7 +84,7 @@ public function setDescription(?string $description): static public function getSchema(): string { - return (string) $this->schema; + return $this->schema; } public function setSchema(string $schema): static @@ -115,9 +115,9 @@ public function addQuestion(Question $question): static public function removeQuestion(Question $question): static { if ($this->questions->removeElement($question)) { - // set the owning side to null (unless already changed) + // set the owning side to a default class (unless already changed) if ($question->getSchema() === $this) { - $question->setSchema(null); + $question->setSchema(new self()); } } diff --git a/src/Entity/SolutionEvent.php b/src/Entity/SolutionEvent.php index 2c4a4d0..ea39e90 100644 --- a/src/Entity/SolutionEvent.php +++ b/src/Entity/SolutionEvent.php @@ -13,36 +13,36 @@ class SolutionEvent extends BaseEvent { #[ORM\ManyToOne(inversedBy: 'solutionEvents')] #[ORM\JoinColumn(nullable: false)] - private ?User $submitter = null; + private User $submitter; #[ORM\ManyToOne(inversedBy: 'solutionEvents')] #[ORM\JoinColumn(nullable: false)] - private ?Question $question = null; + private Question $question; #[ORM\Column(enumType: SolutionEventStatus::class)] - private ?SolutionEventStatus $status = null; + private SolutionEventStatus $status; #[ORM\Column(type: Types::TEXT)] - private ?string $query = null; + private string $query; - public function getSubmitter(): ?User + public function getSubmitter(): User { return $this->submitter; } - public function setSubmitter(?User $submitter): static + public function setSubmitter(User $submitter): static { $this->submitter = $submitter; return $this; } - public function getQuestion(): ?Question + public function getQuestion(): Question { return $this->question; } - public function setQuestion(?Question $question): static + public function setQuestion(Question $question): static { $this->question = $question; @@ -63,7 +63,7 @@ public function setStatus(SolutionEventStatus $status): static public function getQuery(): string { - return (string) $this->query; + return $this->query; } public function setQuery(string $query): static diff --git a/src/Entity/SolutionVideoEvent.php b/src/Entity/SolutionVideoEvent.php index 5cec795..c1b8824 100644 --- a/src/Entity/SolutionVideoEvent.php +++ b/src/Entity/SolutionVideoEvent.php @@ -12,30 +12,30 @@ class SolutionVideoEvent extends BaseEvent { #[ORM\ManyToOne(inversedBy: 'solutionVideoEvents')] #[ORM\JoinColumn(nullable: false)] - private ?User $opener = null; + private User $opener; #[ORM\ManyToOne(inversedBy: 'solutionVideoEvents')] #[ORM\JoinColumn(nullable: false)] - private ?Question $question = null; + private Question $question; - public function getOpener(): ?User + public function getOpener(): User { return $this->opener; } - public function setOpener(?User $opener): static + public function setOpener(User $opener): static { $this->opener = $opener; return $this; } - public function getQuestion(): ?Question + public function getQuestion(): Question { return $this->question; } - public function setQuestion(?Question $question): static + public function setQuestion(Question $question): static { $this->question = $question; diff --git a/src/Entity/Trait/WithModelTimeInfo.php b/src/Entity/Trait/WithModelTimeInfo.php index 58cfc7a..a1d730b 100644 --- a/src/Entity/Trait/WithModelTimeInfo.php +++ b/src/Entity/Trait/WithModelTimeInfo.php @@ -13,13 +13,13 @@ */ trait WithModelTimeInfo { - #[ORM\Column(name: 'created_at')] - private ?\DateTimeImmutable $createdAt = null; + #[ORM\Column] + private \DateTimeImmutable $createdAt; - #[ORM\Column(name: 'updated_at')] - private ?\DateTimeImmutable $updatedAt = null; + #[ORM\Column] + private \DateTimeImmutable $updatedAt; - public function getCreatedAt(): ?\DateTimeImmutable + public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; } @@ -31,7 +31,7 @@ public function setCreatedAt(\DateTimeImmutable $createdAt): static return $this; } - public function getUpdatedAt(): ?\DateTimeImmutable + public function getUpdatedAt(): \DateTimeImmutable { return $this->updatedAt; } diff --git a/src/Entity/User.php b/src/Entity/User.php index 7839614..eb705a8 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -11,6 +11,7 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Validator\Constraints\NotBlank; #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Table(name: '`user`')] @@ -32,20 +33,20 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface private array $roles = []; /** - * @var ?string The hashed password. - * It usually contains a value. + * @var string the hashed password */ #[ORM\Column] - private ?string $password = null; + private string $password; #[ORM\ManyToOne(inversedBy: 'users')] private ?Group $group = null; #[ORM\Column(length: 255, unique: true)] - private ?string $email = null; + #[NotBlank] + private string $email; #[ORM\Column(length: 255)] - private ?string $name = null; + private string $name; /** * @var Collection @@ -110,9 +111,9 @@ public function __toString(): string */ public function getUserIdentifier(): string { - \assert(!empty($this->email), 'The email should not be empty.'); + \assert('' !== $this->email); - return (string) $this->email; + return $this->email; } /** @@ -147,7 +148,7 @@ public function setRoles(array $roles): static */ public function getPassword(): string { - return (string) $this->password; + return $this->password; } public function setPassword(string $password): static @@ -178,7 +179,7 @@ public function setGroup(?Group $group): static return $this; } - public function getEmail(): ?string + public function getEmail(): string { return $this->email; } @@ -223,9 +224,9 @@ public function addSolutionEvent(SolutionEvent $solutionEvent): static public function removeSolutionEvent(SolutionEvent $solutionEvent): static { if ($this->solutionEvents->removeElement($solutionEvent)) { - // set the owning side to null (unless already changed) + // set the owning side to a default class (unless already changed) if ($solutionEvent->getSubmitter() === $this) { - $solutionEvent->setSubmitter(null); + $solutionEvent->setSubmitter(new self()); } } @@ -253,9 +254,9 @@ public function addSolutionVideoEvent(SolutionVideoEvent $solutionVideoEvent): s public function removeSolutionVideoEvent(SolutionVideoEvent $solutionVideoEvent): static { if ($this->solutionVideoEvents->removeElement($solutionVideoEvent)) { - // set the owning side to null (unless already changed) + // set the owning side to a default class (unless already changed) if ($solutionVideoEvent->getOpener() === $this) { - $solutionVideoEvent->setOpener(null); + $solutionVideoEvent->setOpener(new self()); } } @@ -283,9 +284,9 @@ public function addComment(Comment $comment): static public function removeComment(Comment $comment): static { if ($this->comments->removeElement($comment)) { - // set the owning side to null (unless already changed) + // set the owning side to a default class (unless already changed) if ($comment->getCommenter() === $this) { - $comment->setCommenter(null); + $comment->setCommenter(new self()); } } @@ -313,9 +314,9 @@ public function addCommentLikeEvent(CommentLikeEvent $commentLikeEvent): static public function removeCommentLikeEvent(CommentLikeEvent $commentLikeEvent): static { if ($this->commentLikeEvents->removeElement($commentLikeEvent)) { - // set the owning side to null (unless already changed) + // set the owning side to a default class (unless already changed) if ($commentLikeEvent->getLiker() === $this) { - $commentLikeEvent->setLiker(null); + $commentLikeEvent->setLiker(new self()); } } @@ -343,9 +344,9 @@ public function addHintOpenEvent(HintOpenEvent $hintOpenEvent): static public function removeHintOpenEvent(HintOpenEvent $hintOpenEvent): static { if ($this->hintOpenEvents->removeElement($hintOpenEvent)) { - // set the owning side to null (unless already changed) + // set the owning side to a default class (unless already changed) if ($hintOpenEvent->getOpener() === $this) { - $hintOpenEvent->setOpener(null); + $hintOpenEvent->setOpener(new self()); } } @@ -373,9 +374,9 @@ public function addLoginEvent(LoginEvent $loginEvent): static public function removeLoginEvent(LoginEvent $loginEvent): static { if ($this->loginEvents->removeElement($loginEvent)) { - // set the owning side to null (unless already changed) + // set the owning side to a default class (unless already changed) if ($loginEvent->getAccount() === $this) { - $loginEvent->setAccount(null); + $loginEvent->setAccount(new self()); } } diff --git a/src/EventSubscriber/LoginSubscriber.php b/src/EventSubscriber/LoginSubscriber.php index d3d3040..04538c9 100644 --- a/src/EventSubscriber/LoginSubscriber.php +++ b/src/EventSubscriber/LoginSubscriber.php @@ -10,10 +10,10 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; -class LoginSubscriber implements EventSubscriberInterface +final readonly class LoginSubscriber implements EventSubscriberInterface { public function __construct( - private readonly EntityManagerInterface $entityManager, + private EntityManagerInterface $entityManager, ) { } diff --git a/src/Exception/NoSchemaException.php b/src/Exception/NoSchemaException.php deleted file mode 100644 index 5af497b..0000000 --- a/src/Exception/NoSchemaException.php +++ /dev/null @@ -1,18 +0,0 @@ -createQueryBuilder('l') - ->select('COUNT(l.id)') - ->andWhere('l.account = :user') - ->setParameter('user', $user) - ->getQuery() - ->getSingleScalarResult(); - - \assert(\is_int($loginCount)); - - return $loginCount; - } } diff --git a/src/Repository/QuestionRepository.php b/src/Repository/QuestionRepository.php index acac96d..c06671a 100644 --- a/src/Repository/QuestionRepository.php +++ b/src/Repository/QuestionRepository.php @@ -78,12 +78,12 @@ public function getPreviousPage(int $page): ?int public function search(SearchService $searchService, ?string $query, ?string $type, int $page, ?int $pageSize): array { $filters = []; - if ($type) { + if (null !== $type && '' !== $type) { $escapedType = addslashes($type); - $filters[] = "type = \"{$escapedType}\""; + $filters[] = "type = \"$escapedType\""; } - return $searchService->search($this->getEntityManager(), Question::class, $query ?: '', [ + return $searchService->search($this->getEntityManager(), Question::class, $query ?? '', [ 'limit' => $pageSize ?? self::$pageSize, 'page' => $page, 'filter' => $filters, @@ -116,12 +116,12 @@ public function calculateTotalPages(?string $query, ?string $type, ?int $pageSiz $qb = $this->createQueryBuilder('q'); $qb = $qb->select('COUNT(q.id)'); - if ($query) { + if (null !== $query) { $qb = $qb->andWhere('q.title LIKE :query') ->setParameter('query', "%$query%"); } - if ($type) { + if (null !== $type) { $qb = $qb->andWhere('q.type = :type') ->setParameter('type', $type); } @@ -143,7 +143,9 @@ public function listTypes(): array $qb = $qb->select('q.type') ->distinct(); - /** @var string[] $result */ + /** + * @var string[] $result + */ $result = $qb->getQuery()->getSingleColumnResult(); return $result; diff --git a/src/Repository/SolutionEventRepository.php b/src/Repository/SolutionEventRepository.php index 61b55b1..6baf376 100644 --- a/src/Repository/SolutionEventRepository.php +++ b/src/Repository/SolutionEventRepository.php @@ -41,7 +41,7 @@ public function findSolvedQuestions(User $user): array foreach ($solvedQuestionEvents as $event) { $question = $event->getQuestion(); - if (null !== $question && !isset($questions[$question->getId()])) { + if (!isset($questions[$question->getId()])) { $questions[$question->getId()] = $question; } } @@ -159,7 +159,7 @@ public function listLeaderboard(?Group $group, string $interval): array ->setParameter('startedFrom', $startedFrom); // filter by group - if ($group) { + if (null !== $group) { $qb = $qb->andWhere('u.group = :group') ->setParameter('group', $group); } else { @@ -205,7 +205,7 @@ public function getTotalAttempts(Question $question, ?Group $group): array ->where('se.question = :question') ->setParameter('question', $question); - if ($group) { + if (null !== $group) { $qb->andWhere('submitter.group = :group') ->setParameter('group', $group); } else { diff --git a/src/Repository/SolutionVideoEventRepository.php b/src/Repository/SolutionVideoEventRepository.php index f1650f1..3d42aae 100644 --- a/src/Repository/SolutionVideoEventRepository.php +++ b/src/Repository/SolutionVideoEventRepository.php @@ -20,26 +20,6 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, SolutionVideoEvent::class); } - /** - * Find all events this user has triggered. - * - * @return SolutionVideoEvent[] - */ - public function findByUser(User $user): array - { - $qb = $this->createQueryBuilder('sve') - ->distinct() - ->where('sve.opener = :user') - ->setParameter('user', $user); - - $query = $qb->getQuery(); - - /** @var SolutionVideoEvent[] $result */ - $result = $query->getResult(); - - return $result; - } - /** * Check if the user has triggered the event. */ diff --git a/src/Service/DbRunner.php b/src/Service/DbRunner.php index cb121ca..4251802 100644 --- a/src/Service/DbRunner.php +++ b/src/Service/DbRunner.php @@ -19,7 +19,7 @@ /** * A class to run queries on a SQLite3 database. */ -readonly class DbRunner +final readonly class DbRunner { private SqlFormatter $formatter; diff --git a/src/Service/PassRateService.php b/src/Service/PassRateService.php index db696c2..9d90529 100644 --- a/src/Service/PassRateService.php +++ b/src/Service/PassRateService.php @@ -12,7 +12,7 @@ /** * Get the pass rate of a question in the optimized matter. */ -readonly class PassRateService +final readonly class PassRateService { public function __construct( private SolutionEventRepository $solutionEventRepository, diff --git a/src/Service/PointCalculationService.php b/src/Service/PointCalculationService.php index eac6866..0789bda 100644 --- a/src/Service/PointCalculationService.php +++ b/src/Service/PointCalculationService.php @@ -107,7 +107,7 @@ protected function calculateFirstSolutionPoints(User $user): int foreach ($questions as $question) { $firstSolver = $this->listFirstSolversOfQuestion($question, $user->getGroup()); - if ($firstSolver && $firstSolver === $user->getId()) { + if (null !== $firstSolver && $firstSolver === $user->getId()) { $points += self::$firstSolverPoint; } } @@ -127,16 +127,16 @@ protected function calculateFirstSolutionPoints(User $user): int */ protected function listFirstSolversOfQuestion(Question $question, ?Group $group): ?int { - $groupId = $group ? "{$group->getId()}" : '-none'; + $groupId = null !== $group ? "{$group->getId()}" : '-none'; return $this->cache->get( - "question.q{$question->getId()}.g{$groupId}.first-solver", + "question.q{$question->getId()}.g$groupId.first-solver", function (ItemInterface $item) use ($group, $question) { $item->tag(['question', 'first-solver', 'group']); $solutionEvent = $question ->getSolutionEvents() - ->filter(fn (SolutionEvent $event) => $group === $event->getSubmitter()?->getGroup()) + ->filter(fn (SolutionEvent $event) => $group === $event->getSubmitter()->getGroup()) ->findFirst(fn ($_, SolutionEvent $event) => SolutionEventStatus::Passed === $event->getStatus()); return $solutionEvent?->getSubmitter()?->getId(); diff --git a/src/Service/Processes/DbRunnerProcessService.php b/src/Service/Processes/DbRunnerProcessService.php index 8382cf0..6f589d1 100644 --- a/src/Service/Processes/DbRunnerProcessService.php +++ b/src/Service/Processes/DbRunnerProcessService.php @@ -4,10 +4,9 @@ namespace App\Service\Processes; -use App\Exception\QueryExecuteException; -use App\Exception\SchemaExecuteException; use App\Service\Types\DbRunnerProcessPayload; use App\Service\Types\DbRunnerProcessResponse; +use App\Service\Types\SchemaDatabase; class DbRunnerProcessService extends ProcessService { @@ -17,137 +16,30 @@ public function main(object $input): object throw new \InvalidArgumentException('Invalid input type'); } - $db = $this->getPreparedDatabase($input->getSchema()); - try { - // query - try { - $result = $db->query($input->getQuery()); - } catch (\SQLite3Exception) { - throw new QueryExecuteException($db->lastErrorMsg()); - } - - if (\is_bool($result)) { - throw new QueryExecuteException("Invalid query given: '{$input->getQuery()}'"); - } - - /** - * @var array> $resultArray - */ - $resultArray = []; + $db = SchemaDatabase::get($input->getSchema()); + $result = $db->query($input->getQuery()); - try { - while ($row = $result->fetchArray(\SQLITE3_ASSOC)) { - $rowCasted = []; + /** + * @var array> $resultArray + */ + $resultArray = []; - foreach ($row as $key => $value) { - \assert(\is_string($key)); + try { + while ($row = $result->fetchArray(\SQLITE3_ASSOC)) { + $rowCasted = []; - $rowCasted[$key] = $value; - } + foreach ($row as $key => $value) { + \assert(\is_string($key)); - $resultArray[] = $rowCasted; + $rowCasted[$key] = $value; } - } finally { - $result->finalize(); + + $resultArray[] = $rowCasted; } } finally { - $db->close(); + $result->finalize(); } return new DbRunnerProcessResponse($resultArray); } - - /** - * Get the prepared isolated database that contains the schema. - * - * You should close the database after using it. - * - * @param string $schema the schema to run - * - * @return \SQLite3 the prepared SQLite3 instance - */ - private function getPreparedDatabase(string $schema): \SQLite3 - { - $schemaDbFile = $this->createDatabase($schema); - $schemaDb = $this->createSqliteInstance($schemaDbFile); - - try { - $isolatedDb = $this->createSqliteInstance(':memory:'); - - $schemaDb->backup($isolatedDb); - - return $isolatedDb; - } finally { - $schemaDb->close(); - } - } - - /** - * Create the prepared database with the schema. - * - * It returns the filename containing the database. - * If the database already exists, it will return the filename only. - * - * @param string $schema the schema to run - * - * @return string the filename containing the database - * - * @throws SchemaExecuteException if the schema could not be executed - */ - private function createDatabase(string $schema): string - { - $tmpdir = sys_get_temp_dir(); - $hash = hash('sha3-256', $schema); - $dbfile = "$tmpdir/dbrunner_$hash.db"; - - if (file_exists($dbfile)) { - return $dbfile; - } - - $db = $this->createSqliteInstance($dbfile); - - try { - $db->exec($schema); - } catch (\SQLite3Exception) { - throw new SchemaExecuteException($db->lastErrorMsg()); - } finally { - $db->close(); - } - - return $dbfile; - } - - /** - * Create a SQLite instance in memory with the schema. - * - * @param string $filename the filename to create the SQLite instance - * - * @return \SQLite3 the SQLite instance - */ - private function createSqliteInstance(string $filename): \SQLite3 - { - $sqlite = new \SQLite3($filename); - $sqlite->busyTimeout(3000 /* milliseconds */); - $sqlite->enableExceptions(true); - - $dateop = fn (string $format) => fn (string $date) => (int) date( - $format, - strtotime($date) - ?: throw new \InvalidArgumentException("Failed to convert $date as $format."), - ); - - // MySQL-compatible functions - $sqlite->createFunction('YEAR', $dateop('Y'), 1); - $sqlite->createFunction('MONTH', $dateop('n'), 1); - $sqlite->createFunction('DAY', $dateop('j'), 1); - $sqlite->createFunction( - 'IF', - fn (bool $condition, mixed $true, mixed $false) => $condition ? $true : $false, - 3, - ); - - $sqlite->exec('PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;'); - - return $sqlite; - } } diff --git a/src/Service/PromptService.php b/src/Service/PromptService.php index d433f31..8199cc0 100644 --- a/src/Service/PromptService.php +++ b/src/Service/PromptService.php @@ -6,13 +6,13 @@ use Psr\Log\LoggerInterface; -class PromptService +final readonly class PromptService { protected \OpenAI\Client $client; public function __construct( - private readonly string $apiKey, - private readonly LoggerInterface $logger, + private string $apiKey, + private LoggerInterface $logger, ) { $this->client = \OpenAI::client($this->apiKey); } @@ -85,7 +85,7 @@ public function hint(string $query, string $error, string $answer): string foreach ($response->choices as $choice) { if ( 'assistant' === $choice->message->role - && $choice->message->content + && null !== $choice->message->content && 'stop' === $choice->finishReason) { return $choice->message->content; } diff --git a/src/Service/QuestionDbRunnerService.php b/src/Service/QuestionDbRunnerService.php index a5c1d2b..b8fc176 100644 --- a/src/Service/QuestionDbRunnerService.php +++ b/src/Service/QuestionDbRunnerService.php @@ -5,7 +5,6 @@ namespace App\Service; use App\Entity\Question; -use App\Exception\NoSchemaException; use App\Exception\QueryExecuteException; use App\Exception\SchemaExecuteException; use Psr\Cache\InvalidArgumentException; @@ -39,9 +38,6 @@ public function __construct( protected function getResult(Question $question, string $query): array { $schema = $question->getSchema(); - if (!$schema) { - throw new NoSchemaException($question->getId()); - } return $this->dbRunnerService->runQuery( $schema->getSchema(), diff --git a/src/Service/Types/PassRate.php b/src/Service/Types/PassRate.php index 067d080..9fb5004 100644 --- a/src/Service/Types/PassRate.php +++ b/src/Service/Types/PassRate.php @@ -34,7 +34,7 @@ public function __construct( array $attempts, ) { $this->total = \count($attempts); - $this->passed = \count(array_filter($attempts, fn (SolutionEvent $event) => SolutionEventStatus::Passed == $event->getStatus())); + $this->passed = \count(array_filter($attempts, fn (SolutionEvent $event) => SolutionEventStatus::Passed === $event->getStatus())); } /** diff --git a/src/Service/Types/SchemaDatabase.php b/src/Service/Types/SchemaDatabase.php new file mode 100644 index 0000000..f0b3aa7 --- /dev/null +++ b/src/Service/Types/SchemaDatabase.php @@ -0,0 +1,122 @@ +db->close(); + } + + /** + * Execute the query and return the result. + * + * @param string $query the query to execute + * + * @return \SQLite3Result the result of the query + * + * @throws QueryExecuteException if the query could not be executed + */ + public function query(string $query): \SQLite3Result + { + try { + $result = $this->db->query($query); + } catch (\Throwable) { + throw new QueryExecuteException($this->db->lastErrorMsg()); + } + + if (\is_bool($result)) { + throw new QueryExecuteException("Invalid query given: '$query'"); + } + + return $result; + } + + private static function setUp(\SQLite3 $db): \SQLite3 + { + $db->busyTimeout(3000 /* milliseconds */); + $db->enableExceptions(true); + + $dateop = fn (string $format) => fn (string $date) => (int) date( + $format, + ($datestr = strtotime($date)) !== false + ? $datestr + : throw new \InvalidArgumentException("Failed to convert $date as $format."), + ); + + // MySQL-compatible functions + $db->createFunction('YEAR', $dateop('Y'), 1); + $db->createFunction('MONTH', $dateop('n'), 1); + $db->createFunction('DAY', $dateop('j'), 1); + $db->createFunction( + 'IF', + fn (bool $condition, mixed $true, mixed $false) => $condition ? $true : $false, + 3, + ); + + return $db; + } + + /** + * Initialize the database and return the filename to the schema sqlite3. + * + * It does nothing if the file already exists. + * + * @param string $filename the filename to the schema sqlite3 + * @param string $schema the schema to initialize + * + * @throws SchemaExecuteException if the schema could not be executed + */ + private static function initialize(string $filename, string $schema): void + { + if (file_exists($filename)) { + return; + } + + $db = new \SQLite3($filename); + $db->enableExceptions(true); + + try { + $db->exec($schema); + $db->close(); + } catch (\Throwable) { + $lastErrorMessage = $db->lastErrorMsg(); + + // remove the file if the schema could not be executed + $db->close(); + unlink($filename); + + throw new SchemaExecuteException($lastErrorMessage); + } + } + + private static function getSchemaSqlFilename(string $schema): string + { + $tmpdir = sys_get_temp_dir(); + $schemaHash = hash('sha3-256', $schema); + + return "$tmpdir/dbrunner_$schemaHash.sql"; + } +} diff --git a/src/Twig/Components/Challenge/CommentModule/Comment.php b/src/Twig/Components/Challenge/CommentModule/Comment.php index 3a893b8..7880a79 100644 --- a/src/Twig/Components/Challenge/CommentModule/Comment.php +++ b/src/Twig/Components/Challenge/CommentModule/Comment.php @@ -47,7 +47,6 @@ public function getLiked(): bool public function isOwned(): bool { $commenter = $this->comment->getCommenter(); - \assert(null !== $commenter); return $this->currentUser->getUserIdentifier() === $commenter->getUserIdentifier(); } diff --git a/src/Twig/Components/Challenge/Description.php b/src/Twig/Components/Challenge/Description.php index 2640cda..8238072 100644 --- a/src/Twig/Components/Challenge/Description.php +++ b/src/Twig/Components/Challenge/Description.php @@ -29,7 +29,7 @@ public function getColumnsOfAnswer(): array $answer = $this->questionDbRunnerService->getAnswerResult($this->question); // check if we have at least one row - if (empty($answer)) { + if (0 === \count($answer)) { return []; } diff --git a/src/Twig/Components/Challenge/Executor.php b/src/Twig/Components/Challenge/Executor.php index f66dd20..63fb429 100644 --- a/src/Twig/Components/Challenge/Executor.php +++ b/src/Twig/Components/Challenge/Executor.php @@ -8,7 +8,6 @@ use App\Entity\SolutionEvent; use App\Entity\SolutionEventStatus; use App\Entity\User; -use App\Exception\QueryExecuteException; use App\Service\QuestionDbRunnerService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -55,23 +54,11 @@ public function execute(SerializerInterface $serializer): void ->setSubmitter($this->user) ->setQuery($this->query); - /** - * @var Payload|null $payload - */ - $payload = null; - try { $result = $this->questionDbRunnerService->getQueryResult($this->question, $this->query); - // check if the result is UTF-8 encoded - try { - json_encode($result, \JSON_THROW_ON_ERROR); - } catch (\JsonException $e) { - throw new QueryExecuteException('The result is not UTF-8 encoded.', previous: $e); - } - $answer = $this->questionDbRunnerService->getAnswerResult($this->question); - $same = $result == $answer; + $same = $result === $answer; $solutionEvent = $solutionEvent->setStatus($same ? SolutionEventStatus::Passed : SolutionEventStatus::Failed); @@ -84,15 +71,24 @@ public function execute(SerializerInterface $serializer): void $solutionEvent = $solutionEvent->setStatus(SolutionEventStatus::Failed); $payload = Payload::fromErrorWithCode(500, $e->getMessage()); - } finally { - if ($payload) { - $this->emitUp('app:challenge-payload', [ - 'payload' => $serializer->serialize($payload, 'json'), - ]); - } - - $this->entityManager->persist($solutionEvent); - $this->entityManager->flush(); } + + try { + $serializedPayload = $serializer->serialize($payload, 'json'); + } catch (\Throwable $e) { + $solutionEvent = $solutionEvent->setStatus(SolutionEventStatus::Failed); + + $serializedPayload = $serializer->serialize( + Payload::fromErrorWithCode(500, $e->getMessage()), + 'json' + ); + } + + $this->emitUp('app:challenge-payload', [ + 'payload' => $serializedPayload, + ]); + + $this->entityManager->persist($solutionEvent); + $this->entityManager->flush(); } } diff --git a/src/Twig/Components/Challenge/Instruction/Modal.php b/src/Twig/Components/Challenge/Instruction/Modal.php index a6aa2b3..fbe7df7 100644 --- a/src/Twig/Components/Challenge/Instruction/Modal.php +++ b/src/Twig/Components/Challenge/Instruction/Modal.php @@ -11,7 +11,6 @@ use App\Service\PointCalculationService; use App\Service\PromptService; use Doctrine\ORM\EntityManagerInterface; -use Psr\Log\LoggerInterface; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; @@ -43,14 +42,13 @@ public function getCost(): int /** * Ask GPT to generate an instruction for the user. * - * It will emit an event {@link \App\Twig\Components\Challenge\Instruction\Content} will listen to. + * It will emit an event {@link Content} will listen to. */ #[LiveAction] public function instruct( DbRunnerService $dbRunnerService, PromptService $promptService, TranslatorInterface $translator, - LoggerInterface $logger, SerializerInterface $serializer, EntityManagerInterface $entityManager, ): void { @@ -58,13 +56,7 @@ public function instruct( return; } - $schema = $this->question->getSchema()?->getSchema(); - if (!$schema) { - $logger->warning('No schema found for question', ['question' => $this->question->getId()]); - - return; - } - + $schema = $this->question->getSchema()->getSchema(); $answer = $this->question->getAnswer(); $hintOpenEvent = (new HintOpenEvent()) @@ -90,7 +82,7 @@ public function instruct( $hint = $promptService->hint($this->query, $e->getMessage(), $answer); } - if (isset($result) && $result != $answerResult) { + if (isset($result) && $result !== $answerResult) { $hint = $promptService->hint($this->query, 'Different output', $answer); } diff --git a/src/Twig/Components/Challenge/ResultPresenterModule/DiffPresenter.php b/src/Twig/Components/Challenge/ResultPresenterModule/DiffPresenter.php index 256739a..fc199c7 100644 --- a/src/Twig/Components/Challenge/ResultPresenterModule/DiffPresenter.php +++ b/src/Twig/Components/Challenge/ResultPresenterModule/DiffPresenter.php @@ -5,9 +5,9 @@ namespace App\Twig\Components\Challenge\ResultPresenterModule; use App\Twig\Components\Challenge\Payload; -use App\Utils\TablePrinter; use jblond\Diff; use jblond\Diff\Renderer\Html\SideBySide; +use Symfony\Component\Serializer\SerializerInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; @@ -19,6 +19,7 @@ final class DiffPresenter public function __construct( private readonly TranslatorInterface $translator, + private readonly SerializerInterface $serializer, ) { } @@ -32,12 +33,18 @@ public function getDiff(): ?string $leftQueryResult = $this->answerPayload->getResult()?->getQueryResult(); $rightQueryResult = $this->userPayload?->getResult()?->getQueryResult(); - if (!$leftQueryResult || !$rightQueryResult) { + if (null === $leftQueryResult || null === $rightQueryResult) { return null; } - $left = TablePrinter::toStringTable($leftQueryResult); - $right = TablePrinter::toStringTable($rightQueryResult); + $left = $this->serializer->serialize($leftQueryResult, 'csv', [ + 'csv_delimiter' => "\t", + 'csv_enclosure' => ' ', + ]); + $right = $this->serializer->serialize($rightQueryResult, 'csv', [ + 'csv_delimiter' => "\t", + 'csv_enclosure' => ' ', + ]); $diff = new Diff(explode("\n", $left), explode("\n", $right)); $renderer = new SideBySide([ @@ -46,7 +53,7 @@ public function getDiff(): ?string ]); $result = $diff->render($renderer); - if (!$result) { + if (null === $result || false === $result) { return ''; } diff --git a/src/Twig/Components/Challenge/ResultPresenterModule/Table.php b/src/Twig/Components/Challenge/ResultPresenterModule/Table.php index 0cec975..6e83631 100644 --- a/src/Twig/Components/Challenge/ResultPresenterModule/Table.php +++ b/src/Twig/Components/Challenge/ResultPresenterModule/Table.php @@ -25,7 +25,7 @@ final class Table */ public function getHeader(): array { - if (!$this->result) { + if (0 === \count($this->result)) { return []; } diff --git a/src/Twig/Components/Challenge/SolutionVideoModal.php b/src/Twig/Components/Challenge/SolutionVideoModal.php index 12e69cf..0a6516a 100644 --- a/src/Twig/Components/Challenge/SolutionVideoModal.php +++ b/src/Twig/Components/Challenge/SolutionVideoModal.php @@ -6,13 +6,10 @@ use App\Entity\Question; use App\Entity\QuestionDifficulty; -use App\Entity\SolutionVideoEvent; use App\Entity\User; use App\Repository\SolutionVideoEventRepository; use App\Service\PointCalculationService; -use Doctrine\ORM\EntityManagerInterface; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; -use Symfony\UX\LiveComponent\Attribute\LiveAction; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\ComponentToolsTrait; use Symfony\UX\LiveComponent\DefaultActionTrait; @@ -30,7 +27,6 @@ final class SolutionVideoModal public User $user; public function __construct( - private readonly EntityManagerInterface $entityManager, private readonly SolutionVideoEventRepository $solutionVideoEventRepository, ) { } @@ -49,28 +45,4 @@ public function getUnlocked(): bool { return $this->solutionVideoEventRepository->hasTriggered($this->user, $this->question); } - - /** - * Get the solution video. - * - * It writes the solution video event to the database, - * then emits the `challenge:solution-video:open` event - * for the solution video. - * - * Our Stimulus controller handles the `window.open()` call - * for the video. - */ - #[LiveAction] - public function openSolutionVideo(): void - { - $event = (new SolutionVideoEvent()) - ->setQuestion($this->question) - ->setOpener($this->user); - $this->entityManager->persist($event); - $this->entityManager->flush(); - - $this->dispatchBrowserEvent('challenge:solution-video:open', [ - 'solutionVideo' => $this->question->getSolutionVideo(), - ]); - } } diff --git a/src/Twig/Components/Challenge/SolveState.php b/src/Twig/Components/Challenge/SolveState.php index 0d33a3d..76fc616 100644 --- a/src/Twig/Components/Challenge/SolveState.php +++ b/src/Twig/Components/Challenge/SolveState.php @@ -15,6 +15,6 @@ enum SolveState: string implements TranslatableInterface public function trans(TranslatorInterface $translator, ?string $locale = null): string { - return $translator->trans("challenge.solve-state.{$this->value}", locale: $locale); + return $translator->trans("challenge.solve-state.$this->value", locale: $locale); } } diff --git a/src/Utils/TablePrinter.php b/src/Utils/TablePrinter.php deleted file mode 100644 index c8f8919..0000000 --- a/src/Utils/TablePrinter.php +++ /dev/null @@ -1,85 +0,0 @@ -> $table The table to turn into a string - * - * @return string The string representation of the table - */ - public static function toStringTable(array $table): string - { - $result = ''; - - if (0 === \count($table)) { - return $result; - } - - $columns = array_keys($table[0]); - - // calculate the max width of each column - $columnWidths = array_map(fn ($column) => \strlen(self::mixedToString($column)), $columns); - - foreach ($table as $row) { - foreach ($columns as $i => $column) { - if (null === $row[$column]) { - $row[$column] = 'NULL'; - } - - $columnWidths[$i] = max($columnWidths[$i], \strlen(self::mixedToString($row[$column]))); - } - } - - // print the header - - $header = ''; - - foreach ($columns as $i => $column) { - $header .= str_pad(self::mixedToString($column), $columnWidths[$i] + 2); - } - - $result .= trim($header)."\n"; - - // print the rows - foreach ($table as $row) { - $line = ''; - - foreach ($columns as $i => $column) { - if (null === $row[$column]) { - $row[$column] = 'NULL'; - } - - $line .= str_pad(self::mixedToString($row[$column]), $columnWidths[$i] + 2); - } - - $result .= trim($line)."\n"; - } - - return $result; - } - - public static function mixedToString(mixed $value): string - { - // make sure if $value is the primitive type - if (\is_string($value) || \is_int($value) || \is_float($value) || \is_bool($value)) { - return (string) $value; - } - - // if $value == null, it shows as 'NULL' - if (null === $value) { - return 'NULL'; - } - - // for other cases, we export it - $exported = var_export($value, true); - - // remove the new line character and the leading space - return str_replace(["\n", ' ', ',)'], ['', '', ')'], $exported); - } -} diff --git a/templates/bundles/TwigBundle/Exception/error.html.twig b/templates/bundles/TwigBundle/Exception/error.html.twig index 15dd5a7..50e6d79 100644 --- a/templates/bundles/TwigBundle/Exception/error.html.twig +++ b/templates/bundles/TwigBundle/Exception/error.html.twig @@ -8,7 +8,7 @@

系統出錯 😰

系統發生 HTTP {{ status_code }} {{ status_text }} 錯誤。

- 請聯絡系統管理員進行修正。 + 請聯絡系統管理員進行修正,並附上下面錯誤資訊的所有內容。 如果你想挑戰修正這個問題,下方為相關的堆疊資訊,您可以到 GitHub 上送交修正。

diff --git a/templates/complementary/index.html.twig b/templates/complementary/index.html.twig index 26be583..80415c3 100644 --- a/templates/complementary/index.html.twig +++ b/templates/complementary/index.html.twig @@ -1,42 +1,46 @@ {% extends 'app.html.twig' %} -{% block nav %}{% endblock %} +{% block nav %} + {% endblock %} {% block title %}補充資料{% endblock %} {% block app %} -
-

補充資料

-
+
+
+

補充資料

+
-
-

Schema SQLs

+
+

Schema SQLs

-
- {% for schema in schemas %} -
-
- {% if schema.picture %} - {{ schema.id }} 的結構圖 - {% endif %} +
+ {% for schema in schemas %} +
+
+ {% if schema.picture %} + {{ schema.id }} 的結構圖 + {% endif %} -
-
{{ schema.id }}
-
- {{ schema.description|striptags }} +
+
{{ schema.id }}
+
+ {{ schema.description|striptags }} -

適用題目:{{ - schema.questions - |sort - |map((question) => "\##{question.id}") - |join('、') - ?: '無' - }}

+

適用題目:{{ schema.questions + |sort + |map((question) => "\##{question.id}") + |join('、') + ?: '無' }}

+
+ 下載 SQL 檔案 +
- 下載 SQL 檔案
-
+ {% endfor %}
- {% endfor %} -
-
+
+
+ {% endblock %} diff --git a/templates/components/Challenge/Comment.html.twig b/templates/components/Challenge/Comment.html.twig index 4fe453c..2dc225a 100644 --- a/templates/components/Challenge/Comment.html.twig +++ b/templates/components/Challenge/Comment.html.twig @@ -1,9 +1,9 @@ -
-
+
+
-
+
{% for comment in this.comments %} {% else %} diff --git a/templates/components/Challenge/CommentModule/Comment.html.twig b/templates/components/Challenge/CommentModule/Comment.html.twig index 37577d1..37c75eb 100644 --- a/templates/components/Challenge/CommentModule/Comment.html.twig +++ b/templates/components/Challenge/CommentModule/Comment.html.twig @@ -1,11 +1,11 @@ {% set previewCommentCharLimit = 30 %} {% set plainContent = comment.content|striptags %} - + {# modals #} {% if this.owned %} {% endif %} -
+
{{ comment.commenter.name }} diff --git a/templates/components/Challenge/CommentModule/CommentForm.html.twig b/templates/components/Challenge/CommentModule/CommentForm.html.twig index 0fecebc..d4487bf 100644 --- a/templates/components/Challenge/CommentModule/CommentForm.html.twig +++ b/templates/components/Challenge/CommentModule/CommentForm.html.twig @@ -1,4 +1,4 @@ - + {{ form_start(form, { attr: { 'data-action': 'live#action:prevent', diff --git a/templates/components/Challenge/Header.html.twig b/templates/components/Challenge/Header.html.twig index eb8a9c8..51ddf6a 100644 --- a/templates/components/Challenge/Header.html.twig +++ b/templates/components/Challenge/Header.html.twig @@ -1,12 +1,12 @@ - -

#{{ question.id }} {{ question.title }}

-