Browse Source

Merge branch 'develop'

Olivier Massot 2 years ago
parent
commit
ef6963f47b
100 changed files with 1982 additions and 855 deletions
  1. 10 6
      .env
  2. 3 0
      .env.ci
  3. 6 2
      .env.docker
  4. 0 57
      .env.preprod
  5. 52 0
      .env.staging
  6. 5 1
      .env.test
  7. 13 0
      .gitignore
  8. 17 22
      .gitlab-ci.yml
  9. 165 87
      composer.json
  10. 7 30
      config/api_platform/Access/access.yaml
  11. 11 5
      config/bundles.php
  12. 178 176
      config/opentalent/enum.yaml
  13. 18 2
      config/opentalent/modulesbyconditions.yaml
  14. 6 4
      config/opentalent/products.yaml
  15. 55 0
      config/opentalent/subdomains.yaml
  16. 9 4
      config/packages/api_platform.yaml
  17. 0 0
      config/packages/dh_auditor.yaml.ori
  18. 2 0
      config/packages/docker/hautelook_alice.yaml
  19. 13 14
      config/packages/docker/monolog.yaml
  20. 9 0
      config/packages/docker/nelmio_alice.yaml
  21. 3 3
      config/packages/docker/security.yaml
  22. 4 0
      config/packages/docker/zenstruck_foundry.yaml
  23. 4 0
      config/packages/doctrine.yaml
  24. 3 1
      config/packages/framework.yaml
  25. 2 2
      config/packages/messenger.yaml
  26. 3 0
      config/packages/monolog.yaml
  27. 0 8
      config/packages/prod/deprecations.yaml
  28. 24 25
      config/packages/prod/monolog.yaml
  29. 4 0
      config/packages/ramsey_uuid_doctrine.yaml
  30. 12 9
      config/packages/security.yaml
  31. 4 0
      config/packages/staging/debug.yaml
  32. 4 0
      config/packages/staging/framework.yaml
  33. 2 0
      config/packages/staging/hautelook_alice.yaml
  34. 12 0
      config/packages/staging/monolog.yaml
  35. 9 0
      config/packages/staging/nelmio_alice.yaml
  36. 6 0
      config/packages/staging/security.yaml
  37. 2 0
      config/packages/staging/twig.yaml
  38. 3 0
      config/packages/staging/validator.yaml
  39. 0 0
      config/packages/staging/web_profiler.yaml
  40. 4 0
      config/packages/staging/zenstruck_foundry.yaml
  41. 2 2
      config/packages/test/framework.yaml
  42. 11 0
      config/packages/test/security.yaml
  43. 4 0
      config/packages/uid.yaml
  44. 7 1
      config/routes.yaml
  45. 0 7
      config/routes/annotations.yaml
  46. 0 0
      config/routes/dh_auditor.yaml.ori
  47. 0 0
      config/routes/prod/.gitkeep
  48. 3 0
      config/routes/staging/framework.yaml
  49. 0 0
      config/routes/test/.gitkeep
  50. 26 13
      config/services.yaml
  51. 21 0
      config/services/api-platform.yaml
  52. 7 0
      config/services/monolog.yaml
  53. 0 24
      doc/bindfile.md
  54. 74 0
      doc/internal_requests.md
  55. 186 0
      doc/security.md
  56. 53 0
      doc/subdomain.md
  57. 20 0
      docker-compose.override.yml
  58. 44 0
      docker-compose.yml
  59. 0 0
      fixtures/.gitignore
  60. 1 0
      phpstan.json
  61. 12 0
      phpstan.neon.dist
  62. 17 5
      phpunit.xml.dist
  63. 14 0
      public/.htaccess
  64. 32 0
      readme.md
  65. 19 25
      rector.php
  66. 2 1
      sql/schema-extensions/002-view_federation_structures.sql
  67. 0 16
      src/Annotation/ActivityYearConstraintAware.php
  68. 0 16
      src/Annotation/DateTimeConstraintAware.php
  69. 0 0
      src/ApiResource/.gitignore
  70. 16 31
      src/ApiResources/Access/AdminAccess.php
  71. 48 0
      src/ApiResources/Core/File/DownloadRequest.php
  72. 16 16
      src/ApiResources/Cotisation/Cotisation.php
  73. 15 16
      src/ApiResources/Dolibarr/DolibarrAccount.php
  74. 6 7
      src/ApiResources/Dolibarr/DolibarrBill.php
  75. 6 6
      src/ApiResources/Dolibarr/DolibarrContract.php
  76. 6 6
      src/ApiResources/Dolibarr/DolibarrContractLine.php
  77. 0 44
      src/ApiResources/DownloadRequest.php
  78. 20 11
      src/ApiResources/Enum/Enum.php
  79. 1 1
      src/ApiResources/Export/ExportRequest.php
  80. 1 1
      src/ApiResources/Export/ExportRequestInterface.php
  81. 33 25
      src/ApiResources/Export/LicenceCmf/LicenceCmfOrganizationER.php
  82. 12 11
      src/ApiResources/Mobyt/MobytUserStatus.php
  83. 59 0
      src/ApiResources/OnlineRegistration/RegistrationAvailability.php
  84. 48 0
      src/ApiResources/OnlineRegistration/RegistrationStatus.php
  85. 34 25
      src/ApiResources/Profile/AccessProfile.php
  86. 23 19
      src/ApiResources/Profile/OrganizationProfile.php
  87. 16 16
      src/ApiResources/Utils/GpsCoordinate.php
  88. 10 13
      src/ApiResources/Utils/Siret.php
  89. 22 0
      src/Attribute/ActivityYearConstraintAware.php
  90. 7 4
      src/Attribute/BillingSettingDefaultValue.php
  91. 22 0
      src/Attribute/DateTimeConstraintAware.php
  92. 5 4
      src/Attribute/OrganizationDefaultValue.php
  93. 53 0
      src/Commands/AddSubdomainCommand.php
  94. 216 0
      src/Commands/CronCommand.php
  95. 1 1
      src/Commands/Doctrine/SchemaUpdateCommand.php
  96. 1 1
      src/Commands/DolibarrSyncCommand.php
  97. 3 3
      src/Commands/PostUpgrade/V0_2/PostUpgradeCommand.php
  98. 0 26
      src/Controller/AuditController.php
  99. 26 0
      src/Controller/AuditController.php.ori
  100. 17 0
      src/DataFixtures/AppFixtures.php

+ 10 - 6
.env

@@ -18,10 +18,6 @@ APP_DEBUG=0
 APP_SECRET=6a76497c8658bb23e2236f97a2627df3
 ###< symfony/framework-bundle ###
 
-###> files management ###
-INTERNAL_FILES_DOWNLOAD_URI=https://api.opentalent.fr/_internal/secure/files
-###< files management ###
-
 ###> doctrine/doctrine-bundle ###
 # => defined in the .env.<environment> files
 ###< doctrine/doctrine-bundle ###
@@ -51,7 +47,10 @@ WKHTMLTOIMAGE_PATH=/usr/local/bin/wkhtmltoimage
 ###< knplabs/knp-snappy-bundle ###
 
 ###> symfony/messenger ###
-MESSENGER_TRANSPORT_DSN=doctrine://default
+# Choose one of the transports below
+# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
+# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
+MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
 ###< symfony/messenger ###
 
 ###> AdminAssos configuration ###
@@ -67,7 +66,7 @@ MAILER_DSN=smtp://localhost
 ###< symfony/mailer ###
 
 ###> bindfile populate buffer file
-BIND_FILE_BUFFER_FILE=/env/subdomain.txt
+BIND_FILE_BUFFER_FILE=var/subdomain.txt
 ###< bindfile populate buffer file
 
 ###> elasticsearch ###
@@ -104,3 +103,8 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
 ###> filename log ###
 LOG_FILE_NAME=undefined
 ###< filename log ###
+
+### Internal requests (@see doc/internal_requests.md)
+INTERNAL_REQUESTS_TOKEN=sRyfu6SZLR9StpnSKYRdl6i9wr5qs1bJQzro4DUiVyYJ2jknl
+INTERNAL_FILES_DOWNLOAD_URI=https://local.api.opentalent.fr/_internal/secure/files
+###

+ 3 - 0
.env.ci

@@ -37,3 +37,6 @@ BIND_FILE_BUFFER_FILE=var/subdomain.txt
 ###> filename log ###
 LOG_FILE_NAME=ci
 ###< filename log ###
+
+###> api v1 ###
+API_LEG_BASE_URL=https://api.ci.opentalent.fr/api

+ 6 - 2
.env.docker

@@ -7,12 +7,12 @@ DATABASE_URL=mysql://root:mysql660@db:3306/opentalent?serverVersion=5.7
 ###< doctrine/doctrine-bundle ###
 
 ###> nelmio/cors-bundle ###
-CORS_ALLOW_ORIGIN=^https?:\/\/(localhost|127\.0\.0\.1|(local.(admin|app|app-v3|frames).opentalent.fr))(:[0-9]+)?$
+CORS_ALLOW_ORIGIN=^https?:\/\/(localhost|127\.0\.0\.1|(local.(admin|app|app\-v3|frames|frames_v3).opentalent.fr))(:[0-9]+)?$
 ###< nelmio/cors-bundle ###
 
 ###> api v1 ###
 API_LEG_BASE_URL=http://nginx/
-###< files management ###
+###< api v1 ###
 
 ###> BlackFire configuration ###
 BLACKFIRE_CLIENT_ID=988fcba8-552d-48df-a9c2-035c76535b69
@@ -54,3 +54,7 @@ MAILER_DSN=smtp://mailcatcher:1025
 ###> filename log ###
 LOG_FILE_NAME=docker
 ###< filename log ###
+
+### Internal requests (@see doc/internal_requests.md)
+INTERNAL_FILES_DOWNLOAD_URI=https://nginx/_internal/secure/files
+###

+ 0 - 57
.env.preprod

@@ -1,57 +0,0 @@
-###> symfony/framework-bundle ###
-APP_DEBUG=1
-###< symfony/framework-bundle ###
-
-###> doctrine/doctrine-bundle ###
-DATABASE_URL=mysql://root:mysql2iopenservice369566@preprod:3306/opentalent?serverVersion=5.7
-###< doctrine/doctrine-bundle ###
-
-###> nelmio/cors-bundle ###
-CORS_ALLOW_ORIGIN=^https?://(localhost|127\.0\.0\.1)(:[0-9]+)$
-###< nelmio/cors-bundle ###
-
-###> api v1 ###
-API_LEG_BASE_URL=https://api.preprod.opentalent.fr/api
-###< files management ###
-
-###> typo3 client ###
-TYPO3_BASE_URI=http://preprod.opentalent.fr/ohcluses
-###< typo3 client ###
-
-###> BlackFire configuration ###
-BLACKFIRE_CLIENT_ID=988fcba8-552d-48df-a9c2-035c76535b69
-BLACKFIRE_CLIENT_TOKEN=8cfbeb263d044da9678dc2612531504da3790c308da7448e35724a5da91c136f
-BLACKFIRE_SERVER_ID=1171e53b-459b-41da-a292-80ff68cee8c2
-BLACKFIRE_SERVER_TOKEN=dbd1cfbea015fe83cccfc189a36ca3c16f3a1b43b94f50032a15e41e53548e8b
-###< BlackFire configuration ###
-
-###> AdminAssos configuration ###
-DATABASE_ADMINASSOS_URL=mysql://root:mysql2iopenservice369566@preprod:3306/adminassos?serverVersion=5.7
-###< AdminAssos configuration ###
-
-###> Audit configuration ###
-DATABASE_AUDIT_URL=mysql://root:mysql2iopenservice369566@preprod:3306/audit?serverVersion=5.7
-###< Audit configuration ###
-
-###> symfony/mercure-bundle ###
-# See https://symfony.com/doc/current/mercure.html#configuration
-# The URL of the Mercure hub, used by the app to publish updates (can be a local URL)
-MERCURE_URL=https://mercure.preprod.opentalent.fr/.well-known/mercure
-# The public URL of the Mercure hub, used by the browser to connect
-MERCURE_PUBLIC_URL=https://mercure.preprod.opentalent.fr/.well-known/mercure
-# The secret used to sign the JWTs
-MERCURE_JWT_SECRET=NQEupdREijrfYvCmF2mnvZQFL9zLKDH9RCYter6tUWzjemPqzicffhc2fSf0yEmM
-###< symfony/mercure-bundle ###
-
-###> bindfile populate buffer file
-BIND_FILE_BUFFER_FILE=var/subdomain.txt
-###< bindfile populate buffer file
-
-###> knplabs/knp-snappy-bundle ###
-WKHTMLTOPDF_PATH=/usr/bin/wkhtmltopdf
-WKHTMLTOIMAGE_PATH=/usr/bin/wkhtmltoimage
-###< knplabs/knp-snappy-bundle ###
-
-###> filename log ###
-LOG_FILE_NAME=preprod
-###< filename log ###

+ 52 - 0
.env.staging

@@ -0,0 +1,52 @@
+# Fichier d'env utilisé pour les tests fonctionnels et applicatifs
+APP_DEBUG=1
+
+###> doctrine/doctrine-bundle ###
+DATABASE_URL=mysql://root:mysql660@db:3306/opentalent_test?serverVersion=5.7
+###< doctrine/doctrine-bundle ###
+
+###> nelmio/cors-bundle ###
+CORS_ALLOW_ORIGIN=^$
+###< nelmio/cors-bundle ###
+
+####> api v1 ###
+API_LEG_BASE_URL=https://none
+####< api v1 ###
+
+###> elasticsearch ###
+ELASTICSEARCH_HOST=es
+ELASTICSEARCH_PORT=9200
+###< elasticsearch ###
+
+###> BlackFire configuration ###
+BLACKFIRE_CLIENT_ID=988fcba8-552d-48df-a9c2-035c76535b69
+BLACKFIRE_CLIENT_TOKEN=8cfbeb263d044da9678dc2612531504da3790c308da7448e35724a5da91c136f
+BLACKFIRE_SERVER_ID=1171e53b-459b-41da-a292-80ff68cee8c2
+BLACKFIRE_SERVER_TOKEN=dbd1cfbea015fe83cccfc189a36ca3c16f3a1b43b94f50032a15e41e53548e8b
+###< BlackFire configuration ###
+
+###> AdminAssos configuration ###
+DATABASE_ADMINASSOS_URL=mysql://root:mysql660@db:3306/adminassos_test?serverVersion=5.7
+###< AdminAssos configuration ###
+
+###> Audit configuration ###
+DATABASE_AUDIT_URL=mysql://root:mysql660@db:3306/audit_test?serverVersion=5.7
+###< Audit configuration ###
+
+###> typo3 client ###
+TYPO3_BASE_URI=https://none
+###< typo3 client ###
+
+###> symfony/mercure-bundle ###
+# See https://symfony.com/doc/current/mercure.html#configuration
+# The URL of the Mercure hub, used by the app to publish updates (can be a local URL)
+MERCURE_URL=https://none
+# The public URL of the Mercure hub, used by the browser to connect
+MERCURE_PUBLIC_URL=https://none
+# The secret used to sign the JWTs
+MERCURE_JWT_SECRET=gEwnJpcR8k0xE2sfBpEJzzuP2b2TXhZnzImIqCUk3j4RStBZa2pQjbEMsnGE4iGM
+###< symfony/mercure-bundle ###
+
+### Internal requests (@see doc/internal_requests.md)
+INTERNAL_FILES_DOWNLOAD_URI=https://none
+###

+ 5 - 1
.env.test

@@ -1,5 +1,5 @@
 ###> symfony/framework-bundle ###
-APP_DEBUG=0
+APP_DEBUG=1
 ###< symfony/framework-bundle ###
 
 ###> doctrine/doctrine-bundle ###
@@ -55,3 +55,7 @@ WKHTMLTOIMAGE_PATH=/usr/bin/wkhtmltoimage
 ###> filename log ###
 LOG_FILE_NAME=test
 ###< filename log ###
+
+### Internal requests (@see doc/internal_requests.md)
+INTERNAL_FILES_DOWNLOAD_URI=https://api.test1.opentalent.fr/_internal/secure/files
+###

+ 13 - 0
.gitignore

@@ -33,3 +33,16 @@ symfony.lock
 
 /coverage/
 
+###> phpunit/phpunit ###
+/phpunit.xml
+.phpunit.result.cache
+###< phpunit/phpunit ###
+
+###> phpstan ###
+.phpstan.neon
+###< phpstan ###
+public/phpstorm_debug_validator.phar
+
+public/phpstorm_debug.php
+
+public/phpstorm_index.php

+ 17 - 22
.gitlab-ci.yml

@@ -4,37 +4,32 @@ stages:
 variables:
   APP_ENV: ci
   SSH_PRIVATE_KEY: $SSH_PRIVATE_KEY
-
-before_script:
-  - apt-get -yqq update
-  - apt-get -yqq install zip unzip git openssh-client
-
-  # Run ssh-agent and add private key
-  - eval $(ssh-agent -s)
-  - mkdir -p ~/.ssh
-  - chmod 700 ~/.ssh
-  - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
-  - ssh-add <(echo "$SSH_PRIVATE_KEY")
-  - git config --global user.email "exploitation@opentalent.fr"
-  - git config --global user.name "git"
-
-  # install composer
-  - curl -sS https://composer.github.io/installer.sig > installer.sig
-  - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
-  - php -r "if (hash_file('SHA384', 'composer-setup.php') === file_get_contents('installer.sig')) { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
-  - php composer-setup.php
-  - php -r "unlink('composer-setup.php'); unlink('installer.sig');"
+  XDEBUG_MODE: coverage
 
 cache:
   paths:
     - ./vendor
 
+before_script:
+  - bash tests/ci_docker_install.sh > /dev/null
+  - php -v
+
+static_analysis:
+  script:
+    - php --version
+    - php -d memory_limit=512M vendor/bin/phpstan analyse -c phpstan.neon.dist --error-format gitlab > phpstan.json  # Display code quality in MR
+    - php -d memory_limit=512M vendor/bin/phpstan analyse -c phpstan.neon.dist  # Display in console
+  artifacts:
+    when: always
+    reports:
+      codequality: phpstan.json
+
 unit:
   stage: test
 
   script:
-    - php composer.phar --no-interaction --quiet install
-    - php bin/phpunit --configuration phpunit.xml.dist --colors=never --no-interaction
+    - php vendor/phpunit/phpunit/phpunit --configuration phpunit.xml.dist --colors=never --no-interaction --testsuite=unit
+    #- php vendor/phpunit/phpunit/phpunit --configuration phpunit.xml.dist --colors=never --no-interaction --no-coverage --testsuite=application
 
   artifacts:
     paths:

+ 165 - 87
composer.json

@@ -1,95 +1,122 @@
 {
-    "type": "project",
-    "license": "proprietary",
-    "repositories": [
-        {
-            "type": "vcs",
-            "url": "ssh://git@gitlab.2iopenservice.com/vincent/foselastica.git"
-        }
-    ],
-    "require": {
-        "php": ">=8.0",
-        "ext-ctype": "*",
-        "ext-iconv": "*",
-        "api-platform/core": "^2.6",
-        "beberlei/doctrineextensions": "^1.3",
-        "blackfire/php-sdk": "^1.23",
-        "composer/package-versions-deprecated": "^1.11",
-        "damienharper/auditor-bundle": "^5.0",
-        "doctrine/dbal": "^2.6",
-        "doctrine/doctrine-bundle": "^2.1",
-        "doctrine/doctrine-migrations-bundle": "^3.0",
-        "doctrine/orm": "^2.9",
-        "egulias/email-validator": "^3.0",
-        "jbouzekri/phumbor-bundle": "^2.1",
-        "knplabs/knp-gaufrette-bundle": "^0.7.1",
-        "knplabs/knp-snappy-bundle": "^1.9",
-        "lcobucci/jwt": "^4.1",
-        "lexik/jwt-authentication-bundle": "^2.8",
-        "lorenzo/pinky": "^1.0",
-        "myclabs/php-enum": "^1.7",
-        "nelmio/cors-bundle": "^2.1",
-        "odolbeau/phone-number-bundle": "^3.1",
-        "phpdocumentor/reflection-docblock": "^5.2",
-        "ralouphie/mimey": "^1.0",
-        "ramsey/uuid": "^4.2",
-        "symfony/asset": "5.4.*",
-        "symfony/console": "5.4.*",
-        "symfony/doctrine-messenger": "5.4.*",
-        "symfony/dotenv": "5.4.*",
-        "symfony/expression-language": "5.4.*",
-        "symfony/flex": "^1.3.1",
-        "symfony/framework-bundle": "5.4.*",
-        "symfony/http-client": "5.4.*",
-        "symfony/intl": "5.4.*",
-        "symfony/lock": "5.4.*",
-        "symfony/mailer": "^5.4",
-        "symfony/mercure": "^0.6.1",
-        "symfony/mercure-bundle": "^0.3.4",
-        "symfony/monolog-bundle": "^3.7",
-        "symfony/polyfill-intl-messageformatter": "^1.24",
-        "symfony/property-access": "5.4.*",
-        "symfony/property-info": "5.4.*",
-        "symfony/security-bundle": "5.4.*",
-        "symfony/serializer": "5.4.*",
-        "symfony/translation": "5.4.*",
-        "symfony/twig-bundle": "^5.4",
-        "symfony/uid": "5.4.*",
-        "symfony/validator": "5.4.*",
-        "symfony/yaml": "5.4.*",
-        "twig/cssinliner-extra": "^3.4",
-        "twig/extra-bundle": "^3.4",
-        "twig/inky-extra": "^3.4",
-        "vincent/foselastica": "1.2",
-        "webonyx/graphql-php": "^14.3"
+  "type": "project",
+  "license": "proprietary",
+  "repositories": [
+    {
+      "type": "vcs",
+      "url": "ssh://git@gitlab.2iopenservice.com/vincent/foselastica.git"
     },
-    "require-dev": {
-        "cyclonedx/cyclonedx-php-composer": "^3.4",
-        "rector/rector": "^0.12",
-        "symfony/debug-bundle": "5.4.*",
-        "symfony/maker-bundle": "^1.21",
-        "symfony/phpunit-bridge": "^5.4",
-        "symfony/stopwatch": "^5.4",
-        "symfony/web-profiler-bundle": "^5.4"
-    },
-    "config": {
-        "optimize-autoloader": true,
-        "preferred-install": {
-            "*": "dist"
-        },
-        "sort-packages": true,
-        "allow-plugins": {
-            "cyclonedx/cyclonedx-php-composer": true,
-            "symfony/flex": true
-        }
+    {
+      "type": "vcs",
+      "url": "ssh://git@gitlab.2iopenservice.com/opentalent/phpdocx.git"
+    }
+  ],
+  "require": {
+    "php": ">=8.1",
+    "ext-ctype": "*",
+    "ext-iconv": "*",
+    "api-platform/core": "3.1.7",
+    "beberlei/doctrineextensions": "^1.3",
+    "blackfire/php-sdk": "^1.23",
+    "composer/package-versions-deprecated": "^1.11",
+    "doctrine/dbal": "^2.6",
+    "doctrine/doctrine-bundle": "^2.1",
+    "doctrine/doctrine-migrations-bundle": "^3.0",
+    "doctrine/orm": "^2.9",
+    "egulias/email-validator": "^3.0",
+    "jbouzekri/phumbor-bundle": "^3.1.0",
+    "knplabs/knp-gaufrette-bundle": "^0.8.0",
+    "knplabs/knp-snappy-bundle": "^1.9",
+    "lcobucci/jwt": "^4.1",
+    "lexik/jwt-authentication-bundle": "^2.8",
+    "lorenzo/pinky": "^1.0",
+    "myclabs/php-enum": "^1.7",
+    "nelmio/cors-bundle": "^2.1",
+    "odolbeau/phone-number-bundle": "^3.1",
+    "phpdocumentor/reflection-docblock": "^5.2",
+    "ramsey/uuid": "^4.2",
+    "ramsey/uuid-doctrine": "^2.0",
+    "symfony/asset": "6.3.*",
+    "symfony/console": "6.3.*",
+    "symfony/doctrine-messenger": "6.3.*",
+    "symfony/dotenv": "6.3.*",
+    "symfony/error-handler": "6.3.*",
+    "symfony/expression-language": "6.3.*",
+    "symfony/flex": "^1.3.1",
+    "symfony/framework-bundle": "6.3.*",
+    "symfony/http-client": "6.3.*",
+    "symfony/intl": "6.3.*",
+    "symfony/lock": "6.3.*",
+    "symfony/mailer": "6.3.*",
+    "symfony/mercure": "^0.6.1",
+    "symfony/mercure-bundle": "^0.3.4",
+    "symfony/messenger": "6.3.*",
+    "symfony/monolog-bundle": "^3.7",
+    "symfony/polyfill-intl-icu": "^1.21",
+    "symfony/polyfill-intl-messageformatter": "^1.24",
+    "symfony/property-access": "6.3.*",
+    "symfony/property-info": "6.3.*",
+    "symfony/security-bundle": "6.3.*",
+    "symfony/serializer": "6.3.*",
+    "symfony/translation": "6.3.*",
+    "symfony/twig-bundle": "6.3.*",
+    "symfony/uid": "6.3.*",
+    "symfony/validator": "6.3.*",
+    "symfony/yaml": "6.3.*",
+    "twig/cssinliner-extra": "^3.4",
+    "twig/extra-bundle": "^3.4",
+    "twig/inky-extra": "^3.4",
+    "vincent/foselastica": "1.3.1",
+    "opentalent/phpdocx": "dev-master",
+    "webonyx/graphql-php": "^14.3",
+    "xantios/mimey": "*"
+  },
+  "require-dev": {
+    "cyclonedx/cyclonedx-php-composer": "^3.4",
+    "dg/bypass-finals": "^1.4",
+    "doctrine/doctrine-fixtures-bundle": "^3.4",
+    "hautelook/alice-bundle": "^2.11",
+    "justinrainbow/json-schema": "^5.2",
+    "phpstan/extension-installer": "^1.2",
+    "phpstan/phpstan": "^1.9",
+    "phpstan/phpstan-doctrine": "^1.3",
+    "phpstan/phpstan-phpunit": "^1.3",
+    "phpstan/phpstan-symfony": "^1.2",
+    "phpunit/phpunit": "^9.6",
+    "rector/rector": "^0.15.13",
+    "symfony/browser-kit": "6.3.*",
+    "symfony/css-selector": "6.3.*",
+    "symfony/debug-bundle": "6.3.*",
+    "symfony/maker-bundle": "^1.48",
+    "symfony/phpunit-bridge": "^6.2",
+    "symfony/stopwatch": "6.3.*",
+    "symfony/web-profiler-bundle": "6.3.*",
+    "timeweb/phpstan-enum": "^3.1",
+    "zenstruck/foundry": "^1.31"
+  },
+  "config": {
+    "optimize-autoloader": true,
+    "preferred-install": {
+      "*": "dist"
     },
-    "autoload": {
-        "psr-4": {
-            "App\\": "src/"
-        }
+    "sort-packages": true,
+    "allow-plugins": {
+      "cyclonedx/cyclonedx-php-composer": true,
+      "symfony/flex": true,
+      "phpstan/extension-installer": true
+    }
+  },
+  "autoload": {
+    "psr-4": {
+      "App\\": "src/"
     },
+    "classmap": [
+      "vendor/opentalent/phpdocx/Classes/Phpdocx/Create/CreateDocx.php",
+      "vendor/opentalent/phpdocx/Classes/Phpdocx/Create/CreateDocxFromTemplate.php"
+    ],
     "autoload-dev": {
         "psr-4": {
+            "DataFixtures\\": "tests/Fixture",
             "App\\Tests\\": "tests/"
         }
     },
@@ -120,7 +147,58 @@
     "extra": {
         "symfony": {
             "allow-contrib": false,
-            "require": "5.4.*"
+            "require": "6.3.*"
+        },
+        "phpstan": {
+            "includes": [
+                "extension.neon"
+            ]
         }
     }
+    "classmap": [
+      "vendor/opentalent/phpdocx/Classes/Phpdocx/Create/CreateDocx.php",
+      "vendor/opentalent/phpdocx/Classes/Phpdocx/Create/CreateDocxFromTemplate.php"
+    ]
+  },
+  "autoload-dev": {
+    "psr-4": {
+      "DataFixtures\\": "tests/Fixture",
+      "App\\Tests\\": "tests/"
+    }
+  },
+  "replace": {
+    "paragonie/random_compat": "2.*",
+    "symfony/polyfill-ctype": "*",
+    "symfony/polyfill-iconv": "*",
+    "symfony/polyfill-php72": "*",
+    "symfony/polyfill-php71": "*",
+    "symfony/polyfill-php70": "*",
+    "symfony/polyfill-php56": "*"
+  },
+  "scripts": {
+    "auto-scripts": {
+      "cache:clear": "symfony-cmd",
+      "assets:install %PUBLIC_DIR%": "symfony-cmd"
+    },
+    "post-install-cmd": [
+      "@auto-scripts"
+    ],
+    "post-update-cmd": [
+      "@auto-scripts"
+    ]
+  },
+  "conflict": {
+    "symfony/symfony": "*"
+  },
+  "extra": {
+    "symfony": {
+      "allow-contrib": false,
+      "require": "6.2.*"
+    },
+    "phpstan": {
+      "includes": [
+        "extension.neon"
+      ]
+    }
+  }
 }

+ 7 - 30
config/api_platform/Access/access.yaml

@@ -1,32 +1,9 @@
-App\Entity\Access\Access:
-  collectionOperations:
-    get: ~
+resources:
+  App\Entity\Access\Access:
+    - operations:
+        ApiPlatform\Metadata\Get:
+          security: '(is_granted("ROLE_USERS_VIEW") and object.getOrganization().getId() == user.getOrganization().getId()) or (object.getId() == user.getId())'
 
-    cget_students:
-      method: GET
-      path: '/students'
-      security: 'is_granted("ROLE_USERS_VIEW")'
+        ApiPlatform\Metadata\Put:
+          security: 'is_granted("ROLE_USERS") or (object.getId() == user.getId())'
 
-    cget_access_person_ref:
-      method: GET
-      path: '/access_people'
-      normalization_context:
-        groups: ['access_people_ref']
-
-  itemOperations:
-    get:
-      security: '(is_granted("ROLE_USERS_VIEW") and object.getOrganization().getId() == user.getOrganization().getId()) or (object.getId() == user.getId())'
-
-    get_access_address:
-      method: GET
-      path: '/access_addresses/{id}'
-      requirements:
-        id : '\d+'
-      normalization_context:
-        groups: ['access_address', 'address']]
-      security: 'object.getOrganization().getId() == user.getOrganization().getId()'
-
-    put:
-      security: 'is_granted("ROLE_USERS") or (object.getId() == user.getId())'
-
-    delete: ~

+ 11 - 5
config/bundles.php

@@ -1,24 +1,30 @@
 <?php
 
+$devEnvs = ['dev' => true, 'docker' => true, 'test' => false, 'staging' => true];
+
 return [
     Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
     Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
-    Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['docker' => true, 'test' => true],
     Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
     Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
     Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
     Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
-    ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
+    ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
     Jb\Bundle\PhumborBundle\JbPhumborBundle::class => ['all' => true],
     Misd\PhoneNumberBundle\MisdPhoneNumberBundle::class => ['all' => true],
     Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
-    Symfony\Bundle\MakerBundle\MakerBundle::class => ['docker' => true],
     FOS\ElasticaBundle\FOSElasticaBundle::class => ['all' => true],
     Knp\Bundle\SnappyBundle\KnpSnappyBundle::class => ['all' => true],
     Knp\Bundle\GaufretteBundle\KnpGaufretteBundle::class => ['all' => true],
     Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
-    Symfony\Bundle\DebugBundle\DebugBundle::class => ['docker' => true],
     Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
     Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
-    DH\AuditorBundle\DHAuditorBundle::class => ['all' => true],
+    Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => $devEnvs,
+    Symfony\Bundle\MakerBundle\MakerBundle::class => $devEnvs,
+    Symfony\Bundle\DebugBundle\DebugBundle::class => $devEnvs,
+    Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => $devEnvs,
+    Zenstruck\Foundry\ZenstruckFoundryBundle::class => $devEnvs,
+    Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => $devEnvs,
+    Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => $devEnvs,
+    Hautelook\AliceBundle\HautelookAliceBundle::class => $devEnvs,
 ];

+ 178 - 176
config/opentalent/enum.yaml

@@ -1,176 +1,178 @@
-opentalent:
-  #AccessAndFunction
-    function: 'App\Enum\AccessAndFunction\FunctionEnum'
-    function_activities: 'App\Enum\AccessAndFunction\ActivitiesFunctionEnum'
-    function_administratives: 'App\Enum\AccessAndFunction\AdministrativesFunctionEnum'
-    function_associatives: 'App\Enum\AccessAndFunction\AssociativesFunctionEnum'
-    function_departures_cause: 'App\Enum\AccessAndFunction\DeparturesCauseEnum'
-    function_pedagogics: 'App\Enum\AccessAndFunction\PedagogicsFunctionEnum'
-    function_other: 'App\Enum\AccessAndFunction\OtherFunctionEnum'
-    function_type: 'App\Enum\AccessAndFunction\TypeFunctionEnum'
-    license_type: 'App\Enum\AccessAndFunction\LicenseTypeEnum'
-    license_rate: 'App\Enum\AccessAndFunction\LicenseRateEnum'
-    function_role: 'App\Enum\AccessAndFunction\FunctionalRoleEnum'
-
-  #AccessSocial
-    professional_status: 'App\Enum\AccessSocial\ProfessionalStatusEnum'
-    type_of_contact: 'App\Enum\AccessSocial\TypeOfContractEnum'
-    assisted_contract: 'App\Enum\AccessSocial\AssistedContractEnum'
-    period_unit: 'App\Enum\AccessSocial\PeriodUnitEnum'
-    family_situation: 'App\Enum\AccessSocial\FamilySituationEnum'
-
-  #Core
-    period: 'App\Enum\Core\PeriodEnum'
-    action_status_type: 'App\Enum\Core\ActionStatusTypeEnum'
-    control_type: 'App\Enum\Core\ControlTypeEnum'
-    repair_type: 'App\Enum\Core\RepairTypeEnum'
-    contact_point_type: 'App\Enum\Core\ContactPointTypeEnum'
-    contact_point_type_person: 'App\Enum\Core\ContactPointTypePersonEnum'
-    appreciation: 'App\Enum\Core\AppreciationEnum'
-    semester: 'App\Enum\Core\SemesterEnum'
-    trimester: 'App\Enum\Core\TrimesterEnum'
-    month: 'App\Enum\Core\MonthEnum'
-    notification_type: 'App\Enum\Core\NotificationTypeEnum'
-    tips_priority_level: 'App\Enum\Core\PriorityLevelEnum'
-    tips_type_person: 'App\Enum\Core\TipsTypePersonEnum'
-    tips_network: 'App\Enum\Core\TipsNetworkEnum'
-    tips_product: 'App\Enum\Core\TipsProductEnum'
-    days_enum: 'App\Enum\Core\DaysEnum'
-    timezone: 'App\Enum\Core\TimeZoneEnum'
-    tagtextcolor: 'App\Enum\Core\TagTextColorEnum'
-    tips_legalstatus: 'App\Enum\Core\TipsLegalStatusEnum'
-
-  #Donor
-    donor_displayed_on: 'App\Enum\Donor\DonorDisplayedOnEnum'
-    donor_status: 'App\Enum\Donor\DonorStatusEnum'
-
-  #Network
-    network_leading: 'App\Enum\Network\LeadingCauseEnum'
-    network: 'App\Enum\Network\NetworkEnum'
-
-  #Organization
-    organization_category: 'App\Enum\Organization\CategoryEnum'
-    organization_financial: 'App\Enum\Organization\FinancialEnum'
-    organization_legal: 'App\Enum\Organization\LegalEnum'
-    organization_opca: 'App\Enum\Organization\OpcaEnum'
-    organization_principal_type: 'App\Enum\Organization\PrincipalTypeEnum'
-    organization_school_cat: 'App\Enum\Organization\SchoolCategoryEnum'
-    organization_type_establishment_detail: 'App\Enum\Organization\TypeEstablishmentDetailEnum'
-    organization_type_establishment: 'App\Enum\Organization\TypeEstablishmentEnum'
-    organization_familly_type: 'App\Enum\Organization\FamillyTypeEnum'
-    organization_sub_familly_type: 'App\Enum\Organization\SubFamillyTypeEnum'
-    organization_bulletin_period: 'App\Enum\Organization\BulletinPeriodEnum'
-    organization_bulletin_output: 'App\Enum\Organization\BulletinOutputEnum'
-    organization_bulletin_send_to: 'App\Enum\Organization\SendToBulletinEnum'
-    organization_setting_country: 'App\Enum\Organization\CountryEnum'
-    address_postal_organization: 'App\Enum\Organization\AddressPostalOrganizationTypeEnum'
-
-  #Person
-    person_gender: 'App\Enum\Person\GenderEnum'
-    person_insee: 'App\Enum\Person\InseeEnum'
-    person_medal_type: 'App\Enum\Person\MedalTypeEnum'
-    person_moral_type: 'App\Enum\Person\MoralPersonTypeEnum'
-    medal_type: 'App\Enum\Person\MedalTypeEnum'
-    address_postal_person: 'App\Enum\Person\AddressPostalPersonTypeEnum'
-
-  #Place
-    place_day_of_week: 'App\Enum\Place\DayOfWeekEnum'
-    place_site_status: 'App\Enum\Place\SiteStatusEnum'
-    place_site_type: 'App\Enum\Place\SiteTypeEnum'
-
-  #Product
-    equipment_appreciation: 'App\Enum\Product\AppreciationEnum'
-    equipment_control_type: 'App\Enum\Product\ControlTypeEnum'
-    equipment_loan_type: 'App\Enum\Product\LoanTypeEnum'
-    equipment_category: 'App\Enum\Product\EquipmentCategoryEnum'
-    equipment_detail: 'App\Enum\Product\EquipmentDetailEnum'
-    equipment_family: 'App\Enum\Product\EquipmentFamillyEnum'
-    equipment_group: 'App\Enum\Product\EquipmentGroupEnum'
-    equipment_insurance: 'App\Enum\Product\EquipmentInsuranceEnum'
-    equipment_logistical_platform: 'App\Enum\Product\EquipmentLogisticalPlatformEnum'
-    equipment_status: 'App\Enum\Product\EquipmentStatusEnum'
-    equipment_tone: 'App\Enum\Product\EquipmentToneEnum'
-    equipment_used_for: 'App\Enum\Product\EquipmentUsedForEnum'
-    equipment_repair_type: 'App\Enum\Product\RepairTypeEnum'
-    equipment_band: 'App\Enum\Product\BandEnum'
-    equipment_difficulty: 'App\Enum\Product\DifficultyEnum'
-    equipment_origin: 'App\Enum\Product\OriginMusicScoreEnum'
-    equipment_outcause: 'App\Enum\Product\OutCauseEnum'
-    equipment_checking: 'App\Enum\Product\CheckingEnum'
-    intangible_amount_type: 'App\Enum\Product\IntangibleAmountTypeEnum'
-    equipment_periodicity_control: 'App\Enum\Product\PeriodicityControlEnum'
-
-  #Event
-    event_type: 'App\Enum\Booking\EventTypeEnum'
-    event_visibility: 'App\Enum\Booking\VisibilityEnum'
-    event_type_gender: 'App\Enum\Booking\EventGenderTypeEnum'
-    event_educationalproject_establishment: 'App\Enum\Booking\EducationalProjectEstablishmentEnum'
-    event_educationalproject_specialclass: 'App\Enum\Booking\EducationalProjectSpecialClassEnum'
-    event_educationalproject_educationaldevice: 'App\Enum\Booking\EducationalProjectEducationalDeviceEnum'
-    examen_convocation_status: 'App\Enum\Booking\ExamenConvocationStatusEnum'
-    examen_convocation_presence: 'App\Enum\Booking\ExamenConvocationPresenceEnum'
-    examen_timeline: 'App\Enum\Booking\ExamenTimelineTypeEnum'
-    course_timeline: 'App\Enum\Booking\CourseTimelineTypeEnum'
-    organization_holiday_timeline: 'App\Enum\Booking\OrganizationHolidayTimelineTypeEnum'
-    person_holiday_timeline: 'App\Enum\Booking\PersonHolidayTimelineTypeEnum'
-    event_timeline: 'App\Enum\Booking\EventTimelineTypeEnum'
-    educational_project_timeline: 'App\Enum\Booking\EducationalProjectTimelineTypeEnum'
-    event_participation: 'App\Enum\Booking\ParticipationStatusEnum'
-
-  #file
-    file_visibility: 'App\Enum\Core\FileVisibilityEnum'
-    file_folder: 'App\Enum\Core\FileFolderEnum'
-
-  #education
-    education: 'App\Enum\Education\EducationTypeEnum'
-    education_year: 'App\Enum\Education\YearEnum'
-    education_period: 'App\Enum\Education\PeriodEnum'
-    education_periodicity: 'App\Enum\Education\PeriodicityEnum'
-    advanced_education_notation: 'App\Enum\Education\AdvancedEducationNotationTypeEnum'
-
-    #billing
-    billing_periodicity: 'App\Enum\Billing\PeriodicityEnum'
-    billing_type_periodicity: 'App\Enum\Billing\PeriodicityTypeEnum'
-    billing_pricingline_adhesion: 'App\Enum\Billing\PricingLineAdhesionEnum'
-    billing_pricingline_productrent: 'App\Enum\Billing\PricingLineProductRentEnum'
-    billing_discount_global_tariff_family: 'App\Enum\Billing\DiscountGlobalTariffFamilyEnum'
-    billing_pricingline_invoice_line: 'App\Enum\Billing\PricingLineInvoiceLineEnum'
-    billing_payment_choice: 'App\Enum\Billing\PaymentChoiceEnum'
-    billing_bank: 'App\Enum\Billing\BankEnum'
-    billing_periodicity_payment: 'App\Enum\Billing\PeriodicityPaymentEnum'
-    billing_rent_reduction_type: 'App\Enum\Billing\ReductionTypeEnum'
-    billing_rent_year: 'App\Enum\Billing\RentYearEnum'
-    billing_family_reduction_type: 'App\Enum\Billing\FamilyReductionTypeEnum'
-    billing_payment_state: 'App\Enum\Billing\PaymentStateEnum'
-    billing_period: 'App\Enum\Billing\PeriodEnum'
-    billing_adhesion_choice: 'App\Enum\Billing\AdhesionChoiceEnum'
-    billing_order_adult_type_choice: 'App\Enum\Billing\OrderAdultTypeEnum'
-    billing_adult_condition_type_choice: 'App\Enum\Billing\AdultConditionTypeEnum'
-    billing_payment_type_choice: 'App\Enum\Billing\PaymentTypeEnum'
-    billing_type: 'App\Enum\Billing\BillingTypeEnum'
-
-  #message
-    message_status: 'App\Enum\Message\MessageStatusEnum'
-    report_message_satus: 'App\Enum\Message\ReportMessageSatusEnum'
-    message_sender: 'App\Enum\Message\SenderEnum'
-    message_sender_only_access: 'App\Enum\Message\SenderOnlyAccessEnum'
-    message_send_to: 'App\Enum\Message\SendToEnum'
-
-  #rules
-    default_rules: 'App\Enum\Rulerz\RulesEnum'
-    manager_rules: 'App\Enum\Rulerz\RulesManagerEnum'
-
-  #commission
-    commission_member_function: 'App\Enum\Person\CommissionMemberFunctionEnum'
-    commission_member_cause_output: 'App\Enum\Person\CauseOutputEnum'
-
-  #Cotisation
-    cotisation_function_enum_choices: 'App\Enum\Cotisation\CotisationFunctionEnum'
-    type_of_practices_enum: 'App\Enum\Cotisation\TypeOfPracticeEnum'
-    category_type_of_practices_enum: 'App\Enum\Cotisation\CategoryTypeOfPracticeEnum'
-
-  #OnlineRegistration
-    onlineregistration_registration_status: 'App\Enum\OnlineRegistration\RegistrationStatus'
-    onlineregistration_wish_registration: 'App\Enum\OnlineRegistration\WishRegistration'
-    onlineregistration_file_type: 'App\Enum\OnlineRegistration\FileTypeEnum'
-    onlineregistration_origin_education_student_wish: 'App\Enum\OnlineRegistration\EducationStudentWishOriginEnum'
+parameters:
+    opentalent.enum:
+        #AccessAndFunction
+          function: 'App\Enum\AccessAndFunction\FunctionEnum'
+          function_activities: 'App\Enum\AccessAndFunction\ActivitiesFunctionEnum'
+          function_administratives: 'App\Enum\AccessAndFunction\AdministrativesFunctionEnum'
+          function_associatives: 'App\Enum\AccessAndFunction\AssociativesFunctionEnum'
+          function_departures_cause: 'App\Enum\AccessAndFunction\DeparturesCauseEnum'
+          function_pedagogics: 'App\Enum\AccessAndFunction\PedagogicsFunctionEnum'
+          function_other: 'App\Enum\AccessAndFunction\OtherFunctionEnum'
+          function_type: 'App\Enum\AccessAndFunction\TypeFunctionEnum'
+          license_type: 'App\Enum\AccessAndFunction\LicenseTypeEnum'
+          license_rate: 'App\Enum\AccessAndFunction\LicenseRateEnum'
+          function_role: 'App\Enum\AccessAndFunction\FunctionalRoleEnum'
+
+        #AccessSocial
+          professional_status: 'App\Enum\AccessSocial\ProfessionalStatusEnum'
+          type_of_contact: 'App\Enum\AccessSocial\TypeOfContractEnum'
+          assisted_contract: 'App\Enum\AccessSocial\AssistedContractEnum'
+          period_unit: 'App\Enum\AccessSocial\PeriodUnitEnum'
+          family_situation: 'App\Enum\AccessSocial\FamilySituationEnum'
+
+        #Core
+          period: 'App\Enum\Core\PeriodEnum'
+          action_status_type: 'App\Enum\Core\ActionStatusTypeEnum'
+          control_type: 'App\Enum\Core\ControlTypeEnum'
+          repair_type: 'App\Enum\Core\RepairTypeEnum'
+          contact_point_type: 'App\Enum\Core\ContactPointTypeEnum'
+          contact_point_type_person: 'App\Enum\Core\ContactPointTypePersonEnum'
+          appreciation: 'App\Enum\Core\AppreciationEnum'
+          semester: 'App\Enum\Core\SemesterEnum'
+          trimester: 'App\Enum\Core\TrimesterEnum'
+          month: 'App\Enum\Core\MonthEnum'
+          notification_type: 'App\Enum\Core\NotificationTypeEnum'
+          tips_priority_level: 'App\Enum\Core\PriorityLevelEnum'
+          tips_type_person: 'App\Enum\Core\TipsTypePersonEnum'
+          tips_network: 'App\Enum\Core\TipsNetworkEnum'
+          tips_product: 'App\Enum\Core\TipsProductEnum'
+          days_enum: 'App\Enum\Core\DaysEnum'
+          timezone: 'App\Enum\Core\TimeZoneEnum'
+          tagtextcolor: 'App\Enum\Core\TagTextColorEnum'
+          tips_legalstatus: 'App\Enum\Core\TipsLegalStatusEnum'
+
+        #Donor
+          donor_displayed_on: 'App\Enum\Donor\DonorDisplayedOnEnum'
+          donor_status: 'App\Enum\Donor\DonorStatusEnum'
+
+        #Network
+          network_leading: 'App\Enum\Network\LeadingCauseEnum'
+          network: 'App\Enum\Network\NetworkEnum'
+
+        #Organization
+          organization_category: 'App\Enum\Organization\CategoryEnum'
+          organization_financial: 'App\Enum\Organization\FinancialEnum'
+          organization_legal: 'App\Enum\Organization\LegalEnum'
+          organization_opca: 'App\Enum\Organization\OpcaEnum'
+          organization_principal_type: 'App\Enum\Organization\PrincipalTypeEnum'
+          organization_school_cat: 'App\Enum\Organization\SchoolCategoryEnum'
+          organization_type_establishment_detail: 'App\Enum\Organization\TypeEstablishmentDetailEnum'
+          organization_type_establishment: 'App\Enum\Organization\TypeEstablishmentEnum'
+          organization_familly_type: 'App\Enum\Organization\FamillyTypeEnum'
+          organization_sub_familly_type: 'App\Enum\Organization\SubFamillyTypeEnum'
+          organization_bulletin_period: 'App\Enum\Organization\BulletinPeriodEnum'
+          organization_bulletin_output: 'App\Enum\Organization\BulletinOutputEnum'
+          organization_bulletin_send_to: 'App\Enum\Organization\SendToBulletinEnum'
+          organization_setting_country: 'App\Enum\Organization\CountryEnum'
+          address_postal_organization: 'App\Enum\Organization\AddressPostalOrganizationTypeEnum'
+
+        #Person
+          person_gender: 'App\Enum\Person\GenderEnum'
+          person_insee: 'App\Enum\Person\InseeEnum'
+          person_medal_type: 'App\Enum\Person\MedalTypeEnum'
+          person_moral_type: 'App\Enum\Person\MoralPersonTypeEnum'
+          medal_type: 'App\Enum\Person\MedalTypeEnum'
+          address_postal_person: 'App\Enum\Person\AddressPostalPersonTypeEnum'
+
+        #Place
+          place_day_of_week: 'App\Enum\Place\DayOfWeekEnum'
+          place_site_status: 'App\Enum\Place\SiteStatusEnum'
+          place_site_type: 'App\Enum\Place\SiteTypeEnum'
+
+        #Product
+          equipment_appreciation: 'App\Enum\Product\AppreciationEnum'
+          equipment_control_type: 'App\Enum\Product\ControlTypeEnum'
+          equipment_loan_type: 'App\Enum\Product\LoanTypeEnum'
+          equipment_category: 'App\Enum\Product\EquipmentCategoryEnum'
+          equipment_detail: 'App\Enum\Product\EquipmentDetailEnum'
+          equipment_family: 'App\Enum\Product\EquipmentFamillyEnum'
+          equipment_group: 'App\Enum\Product\EquipmentGroupEnum'
+          equipment_insurance: 'App\Enum\Product\EquipmentInsuranceEnum'
+          equipment_logistical_platform: 'App\Enum\Product\EquipmentLogisticalPlatformEnum'
+          equipment_status: 'App\Enum\Product\EquipmentStatusEnum'
+          equipment_tone: 'App\Enum\Product\EquipmentToneEnum'
+          equipment_used_for: 'App\Enum\Product\EquipmentUsedForEnum'
+          equipment_repair_type: 'App\Enum\Product\RepairTypeEnum'
+          equipment_band: 'App\Enum\Product\BandEnum'
+          equipment_difficulty: 'App\Enum\Product\DifficultyEnum'
+          equipment_origin: 'App\Enum\Product\OriginMusicScoreEnum'
+          equipment_outcause: 'App\Enum\Product\OutCauseEnum'
+          equipment_checking: 'App\Enum\Product\CheckingEnum'
+          intangible_amount_type: 'App\Enum\Product\IntangibleAmountTypeEnum'
+          equipment_periodicity_control: 'App\Enum\Product\PeriodicityControlEnum'
+
+        #Event
+          event_type: 'App\Enum\Booking\EventTypeEnum'
+          event_visibility: 'App\Enum\Booking\VisibilityEnum'
+          event_type_gender: 'App\Enum\Booking\EventGenderTypeEnum'
+          event_educationalproject_establishment: 'App\Enum\Booking\EducationalProjectEstablishmentEnum'
+          event_educationalproject_specialclass: 'App\Enum\Booking\EducationalProjectSpecialClassEnum'
+          event_educationalproject_educationaldevice: 'App\Enum\Booking\EducationalProjectEducationalDeviceEnum'
+          examen_convocation_status: 'App\Enum\Booking\ExamenConvocationStatusEnum'
+          examen_convocation_presence: 'App\Enum\Booking\ExamenConvocationPresenceEnum'
+          examen_timeline: 'App\Enum\Booking\ExamenTimelineTypeEnum'
+          course_timeline: 'App\Enum\Booking\CourseTimelineTypeEnum'
+          organization_holiday_timeline: 'App\Enum\Booking\OrganizationHolidayTimelineTypeEnum'
+          person_holiday_timeline: 'App\Enum\Booking\PersonHolidayTimelineTypeEnum'
+          event_timeline: 'App\Enum\Booking\EventTimelineTypeEnum'
+          educational_project_timeline: 'App\Enum\Booking\EducationalProjectTimelineTypeEnum'
+          event_participation: 'App\Enum\Booking\ParticipationStatusEnum'
+
+        # File
+          file_visibility: 'App\Enum\Core\FileVisibilityEnum'
+          file_folder: 'App\Enum\Core\FileFolderEnum'
+          file_status: 'App\Enum\Core\FileStatusEnum'
+
+        #education
+          education: 'App\Enum\Education\EducationTypeEnum'
+          education_year: 'App\Enum\Education\YearEnum'
+          education_period: 'App\Enum\Education\PeriodEnum'
+          education_periodicity: 'App\Enum\Education\PeriodicityEnum'
+          advanced_education_notation: 'App\Enum\Education\AdvancedEducationNotationTypeEnum'
+
+          #billing
+          billing_periodicity: 'App\Enum\Billing\PeriodicityEnum'
+          billing_type_periodicity: 'App\Enum\Billing\PeriodicityTypeEnum'
+          billing_pricingline_adhesion: 'App\Enum\Billing\PricingLineAdhesionEnum'
+          billing_pricingline_productrent: 'App\Enum\Billing\PricingLineProductRentEnum'
+          billing_discount_global_tariff_family: 'App\Enum\Billing\DiscountGlobalTariffFamilyEnum'
+          billing_pricingline_invoice_line: 'App\Enum\Billing\PricingLineInvoiceLineEnum'
+          billing_payment_choice: 'App\Enum\Billing\PaymentChoiceEnum'
+          billing_bank: 'App\Enum\Billing\BankEnum'
+          billing_periodicity_payment: 'App\Enum\Billing\PeriodicityPaymentEnum'
+          billing_rent_reduction_type: 'App\Enum\Billing\ReductionTypeEnum'
+          billing_rent_year: 'App\Enum\Billing\RentYearEnum'
+          billing_family_reduction_type: 'App\Enum\Billing\FamilyReductionTypeEnum'
+          billing_payment_state: 'App\Enum\Billing\PaymentStateEnum'
+          billing_period: 'App\Enum\Billing\PeriodEnum'
+          billing_adhesion_choice: 'App\Enum\Billing\AdhesionChoiceEnum'
+          billing_order_adult_type_choice: 'App\Enum\Billing\OrderAdultTypeEnum'
+          billing_adult_condition_type_choice: 'App\Enum\Billing\AdultConditionTypeEnum'
+          billing_payment_type_choice: 'App\Enum\Billing\PaymentTypeEnum'
+          billing_type: 'App\Enum\Billing\BillingTypeEnum'
+
+        #message
+          message_status: 'App\Enum\Message\MessageStatusEnum'
+          report_message_satus: 'App\Enum\Message\ReportMessageSatusEnum'
+          message_sender: 'App\Enum\Message\SenderEnum'
+          message_sender_only_access: 'App\Enum\Message\SenderOnlyAccessEnum'
+          message_send_to: 'App\Enum\Message\SendToEnum'
+
+        #rules
+          default_rules: 'App\Enum\Rulerz\RulesEnum'
+          manager_rules: 'App\Enum\Rulerz\RulesManagerEnum'
+
+        #commission
+          commission_member_function: 'App\Enum\Person\CommissionMemberFunctionEnum'
+          commission_member_cause_output: 'App\Enum\Person\CauseOutputEnum'
+
+        #Cotisation
+          cotisation_function_enum_choices: 'App\Enum\Cotisation\CotisationFunctionEnum'
+          type_of_practices_enum: 'App\Enum\Cotisation\TypeOfPracticeEnum'
+          category_type_of_practices_enum: 'App\Enum\Cotisation\CategoryTypeOfPracticeEnum'
+
+        #OnlineRegistration
+          onlineregistration_registration_status: 'App\Enum\OnlineRegistration\RegistrationStatus'
+          onlineregistration_wish_registration: 'App\Enum\OnlineRegistration\WishRegistration'
+          onlineregistration_file_type: 'App\Enum\OnlineRegistration\FileTypeEnum'
+          onlineregistration_origin_education_student_wish: 'App\Enum\OnlineRegistration\EducationStudentWishOriginEnum'

+ 18 - 2
config/opentalent/modulesbyconditions.yaml

@@ -1,5 +1,21 @@
-opentalent:
-    modulesbyconditions:
+parameters:
+    opentalent.modulesbyconditions:
+        ConsultOwnPedagogicResult:
+          conditions:
+            service:
+              name: App\Service\Education\Utils
+              function: isConsultOwnPedagogicResult
+        TeacherContactList:
+          conditions:
+            service:
+              name: App\Service\Education\Utils
+              function: isConsultTeacherListing
+        PeriodValidation:
+          conditions:
+            service:
+              name: App\Service\Education\Utils
+              function: isPeriodValidation
+
         CotisationCall:
             roles:
                 - ROLE_COTISATION

+ 6 - 4
config/opentalent/products.yaml

@@ -1,5 +1,5 @@
-opentalent:
-    modules:
+parameters:
+  opentalent.modules:
       Core:
         entities:
           - AccessProfile
@@ -57,6 +57,7 @@ opentalent:
           - PersonActivity
           - PersonHoliday
           - OrganizationResponsability
+          - Person
         roles:
           - ROLE_USERS
           - ROLE_ACCOUNTS
@@ -227,6 +228,8 @@ opentalent:
             - AccessTmp
             - EducationStudentWish
             - OnlineRegistrationSettings
+            - RegistrationStatus
+            - RegistrationAvailability
           roles:
             - ROLE_ONLINEREGISTRATION_ADMINISTRATION
 
@@ -266,7 +269,7 @@ opentalent:
         entities:
           - DolibarrAccount
 
-    products:
+  opentalent.products:
       artist:
         modules:
           - Core
@@ -282,7 +285,6 @@ opentalent:
           - Messages
           - Tagg
           - Statistic
-          - Cotisation
           - Dolibarr
 
       artist_premium:

+ 55 - 0
config/opentalent/subdomains.yaml

@@ -0,0 +1,55 @@
+parameters:
+  opentalent.subdomains:
+    # Liste des sous-domaines réservés (sous forme de RegEx, sans les caractères de début et de fin de ligne '^' et '$')
+    # @see https://ressources.opentalent.fr/display/SPEC/Nom+de+sous+domaines+reserves+pour+2IOS
+    reserved:
+      - opentalent
+      - 2ios
+      - 2iopenservice
+      - app
+      - my
+      - api
+      - ap2i
+      - local
+      - logs?
+      - stats
+      - preprod\d*
+      - test\d*
+      - admin
+      - frames
+      - git
+      - v\d+
+      - myadmin
+      - mailcatcher
+      - mail
+      - gitlab
+      - metabase
+      - dolibarr
+      - prod
+      - production
+      - assistance
+      - ressources
+      - support
+      - statistiques.*
+      - drive
+      - cloud
+      - www
+      - ww\d*
+      - agenda
+      - store
+      - boutique
+      - annuaire
+      - opentalent
+      - 2iopenservice
+      - typo3
+      - actus?
+      - actualites?
+      - news
+      - annonces?
+      - mobile
+      - membres
+      - mercure
+      - kibana
+      - phpmyadmin
+      - elasticsearch
+      - es

+ 9 - 4
config/packages/api_platform.yaml

@@ -1,4 +1,6 @@
 api_platform:
+    title: 'Opentalent API'
+    version: '3.1'
     enable_swagger_ui: false
     enable_re_doc: false
     mapping:
@@ -10,9 +12,12 @@ api_platform:
         json: ['application/merge-patch+json']
     swagger:
         versions: [3]
-    collection:
-        pagination:
-            # The default number of items per page.
-            items_per_page: 20
     resource_class_directories:
         - '%kernel.project_dir%/src/Entity'
+    defaults:
+        pagination_items_per_page: 20
+        normalization_context:
+            ## In 3.0, in conformance with the JSON Merge Patch RFC, the default value of the skip_null_values
+            ## property is true which means that from now on null values are omitted during serialization.
+            ## we don't want this => surcharge default value to false
+            skip_null_values: false

+ 0 - 0
config/packages/dh_auditor.yaml → config/packages/dh_auditor.yaml.ori


+ 2 - 0
config/packages/docker/hautelook_alice.yaml

@@ -0,0 +1,2 @@
+hautelook_alice:
+    fixtures_path: fixtures

+ 13 - 14
config/packages/docker/monolog.yaml

@@ -1,5 +1,6 @@
 # Voir doc/logging.md
 monolog:
+    channels: ['cron']
     handlers:
         # sorties standards (stdout, stderr, console)
         stderr:
@@ -45,33 +46,31 @@ monolog:
             max_files: 3
             channels: security
 
-        # logs spécifiques à certains process
-        # * synchro dolibarr
-        dolibarrsync:
+        # logs dédiés à l'exécution des cron-jobs
+        cron:
             type: group
-            members: [dolibarrsync_file]
-            channels: dolibarrsync
-        dolibarrsync_file:
+            members: [cron_file]
+            channels: cron
+        cron_file:
             type: rotating_file
-            path: "%kernel.logs_dir%/%kernel.environment%.dolibarrsync.log"
+            path: "%kernel.logs_dir%/%kernel.environment%.cron.log"
             level: debug
             max_files: 7
             formatter: monolog.formatter.message
-
-#        dolibarrsync_critical:
+#        cron_critical:
 #            type:           fingers_crossed
 #            action_level:   critical
-#            handler:        dolibarrsync_deduplicated
-#        dolibarrsync_deduplicated:
+#            handler:        cron_deduplicated
+#        cron_deduplicated:
 #            type: deduplication
 #            # the time in seconds during which duplicate entries are discarded (default: 60)
 #            time: 10
-#            handler: dolibarrsync_mailer
-#        dolibarrsync_mailer:
+#            handler: cron_mailer
+#        cron_mailer:
 #            type:           symfony_mailer
 #            from_email:     "process@opentalent.fr"
 #            to_email:       "exploitation@opentalent.fr"
-#            subject:        "Dolibarr Sync - Critical Error"
+#            subject:        "Cron - Critical Error"
 #            level:          error
 #            formatter:      monolog.formatter.html
 #            content_type:   text/html

+ 9 - 0
config/packages/docker/nelmio_alice.yaml

@@ -0,0 +1,9 @@
+nelmio_alice:
+    functions_blacklist:
+        - 'current'
+        - 'shuffle'
+        - 'date'
+        - 'time'
+        - 'file'
+        - 'md5'
+        - 'sha1'

+ 3 - 3
config/packages/docker/security.yaml

@@ -1,4 +1,4 @@
 security:
-    encoders:
-        App\Entity\Person\Person:
-            algorithm: plaintext
+  password_hashers:
+    App\Entity\Person\Person:
+      algorithm: plaintext

+ 4 - 0
config/packages/docker/zenstruck_foundry.yaml

@@ -0,0 +1,4 @@
+# See full configuration: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#full-default-bundle-configuration
+zenstruck_foundry:
+    # Whether to auto-refresh proxies by default (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh)
+    auto_refresh_proxies: true

+ 4 - 0
config/packages/doctrine.yaml

@@ -9,6 +9,7 @@ doctrine:
                 # IMPORTANT: You MUST configure your server version,
                 # either here or in the DATABASE_URL env var (see .env file)
                 server_version: '5.7'
+                schema_filter: ~^(?!view_)~
 
             audit:
                 url: '%env(resolve:DATABASE_AUDIT_URL)%'
@@ -20,6 +21,9 @@ doctrine:
                 # IMPORTANT: You MUST configure your server version,
                 # either here or in the DATABASE_URL env var (see .env file)
                 server_version: '5.7'
+        types:
+            uuid: Ramsey\Uuid\Doctrine\UuidType
+
 
     orm:
         default_entity_manager: default

+ 3 - 1
config/packages/framework.yaml

@@ -2,7 +2,8 @@
 framework:
     secret: '%env(APP_SECRET)%'
     #csrf_protection: true
-    #http_method_override: true
+    http_method_override: false
+    handle_all_throwables: true
 
     # Enables session support. Note that the session will ONLY be started if you read or write from it.
     # Remove or comment this section to explicitly disable session support.
@@ -10,6 +11,7 @@ framework:
         handler_id: null
         cookie_secure: auto
         cookie_samesite: lax
+        storage_factory_id: session.storage.factory.native
 
     #esi: true
     #fragments: true

+ 2 - 2
config/packages/messenger.yaml

@@ -1,7 +1,7 @@
 framework:
     messenger:
         # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
-        failure_transport: failed
+        # failure_transport: failed
 
         transports:
             # https://symfony.com/doc/current/messenger.html#transport-configuration
@@ -16,4 +16,4 @@ framework:
             'App\Message\Command\Export': async
             'App\Message\Command\Typo3\Typo3UpdateCommand': async
             'App\Message\Command\Typo3\Typo3DeleteCommand': async
-            'App\Message\Command\Typo3\Typo3UndeleteCommand': async
+            'App\Message\Command\Typo3\Typo3UndeleteCommand': async

+ 3 - 0
config/packages/monolog.yaml

@@ -0,0 +1,3 @@
+monolog:
+    channels:
+        - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists

+ 0 - 8
config/packages/prod/deprecations.yaml

@@ -1,8 +0,0 @@
-# As of Symfony 5.1, deprecations are logged in the dedicated "deprecation" channel when it exists
-#monolog:
-#    channels: [deprecation]
-#    handlers:
-#        deprecation:
-#            type: stream
-#            channels: [deprecation]
-#            path: php://stderr

+ 24 - 25
config/packages/prod/monolog.yaml

@@ -1,5 +1,6 @@
 # Voir doc/logging.md
 monolog:
+    channels: ['cron']
     handlers:
         # sorties standards (stdout, stderr, console)
         stderr:
@@ -45,36 +46,34 @@ monolog:
             max_files: 3
             channels: security
 
-        # logs spécifiques à certains process
-        # * synchro dolibarr
-        dolibarrsync:
+        # logs dédiés à l'exécution des cron-jobs
+        cron:
             type: group
-            members: [dolibarrsync_file]
-            channels: dolibarrsync
-        dolibarrsync_file:
+            members: [cron_file]
+            channels: cron
+        cron_file:
             type: rotating_file
-            path: "%kernel.logs_dir%/%kernel.environment%.dolibarrsync.log"
+            path: "%kernel.logs_dir%/%kernel.environment%.cron.log"
             level: debug
             max_files: 7
             formatter: monolog.formatter.message
-
-        #        dolibarrsync_critical:
-        #            type:           fingers_crossed
-        #            action_level:   critical
-        #            handler:        dolibarrsync_deduplicated
-        #        dolibarrsync_deduplicated:
-        #            type: deduplication
-        #            # the time in seconds during which duplicate entries are discarded (default: 60)
-        #            time: 10
-        #            handler: dolibarrsync_mailer
-        #        dolibarrsync_mailer:
-        #            type:           symfony_mailer
-        #            from_email:     "process@opentalent.fr"
-        #            to_email:       "exploitation@opentalent.fr"
-        #            subject:        "Dolibarr Sync - Critical Error"
-        #            level:          error
-        #            formatter:      monolog.formatter.html
-        #            content_type:   text/html
+#        cron_critical:
+#            type:           fingers_crossed
+#            action_level:   critical
+#            handler:        cron_deduplicated
+#        cron_deduplicated:
+#            type: deduplication
+#            # the time in seconds during which duplicate entries are discarded (default: 60)
+#            time: 10
+#            handler: cron_mailer
+#        cron_mailer:
+#            type:           symfony_mailer
+#            from_email:     "process@opentalent.fr"
+#            to_email:       "exploitation@opentalent.fr"
+#            subject:        "Cron - Critical Error"
+#            level:          error
+#            formatter:      monolog.formatter.html
+#            content_type:   text/html
 
 
         # uncomment to get logging in your browser

+ 4 - 0
config/packages/ramsey_uuid_doctrine.yaml

@@ -0,0 +1,4 @@
+doctrine:
+    dbal:
+        types:
+            uuid: 'Ramsey\Uuid\Doctrine\UuidType'

+ 12 - 9
config/packages/security.yaml

@@ -3,9 +3,9 @@ imports:
 
 security:
     role_hierarchy:
-        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH, ROLE_TIPS, ROLE_NETWORK]
+        ROLE_SUPER_ADMIN: [ ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH, ROLE_TIPS, ROLE_NETWORK ]
 
-        BASE_ROLE_ADMINISTRATION_CORE : &BASE_ROLE_ADMINISTRATION_CORE
+        BASE_ROLE_ADMINISTRATION_CORE: &BASE_ROLE_ADMINISTRATION_CORE
             - ROLE_MEMBER_CORE
             - ROLE_ORGANIZATION
 
@@ -91,7 +91,7 @@ security:
         ROLE_CA_CORE:
             - ROLE_MEMBER_CORE
 
-        ROLE_STUDENT :
+        ROLE_STUDENT:
             - ROLE_STUDENT_CORE
 
         ROLE_STUDENT_CORE:
@@ -119,11 +119,11 @@ security:
             - ROLE_CORE
             - ROLE_RULERZ_ACTION
 
-    encoders:
+    password_hashers:
         App\Entity\Person\Person:
             algorithm: bcrypt
 
-    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
+    # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
     providers:
         # used to reload user from session & other features (e.g. switch_user)
         access_provider:
@@ -134,15 +134,13 @@ security:
                 class: App\Entity\Person\Person
                 property: username
 
-    enable_authenticator_manager: true
-
     firewalls:
         dev:
             pattern: ^/(_(profiler|wdt)|css|images|js)/
             security: false
 
         login:
-            pattern:  ^/login_check
+            pattern: ^/login_check
             stateless: true
             json_login:
                 provider: person_provider
@@ -162,10 +160,15 @@ security:
 
         main:
             # activate different ways to authenticate
-            # https://symfony.com/doc/current/security.html#firewalls-authentication
+            # https://symfony.com/doc/current/security.html#the-firewall
+
+            # https://symfony.com/doc/current/security/impersonating_user.html
+            # switch_user: true
 
     # Easy way to control access for large sections of your site
     # Note: Only the *first* access control that matches will be used
     access_control:
         - { path: ^/api/public, roles: PUBLIC_ACCESS }
+        - { path: ^/api/internal, roles: INTERNAL_REQUEST }
+        - { path: ^/api/internal, roles: ROLE_NO_ACCESS }
         - { path: ^/api/, roles: IS_HAVING_MODULE }

+ 4 - 0
config/packages/staging/debug.yaml

@@ -0,0 +1,4 @@
+debug:
+    # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
+    # See the "server:dump" command to start a new server.
+    dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"

+ 4 - 0
config/packages/staging/framework.yaml

@@ -0,0 +1,4 @@
+framework:
+    test: true
+#    session:
+#        storage_factory_id: session.storage.mock_file

+ 2 - 0
config/packages/staging/hautelook_alice.yaml

@@ -0,0 +1,2 @@
+hautelook_alice:
+    fixtures_path: fixtures

+ 12 - 0
config/packages/staging/monolog.yaml

@@ -0,0 +1,12 @@
+monolog:
+    handlers:
+        main:
+            type: fingers_crossed
+            action_level: error
+            handler: nested
+            excluded_http_codes: [404, 405]
+            channels: ["!event"]
+        nested:
+            type: stream
+            path: "%kernel.logs_dir%/%kernel.environment%.log"
+            level: debug

+ 9 - 0
config/packages/staging/nelmio_alice.yaml

@@ -0,0 +1,9 @@
+nelmio_alice:
+    functions_blacklist:
+        - 'current'
+        - 'shuffle'
+        - 'date'
+        - 'time'
+        - 'file'
+        - 'md5'
+        - 'sha1'

+ 6 - 0
config/packages/staging/security.yaml

@@ -0,0 +1,6 @@
+# override in api/config/packages/test/security.yaml for test env
+# improve the test suite speed
+security:
+  password_hashers:
+    App\Entity\Person\Person:
+      algorithm: plaintext

+ 2 - 0
config/packages/staging/twig.yaml

@@ -0,0 +1,2 @@
+twig:
+    strict_variables: true

+ 3 - 0
config/packages/staging/validator.yaml

@@ -0,0 +1,3 @@
+framework:
+    validation:
+        not_compromised_password: false

+ 0 - 0
config/packages/test/web_profiler.yaml → config/packages/staging/web_profiler.yaml


+ 4 - 0
config/packages/staging/zenstruck_foundry.yaml

@@ -0,0 +1,4 @@
+# See full configuration: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#full-default-bundle-configuration
+zenstruck_foundry:
+    # Whether to auto-refresh proxies by default (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh)
+    auto_refresh_proxies: true

+ 2 - 2
config/packages/test/framework.yaml

@@ -1,4 +1,4 @@
 framework:
     test: true
-    session:
-        storage_id: session.storage.mock_file
+#    session:
+#        storage_factory_id: session.storage.mock_file

+ 11 - 0
config/packages/test/security.yaml

@@ -0,0 +1,11 @@
+security:
+  password_hashers:
+    # By default, password hashers are resource intensive and take time. This is
+    # important to generate secure password hashes. In tests however, secure hashes
+    # are not important, waste resources and increase test times. The following
+    # reduces the work factor to the lowest possible values.
+    Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
+      algorithm: auto
+      cost: 4 # Lowest possible value for bcrypt
+      time_cost: 3 # Lowest possible value for argon
+      memory_cost: 10 # Lowest possible value for argon

+ 4 - 0
config/packages/uid.yaml

@@ -0,0 +1,4 @@
+framework:
+    uid:
+        default_uuid_version: 7
+        time_based_uuid_version: 7

+ 7 - 1
config/routes.yaml

@@ -1,10 +1,16 @@
+controllers:
+    resource:
+        path: ../src/Controller/
+        namespace: App\Controller
+    type: attribute
+
 login_check:
   path: /login_check
   methods: ['POST']
 
 swagger_ui:
   path: /docs
-  controller: api_platform.swagger.action.ui
+  controller: api_platform.swagger_ui.action
 
 # todo: comment ça s'articule avec le LocalStorage ça?
 ot_internal_secure_file_download:

+ 0 - 7
config/routes/annotations.yaml

@@ -1,7 +0,0 @@
-controllers:
-    resource: ../../src/Controller/
-    type: annotation
-
-kernel:
-    resource: ../../src/Kernel.php
-    type: annotation

+ 0 - 0
config/routes/dh_auditor.yaml → config/routes/dh_auditor.yaml.ori


+ 0 - 0
config/routes/prod/.gitkeep


+ 3 - 0
config/routes/staging/framework.yaml

@@ -0,0 +1,3 @@
+_errors:
+    resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
+    prefix: /_error

+ 0 - 0
config/routes/test/.gitkeep


+ 26 - 13
config/services.yaml

@@ -1,3 +1,8 @@
+# config/services.yaml
+imports:
+    - { resource: opentalent/* }
+    - { resource: services/* }
+
 # Put parameters here that don't need to change on each machine where the app is deployed
 # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
 parameters:
@@ -10,17 +15,12 @@ services:
         bind:
             $opentalentConfig: '%kernel.project_dir%%env(OPENTALENT_CONFIG)%'
             $internalFilesUploadUri: '%env(INTERNAL_FILES_DOWNLOAD_URI)%'
+            $internalRequestsToken: '%env(INTERNAL_REQUESTS_TOKEN)%'
             $bindfileBufferFile: '%env(BIND_FILE_BUFFER_FILE)%'
-            $contextAwareDataPersister: '@api_platform.doctrine.orm.data_persister'
+            $persistProcessor: '@api_platform.doctrine.orm.state.persist_processor'
+            $removeProcessor: '@api_platform.doctrine.orm.state.remove_processor'
             $opentalentNoReplyEmailAddress: 'noreply@opentalent.fr'
 
-    # Logging: a shorter version of the default monolog line formatter
-    monolog.formatter.message:
-        class: Monolog\Formatter\LineFormatter
-        arguments:
-            - "[%%datetime%%] %%level_name%% : %%message%%\n"
-            - "Y-m-d H:i:s.v"
-
     # makes classes in src/ available to be used as services
     # this creates a service per class whose id is the fully-qualified class name
     App\:
@@ -39,6 +39,9 @@ services:
     App\Service\Cotisation\Utils:
         public: true
 
+    App\Service\Education\Utils:
+        public: true
+
     App\Service\Network\Utils:
         public: true
 
@@ -47,10 +50,14 @@ services:
 
     Gaufrette\Filesystem: '@knp_gaufrette.filesystem_map'
 
+    # To use the test fixtures
+    App\Tests\Fixture\:
+        resource: '%kernel.project_dir%/tests/Fixture/*'
+
     #########################################
     ##  TAG Services ##
     _instanceof:
-        App\Doctrine\Access\AccessExtensionInterface:
+        App\Doctrine\Access\AdditionalExtension\AdditionalAccessExtensionInterface:
             tags: ['app.extensions.access']
         App\Service\Access\OptionalsRolesInterface:
             tags: ['app.optionalsroles']
@@ -62,6 +69,8 @@ services:
             tags: [ 'app.mailer.builder' ]
         App\Service\Twig\AssetsExtension:
             tags: [ 'twig.extension' ]
+        App\Service\Cron\CronjobInterface:
+            tags: [ 'app.cronjob' ]
 
     App\Service\ServiceIterator\CurrentAccessExtensionIterator:
         - !tagged_iterator app.extensions.access
@@ -73,10 +82,8 @@ services:
         - !tagged_iterator app.encoder
     App\Service\ServiceIterator\Mailer\BuilderIterator:
         - !tagged_iterator app.mailer.builder
-
-    App\Service\Dolibarr\DolibarrSyncService:
-        tags:
-            - { name: monolog.logger, channel: dolibarrsync }
+    App\Service\ServiceIterator\CronjobIterator:
+        - !tagged_iterator app.cronjob
 
     #########################################
     ##  SERIALIZER Decorates ##
@@ -104,3 +111,9 @@ services:
     App\Service\Elasticsearch\EducationNotationUpdater:
         arguments:
             - '@fos_elastica.object_persister.search.educationNotation'
+
+    #########################################
+    ## AutoWiring du service container ##
+    Symfony\Component\DependencyInjection\ContainerInterface: '@service_container'
+    #########################################
+

+ 21 - 0
config/services/api-platform.yaml

@@ -0,0 +1,21 @@
+services:
+    # Speeds up api-platform cache build substantially
+    # @see https://github.com/api-platform/core/issues/4975
+    api_platform.cache.metadata.property:
+      parent: cache.system
+      tags: [ cache.pool ]
+    api_platform.cache.metadata.resource:
+      parent: cache.system
+      tags: [ cache.pool ]
+    api_platform.cache.metadata.resource_collection:
+      parent: cache.system
+      tags: [ cache.pool ]
+    api_platform.cache.route_name_resolver:
+      parent: cache.system
+      tags: [ cache.pool ]
+    api_platform.cache.identifiers_extractor:
+      parent: cache.system
+      tags: [ cache.pool ]
+    api_platform.elasticsearch.cache.metadata.document:
+      parent: cache.system
+      tags: [ cache.pool ]

+ 7 - 0
config/services/monolog.yaml

@@ -0,0 +1,7 @@
+services:
+    # Logging: a shorter version of the default monolog line formatter
+    monolog.formatter.message:
+      class: Monolog\Formatter\LineFormatter
+      arguments:
+        - "[%%datetime%%] %%level_name%% : %%message%%\n"
+        - "Y-m-d H:i:s.v"

+ 0 - 24
doc/bindfile.md

@@ -1,24 +0,0 @@
-# Fichier Bind
-
-Le fichier "bind" rend possible la résolution des sous-domaines au niveau du serveur DNS (situé sur prod-back).
-Chaque fois qu'un nouveau sous-domaine est enregistré, ce fichier doit être mis à jour. Cependant, pour des questions de 
-droits, c'est l'utilisateur root qui doit procéder à cette mise à jour. 
-
-Voilà ce qu'il se passe lorsqu'une structure enregistre un nouveau sous-domaine depuis `prod-back`:
-
-1. Le logiciel ajoute le sous-domaine en question dans une nouvelle ligne du fichier tampon `/env/subdomain.txt`
-2. Un cron tourne toutes les 5 minutes et consomme le contenu de ce fichier avant de le vider
-3. Ce cron exécute le script `/env/add-subdomain`, qui met à jour le fichier bind `/etc/bind/zones/opentalent.fr.db`
-4. Une fois mis à jour le fichier bind, le cron exécuté sur prod-back (le serveur DNS) envoie par ssh le contenu du 
-   fichier subdomain.txt sur un même fichier `/env/subdomain.txt` situé sur le serveur `vpn` 
-5. Le même cron tourne toutes les 5 minutes sur le serveur vpn procède à son tour à la mise à jour de son fichier bind
-
-La mise à jour des deux fichiers bind est nécessaire, pour permettre la prise en compte des sous-domaines depuis
-l'intérieur ou l'extérieur du VPN.
-
-Lorsqu'un nouveau sous-domaine est enregistré depuis `prod-v2`:
-
-> TODO: à revoir
-
-1. Le logiciel remplit de la même façon un fichier `/env/subdomain.txt`
-2. La suite se déroule comme dans le premier scénario

+ 74 - 0
doc/internal_requests.md

@@ -0,0 +1,74 @@
+# Internal Requests
+
+### Principe général
+
+Les requêtes internes sont des requêtes envoyées de ap2i vers opentalent-platform ou dans le sens inverse, par 
+exemple pour demander un fichier.
+
+Ces requêtes ne sont pas protégées par l'authentification Symfony standard, car elles doivent pouvoir être exécutées 
+en dehors du cadre d'une requête utilisateur, par exemple lors d'une exécution en ligne de commande ou lors 
+d'un processus asynchrone exécuté par messenger.
+
+Pour éviter tout risque de sécurité lié à ces routes :
+
+* on restreint leur accès aux ips internes
+* on conditionne l'autorisation à la présence d'un token
+* on limite les routes concernées
+
+Ainsi, si l'on prend l'exemple d'une requête `/internal/download/123` sur ap2i :
+
+* Un utilisateur dans le VPN qui ferait un curl à cette adresse recevra une erreur 500 à cause du token manquant
+* Un utilisateur hors VPN, même s'il connaissait le token, recevra une erreur 500, car n'ayant pas une ip autorisée
+* Une requête issue de la V1 sera autorisée sans authentification
+
+### Ip internes 
+
+Les ips considérées comme interne sont :
+
+- Le localhost (`127.0.0.[0-1]`)
+- Les adresses venant de l'intérieur du VPN (`10.8.0.[0-255]`)
+- Les adresses publiques des serveurs Opentalent (`141.94.117.[33-61]`)
+- Les adresses privées des serveurs Opentalent (`172.16.0.[0-255]`)
+- Les adresses des autres containers docker (`172.20.[0-255].[0-255]`)
+
+> Plus d'infos ici : https://ressources.opentalent.fr/display/SI/Infrastructure+et+reseau
+
+
+### Mise en oeuvre
+
+On met en place un pattern de routes de la forme `/api/internal/*` qui sera uniquement dédié aux requêtes internes entre
+les deux API ou à d'autres éventuels échanges entre systèmes.
+
+Les appels à cette route ne sont autorisés que si :
+
+1. Que l'ip du client dont émet la requête fait partie d'un pool autorisé d'ips internes
+2. Qu'un header 'internal-requests-token' est défini et que sa valeur correspond à la valeur attendue.
+
+Si ces deux conditions ne sont pas remplies, la requête est rejetée, et ce même si l'utilisateur est authentifié.
+
+Les routes internal sont configurées ici : `config/packages/security.yaml`
+
+
+
+### Valider le fonctionnement
+
+Soit `$id` l'id d'un fichier stocké sur l'environnement V2
+On part du principe que l'utilisateur authentifié a des droits suffisants pour voir ce fichier.
+
+
+Côté ap2i, les requêtes suivantes doivent donner les résultats correspondants :
+
+| query                      | header défini | authentifié | VPN activé | Résultat attendu |
+|----------------------------|---------------|-------------|------------|------------------|
+| /api/internal/download/$id | NON           | NON         | NON        | 401 Unauthorized |
+| /api/internal/download/$id | OUI           | NON         | NON        | 401 Unauthorized |
+| /api/internal/download/$id | OUI           | NON         | OUI        | 200 OK           |
+| /api/internal/download/$id | OUI           | OUI         | OUI        | 200 OK           |
+| /api/internal/download/$id | OUI           | OUI         | NON        | 403 Forbidden    |
+| /api/internal/download/$id | NON           | OUI         | OUI        | 403 Forbidden    |
+| /api/download/$id          | *             | NON         | *          | 401 Unauthorized |
+| /api/download/$id          | *             | OUI         | *          | 200 OK           |
+
+
+Les mêmes tests s'appliquent côté V1, appliqués à un fichier stocké sur l'environnement de la V1.
+

+ 186 - 0
doc/security.md

@@ -0,0 +1,186 @@
+# Security
+
+## Authentification Symfony
+
+### Fonctionnement de base
+
+L'authentification se fait via une requête POST envoyée à l'adresse `/login_check` avec le body suivant : 
+
+    {
+        "username": "login",
+        "password": "password"
+    }
+
+En cas de succès, la requête renvoie un token qui servira ensuite à l'utilisateur à s'identifier.
+
+Les requêtes suivantes devront posséder les headers suivants : 
+
+* `x-accessid` : l'id de l'utilisateur (ou Access)
+* `authorization` : une chaine de caractères de la forme "BEARER XXXXX", où XXXXX est le token retourné par la requête de login
+
+### Connexion Switch
+
+Certains utilisateurs (admins, familles) peuvent prendre le rôle d'un autre utilisateur via la connexion switch.
+
+Pour ce faire, un nouveau header doit être ajouté aux requêtes : 
+
+* `x-switch-user`: l'id de l'utilisateur dont on veut prendre le rôle
+
+
+
+## Roles et Modules
+
+Les droits d'un utilisateur sont conditionnés à différents critères, dont : 
+
+* Les **modules** que possède l'organisation à laquelle il est appartient
+* Les **rôles** de cet utilisateur au sein de cette organisation
+
+On peut obtenir la liste des modules de l'organisation et des rôles de l'utilisateur actif en son sein au moyen de la 
+requête : `/api/my_profile`
+
+
+### Modules
+
+Les modules d'une organisation dépendent du produit acheté par celle-ci et des éventuels modules complémentaires. Ces deux 
+informations sont stoquées dans la table `Settings`.
+
+Le fichier `config/opentalent/products.yaml` définit :
+
+- L'appartenance des _modules_ aux _products_
+- L'appartenance des _entities_ aux _modules_
+
+De plus, le fichier `config/opentalent/modulesbyconditions.yaml` complète cette configuration en définissant des modules
+présentant des conditions particulières (appartenance à la CMF en particulier)
+
+A chaque requête effectuée, la classe `\App\Security\Voter\ModuleVoter` vérifie si la ressource demandée appartient à un
+module possédé par l'organisation de l'utilisateur. Si ce n'est pas le cas, une erreur `AccessDeniedHttpException` est levée.
+
+
+## Différentes méthodes de sécurisation
+
+### Les annotations Api-Platform
+
+La sécurité des ApiResources peut être définie de manière globale pour la ressource, ou pour chaque opération (Get, 
+GetCollection, Put, Post, Delete) via les annotations.
+
+Exemple : 
+
+    #[ApiResource(
+        operations: [
+            new Get(
+                security: '(is_granted("ROLE_ORGANIZATION_VIEW") or is_granted("ROLE_ORGANIZATION")) and object.getOrganization().getId() == user.getOrganization().getId()'
+            ),
+            new Put(
+                security: 'is_granted("ROLE_ORGANIZATION") and object.getOrganization().getId() == user.getOrganization().getId()'
+            )
+        ],
+    )]
+
+Dans certains cas plus complexes (ex: Access), cette configuration peut être déplacée dans un fichier de configuration
+situé dans le répertoire `~/config/api_platform/` et portant le nom de la ressource.
+
+> Voir plus : https://api-platform.com/docs/core/security/
+
+
+### Voters
+
+Les voters permettent de contrôler l'accès à certaines ressources, selon le type d'opération.
+
+Ils implémentent essentiellement deux méthodes : `supports` et `voteOnAttribute` qui prennent en paramètres la ressource 
+et le type d'opération. `supports` retourne `true` si le voter doit s'appliquer dans ce cas. Si oui, `voteOnAttribute`
+est appellée et retourne `true` si l'opération est autorisée.
+
+> TODO: quand faut-il retourner false et quand faut-il lever une AccessDeniedHttpException ?
+
+Pour qu'un Voter soit appellé, il faut configurer la sécurité de la ressource via son annotation et utiliser la méthode 
+`is_granted` combinée à une des constantes : 
+
+Exemple : 
+
+    #[ApiResource(
+        operations: [
+            new Get(security: "is_granted('READ', object)"),
+            new Put(security: "is_granted('EDIT', object)"),
+            new Post(security: "is_granted('CREATE', object)"),
+            new Delete(security: "is_granted('DELETE', object)"),
+        ]
+    )]
+    class File
+
+    
+
+Les voters custom sont enregistrés dans le répertoire `src/Security/Voter`.
+
+> Voir plus : https://symfony.com/doc/current/security/voters.html
+
+
+### Extensions Doctrine
+
+Le framework Api-Platform propose un système d'extensions doctrine pour ajouter des filtres automatiques aux opérations
+réalisées sur des ressources voulues.
+
+Concrètement, l'extension ajoutera une condition au `WHERE` de la requête SQL.
+
+Une extension implémente trois méthodes : 
+
+* `applyToCollection`
+* `applyToItem`
+* `addWhere`
+
+Les deux premières permettent de tester si l'extension doit s'appliquer à la ressource.
+
+La dernière ajoute la condition en question.
+
+> Voir : https://api-platform.com/docs/core/extensions/
+
+
+### Quand utiliser les annotations, un Voter ou une Extension Doctrine ?
+
+Si la sécurité doit pouvoir s'appliquer à une collection et filtrer le résultat de celle-ci (exemple: les notifications 
+d'un utilisateur) : on utilisera une _Extension Doctrine_
+
+Sinon, si les conditions d'accès peuvent s'écrire facilement avec l'expression language des annotations
+api-platform, on utilisera les _annotations Api-platform_.
+
+Si aucune des conditions précédentes n'est remplie, on utilisera un _Voter Symfony_.
+
+
+## Internal Requests
+
+Certaines routes de la forme `/internal/...` permettent d'accepter des requêtes sans que le client ne
+soit authentifié (cf `config/packages/security.yaml`), en se basant sur un contrôle de l'IP et un token. 
+
+> Voir: https://gitlab.2iopenservice.com/opentalent/ap2i/-/blob/develop/doc/internal_requests.md
+
+
+
+## Cas particuliers 
+
+### Les Fichiers
+
+Le client est autorisé à consulter l'enregistrement si l'une de ces conditions est vraie :
+
+* pas de date de disponibilité ou date de disponibilité antérieure à Now
+* date de disponibilité postérieure à Now, mais utilisateur a le rôle ROLE_BILLACCOUNTING et le fichier est de type BILL (facture)
+* visibilité est 'EVERYBODY' 
+* requête interne
+* est connecté et fait partie des AccessPersons du fichier
+* est connecté et est le propriétaire du fichier
+* est connecté et a le rôle ROLE_FILE et fichier appartient à l'utilisateur ou à son organisation
+* est connecté et fichier a des accessRoles et user a un des roles présents dans accessRoles et fichier appartient à l'utilisateur ou à son organisation (deprecated)
+
+
+Est autorisé à éditer si :
+
+* pas de date de disponibilité ou date de disponibilité antérieure à Now
+* date de disponibilité postérieure à Now, mais utilisateur a le rôle ROLE_BILLACCOUNTING et le fichier est de type BILL (facture)
+* est connecté et a le rôle ROLE_FILE et fichier appartient à l'utilisateur ou à son organisation
+* est connecté et est le propriétaire du fichier
+
+Est autorisé à créer si :
+
+* est connecté
+
+Est autorisé à supprimer si :
+
+* est connecté et est autorisé à éditer

+ 53 - 0
doc/subdomain.md

@@ -0,0 +1,53 @@
+# Gestion des sous domaines
+
+## Opérations de mise à jour
+
+La route /api/subdomains permet les opérations suivantes :
+
+* Get : Récupère les sous-domaines existants
+* Post : Créé un nouveau sous domaine, et l'active éventuellement (selon que la prop `active` soit true ou non)
+* Put : Permet l'activation d'un sous-domaine en passant la propriété `active=true`
+
+Une organisation est limitée à 3 sous-domaines. Un sous-domaine ne peut être modifié une fois créé, à l'exception de 
+son statut (actif / inactif)
+
+Quand un sous domaine est ajouté, il est ajouté au fichier bind des serveurs prod-back et vpn (cf. plus bas).
+
+Quand un sous domaine est activé, les opérations suivantes sont réalisées :
+
+* les autres sous-domaines de l'organisation sont désactivés, de manière à ne conserver qu'un seul sous-domaine actif à la fois
+* une commande de mise à jour est envoyée à typo3 via l'API Http
+* le nom de l'admin de l'organisation est mis à jour pour être de la forme 'admin<subdomain>'
+* un email de confirmation est envoyé à la structure
+
+## CLI
+
+On peut ajouter et activer un nouveau sous-domaine au moyen de la commande :
+
+    php bin/console ot:subdomain:add <organization-id> <subdomain>
+
+
+## Fichier Bind
+
+Le fichier "bind" rend possible la résolution des sous-domaines au niveau du serveur DNS (situé sur prod-back).
+Chaque fois qu'un nouveau sous-domaine est enregistré, ce fichier doit être mis à jour. Cependant, pour des questions de
+droits, c'est l'utilisateur root qui doit procéder à cette mise à jour.
+
+Voilà ce qu'il se passe lorsqu'une structure enregistre un nouveau sous-domaine depuis `prod-back`:
+
+1. Le logiciel ajoute le sous-domaine en question dans une nouvelle ligne du fichier tampon `/env/subdomain.txt`
+2. Un cron tourne toutes les 5 minutes et consomme le contenu de ce fichier avant de le vider
+3. Ce cron exécute le script `/env/add-subdomain`, qui met à jour le fichier bind `/etc/bind/zones/opentalent.fr.db`
+4. Une fois mis à jour le fichier bind, le cron exécuté sur prod-back (le serveur DNS) envoie par ssh le contenu du
+   fichier subdomain.txt sur un même fichier `/env/subdomain.txt` situé sur le serveur `vpn`
+5. Le même cron tourne toutes les 5 minutes sur le serveur vpn procède à son tour à la mise à jour de son fichier bind
+
+La mise à jour des deux fichiers bind est nécessaire, pour permettre la prise en compte des sous-domaines depuis
+l'intérieur ou l'extérieur du VPN.
+
+Lorsqu'un nouveau sous-domaine est enregistré depuis `prod-v2`:
+
+> TODO: à revoir
+
+1. Le logiciel remplit de la même façon un fichier `/env/subdomain.txt`
+2. La suite se déroule comme dans le premier scénario

+ 20 - 0
docker-compose.override.yml

@@ -0,0 +1,20 @@
+version: '3'
+
+services:
+###> doctrine/doctrine-bundle ###
+  database:
+    ports:
+      - "5432"
+###< doctrine/doctrine-bundle ###
+
+###> symfony/mailer ###
+  mailer:
+    image: schickling/mailcatcher
+    ports: ["1025", "1080"]
+###< symfony/mailer ###
+
+###> symfony/mercure-bundle ###
+  mercure:
+    ports:
+      - "80"
+###< symfony/mercure-bundle ###

+ 44 - 0
docker-compose.yml

@@ -0,0 +1,44 @@
+version: '3'
+
+services:
+###> doctrine/doctrine-bundle ###
+  database:
+    image: postgres:${POSTGRES_VERSION:-15}-alpine
+    environment:
+      POSTGRES_DB: ${POSTGRES_DB:-app}
+      # You should definitely change the password in production
+      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
+      POSTGRES_USER: ${POSTGRES_USER:-app}
+    volumes:
+      - database_data:/var/lib/postgresql/data:rw
+      # You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
+      # - ./docker/db/data:/var/lib/postgresql/data:rw
+###< doctrine/doctrine-bundle ###
+
+###> symfony/mercure-bundle ###
+  mercure:
+    image: dunglas/mercure
+    restart: unless-stopped
+    environment:
+      SERVER_NAME: ':80'
+      MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
+      MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
+      # Set the URL of your Symfony project (without trailing slash!) as value of the cors_origins directive
+      MERCURE_EXTRA_DIRECTIVES: |
+        cors_origins http://127.0.0.1:8000
+    # Comment the following line to disable the development mode
+    command: /usr/bin/caddy run --config /etc/caddy/Caddyfile.dev
+    volumes:
+      - mercure_data:/data
+      - mercure_config:/config
+###< symfony/mercure-bundle ###
+
+volumes:
+###> doctrine/doctrine-bundle ###
+  database_data:
+###< doctrine/doctrine-bundle ###
+
+###> symfony/mercure-bundle ###
+  mercure_data:
+  mercure_config:
+###< symfony/mercure-bundle ###

+ 0 - 0
fixtures/.gitignore


+ 1 - 0
phpstan.json

@@ -0,0 +1 @@
+[]

+ 12 - 0
phpstan.neon.dist

@@ -0,0 +1,12 @@
+# https://phpstan.org/config-reference
+parameters:
+	level: 6
+	treatPhpDocTypesAsCertain: false
+	checkGenericClassInNonGenericObjectType: false
+
+# on ignore les erreurs qui proviennent d'un importe d'un attribut PhpStorm
+# on ignore les erreurs qui imposent d'indiquer le type d'un iterable dans la phpDoc (cf: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type)
+	ignoreErrors :
+	    - '#Attribute class JetBrains\\PhpStorm\\[a-zA-Z]+ does not exist.#'
+	paths:
+		- src

+ 17 - 5
phpunit.xml.dist

@@ -1,24 +1,36 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
-<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" backupGlobals="false" colors="true" bootstrap="tests/bootstrap.php">
-  <coverage processUncoveredFiles="true">
+<phpunit
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
+        backupGlobals="false"
+        colors="true"
+        bootstrap="tests/bootstrap.php"
+        defaultTestSuite="unit"
+>
+  <coverage includeUncoveredFiles="true">
     <include>
       <directory suffix=".php">src</directory>
     </include>
     <report>
       <html outputDirectory="./coverage" lowUpperBound="35" highLowerBound="70"/>
+      <text outputFile="php://stdout" showUncoveredFiles="true" showOnlySummary="true"/>
     </report>
   </coverage>
   <php>
     <ini name="error_reporting" value="-1"/>
-    <server name="APP_ENV" value="test" force="true"/>
+    <server name="APP_ENV" value="staging" force="true"/>
     <server name="SHELL_VERBOSITY" value="-1"/>
     <server name="SYMFONY_PHPUNIT_REMOVE" value=""/>
     <server name="SYMFONY_PHPUNIT_VERSION" value="9.4"/>
+    <server name="KERNEL_CLASS" value="App\Kernel" />
   </php>
   <testsuites>
-    <testsuite name="Project Test Suite">
-      <directory>tests</directory>
+    <testsuite name="unit">
+      <directory>tests/Unit</directory>
+    </testsuite>
+    <testsuite name="application">
+      <directory>tests/Application</directory>
     </testsuite>
   </testsuites>
   <listeners>

+ 14 - 0
public/.htaccess

@@ -0,0 +1,14 @@
+<IfModule mod_rewrite.c>
+    Options -MultiViews
+    RewriteEngine On
+    RewriteCond %{SERVER_PORT} 80
+    RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
+    RewriteCond %{REQUEST_FILENAME} !-f
+    RewriteRule ^(.*)$ index.php [QSA,L]
+</IfModule>
+
+<IfModule !mod_rewrite.c>
+    <IfModule mod_alias.c>
+        RedirectMatch 302 ^/$ /index.php/
+    </IfModule>
+</IfModule>

+ 32 - 0
readme.md

@@ -14,4 +14,36 @@ Pour consommer les signaux messengers, lancer :
 
     php bin/console messenger:consume async
 
+En cas d'erreur lié à l'absence de la table messenger_messages, lancer d'abord :
+
+    php bin/console messenger:setup-transports
+
+
 > Voir: <https://symfony.com/doc/5.4/messenger.html#consuming-messages-running-the-worker>
+
+## Chercher les erreurs avec phpstan
+
+Pour exécuter une analyse du code :
+
+    vendor/bin/phpstan analyse
+
+> Voir : https://phpstan.org/user-guide/getting-started
+
+# PHPStan
+
+Le fichier comportant la configuration pour chacun est phpstan.neon.dist
+Le fichier phpstan.neon n'est pas versionné et surcharge les valeurs du .dist
+
+## Principales commandes 
+
+Pour lancer l'analyse
+
+    vendor/bin/phpstan analyse
+
+Pour vider le cache (utiles entre chaque analyse si les modifications ne sont pas prises en compte)
+
+    vendor/bin/phpstan clear-result-cache
+
+> Voir : https://phpstan.org/
+
+

+ 19 - 25
rector.php

@@ -2,30 +2,24 @@
 
 declare(strict_types=1);
 
-use Rector\Core\Configuration\Option;
-use Rector\Core\ValueObject\PhpVersion;
-use Rector\Doctrine\Set\DoctrineSetList;
-use Rector\Php74\Rector\Property\TypedPropertyRector;
-use Rector\Set\ValueObject\LevelSetList;
-use Rector\Symfony\Set\SensiolabsSetList;
-use Rector\Symfony\Set\SymfonyLevelSetList;
-use Rector\Symfony\Set\SymfonySetList;
-use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector;
+use Rector\Config\RectorConfig;
+use \Rector\Symfony\Set\SymfonySetList;
+
+return static function (RectorConfig $rectorConfig): void {
+    $rectorConfig->paths([
+        __DIR__ . '/config',
+        __DIR__ . '/public',
+        __DIR__ . '/src',
+    ]);
 
-return static function (ContainerConfigurator $containerConfigurator): void {
-    // region Symfony Container
-    $parameters = $containerConfigurator->parameters();
-    $parameters->set(
-        Option::SYMFONY_CONTAINER_XML_PATH_PARAMETER,
-        __DIR__ . '/var/cache/dev/App_KernelDevDebugContainer.xml'
-    );
-    // endregion
-    // Define what rule sets will be applied
-    $containerConfigurator->import(DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES);
-    $containerConfigurator->import(SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES);
-    $containerConfigurator->import(SensiolabsSetList::FRAMEWORK_EXTRA_61);
-    // get services (needed for register a single rule)
-    // $services = $containerConfigurator->services();
     // register a single rule
-    // $services->set(TypedPropertyRector::class);
-};
+    $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
+    $rectorConfig->symfonyContainerXml(__DIR__ . '/var/cache/docker/App_KernelDockerDebugContainer.xml');
+
+    $rectorConfig->sets([
+        \Rector\Symfony\Set\SymfonyLevelSetList::UP_TO_SYMFONY_60,
+        SymfonySetList::SYMFONY_CODE_QUALITY,
+        SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION,
+    ]);
+};

+ 2 - 1
sql/schema-extensions/002-view_federation_structures.sql

@@ -1,6 +1,6 @@
 CREATE OR REPLACE VIEW view_federation_structures
 AS
-    SELECT o.id, o.name, o.logo_id as logoId, o.description, o.image_id as imageId, o.principalType as type, p.otherWebsite as website,
+    SELECT o.id, o.name, o.logo_id as logoId, o.description, o.image_id as imageId, o.principalType as type, p.website as website,
            CONCAT('[', GROUP_CONCAT(COLUMN_JSON(COLUMN_CREATE(
                    'type', oa.type, 'latitude', a.latitude, 'longitude', a.longitude,
                    'streetAddress', TRIM(BOTH '\n' FROM CONCAT_WS('\n', a.streetAddress, a.streetAddressSecond, a.streetAddressThird)),
@@ -12,6 +12,7 @@ AS
                AS practices,
            oar.articles, n1.parent_id as parentId, net1.name as parentName,
            CONCAT_WS(',', n1.parent_id, n2.parent_id, n3.parent_id, n4.parent_id, n5.parent_id) as parents
+
     FROM opentalent.Organization o
              INNER JOIN opentalent.Parameters p on o.parameters_id = p.id
              LEFT JOIN opentalent.OrganizationAddressPostal oa on oa.organization_id = o.id

+ 0 - 16
src/Annotation/ActivityYearConstraintAware.php

@@ -1,16 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\Annotation;
-
-use Attribute;
-
-/**
- * Classe ActivityYearConstraintAware qui gère l'annotation pour le Doctrine filter
- */
-#[Attribute(Attribute::TARGET_CLASS)]
-final class ActivityYearConstraintAware
-{
-    public string $startYearFieldName;
-    public string $endYearFieldName;
-}

+ 0 - 16
src/Annotation/DateTimeConstraintAware.php

@@ -1,16 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\Annotation;
-
-use Attribute;
-
-/**
- * Classe DateTimeConstraintAware qui gère l'annotation pour le Doctrine filter
- */
-#[Attribute(Attribute::TARGET_CLASS)]
-final class DateTimeConstraintAware
-{
-    public string $startDateFieldName;
-    public string $endDateFieldName;
-}

+ 0 - 0
src/ApiResource/.gitignore


+ 16 - 31
src/ApiResources/Access/AdminAccess.php

@@ -1,32 +1,21 @@
 <?php
-declare(strict_types=1);
+declare (strict_types=1);
 
 namespace App\ApiResources\Access;
 
-use ApiPlatform\Core\Annotation\ApiProperty;
-use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Metadata\Put;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\ApiProperty;
 use App\ApiResources\ApiResourcesInterface;
+use App\State\Processor\Access\AdminAccessProcessor;
+use App\State\Provider\Access\AdminAccessProvider;
 use JetBrains\PhpStorm\Pure;
 use Symfony\Component\Validator\Constraints as Assert;
-
 /**
  * Classe resource qui contient les champs d'un compte admin
  */
-#[ApiResource(
-    collectionOperations:[],
-    itemOperations: [
-        'get' => [
-            'method' => 'GET',
-            'path' => '/admin/{id}',
-            'defaults' => ['id' => 0]
-        ],
-        'put' => [
-            'method' => 'PUT',
-            'path' => '/admin/{id}',
-            'defaults' => ['id' => 0]
-        ]
-    ]
-)]
+#[ApiResource(operations: [])]
 class AdminAccess implements ApiResourcesInterface
 {
     #[ApiProperty(identifier: true)]
@@ -37,45 +26,41 @@ class AdminAccess implements ApiResourcesInterface
     #[Assert\Email(message: 'invalid-email-format', mode: 'strict')]
     private ?string $email = null;
 
-
-    #[Pure] public function __construct()
+    #[Pure]
+    public function __construct()
     {
     }
 
-    public function getId(): ?int
+    public function getId() : ?int
     {
         return $this->id;
     }
 
-    public function setId(?int $id): self
+    public function setId(?int $id) : self
     {
         $this->id = $id;
-
         return $this;
     }
 
-    public function getUsername(): ?string
+    public function getUsername() : ?string
     {
         return $this->username;
     }
 
-    public function setUsername(?string $username): self
+    public function setUsername(?string $username) : self
     {
         $this->username = $username;
-
         return $this;
     }
 
-
-    public function getEmail(): ?string
+    public function getEmail() : ?string
     {
         return $this->email;
     }
 
-    public function setEmail(?string $email): self
+    public function setEmail(?string $email) : self
     {
         $this->email = $email;
-
         return $this;
     }
 }

+ 48 - 0
src/ApiResources/Core/File/DownloadRequest.php

@@ -0,0 +1,48 @@
+<?php
+declare (strict_types=1);
+
+namespace App\ApiResources\Core\File;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use App\State\Provider\Core\DownloadRequestProvider;
+
+/**
+ * A request for a file from the LocalStorage
+ */
+#[ApiResource(
+    operations: [
+        new Get(
+            uriTemplate: '/download/{fileId}',
+            requirements: ['fileId' => '\\d+'],
+            security: 'is_granted("ROLE_FILE")',
+            provider: DownloadRequestProvider::class
+        ),
+        new Get(
+            uriTemplate: '/internal/download/{fileId}',
+            requirements: ['fileId' => '\\d+'],
+            provider: DownloadRequestProvider::class
+        )
+    ]
+)]
+class DownloadRequest
+{
+    #[ApiProperty(identifier: true)]
+    private int $fileId;
+
+    /**
+     * @return int
+     */
+    public function getFileId() : int
+    {
+        return $this->fileId;
+    }
+    /**
+     * @param int $fileId
+     */
+    public function setFileId(int $fileId) : void
+    {
+        $this->fileId = $fileId;
+    }
+}

+ 16 - 16
src/ApiResources/Cotisation/Cotisation.php

@@ -1,33 +1,34 @@
 <?php
-declare(strict_types=1);
+declare (strict_types=1);
 
 namespace App\ApiResources\Cotisation;
 
-use ApiPlatform\Core\Annotation\ApiProperty;
-use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\ApiProperty;
 use App\ApiResources\ApiResourcesInterface;
-use Symfony\Component\Serializer\Annotation\Groups;
+use App\State\Provider\Cotisation\CotisationProvider;
 use Symfony\Component\Validator\Constraints as Assert;
 
 /**
  * Classe resource qui contient les informations des cotisations de la 5.9
+ *
+ * Security :
+ *   * @see App\Security\Voter\CotisationVoter
  */
-#[ApiResource(
-    collectionOperations:[],
-    itemOperations: [
-        'get' => [
-            'method' => 'GET',
-            'path' => '/cotisations/{organizationId}',
-            'security' => 'is_granted("ROLE_COTISATION", object) and object.getOrganizationId() == user.getOrganization().getId()',
-        ]
-    ]
-)]
+#[ApiResource(operations: [
+    new Get(
+        uriTemplate: '/cotisations/{organizationId}',
+        security: 'is_granted("ROLE_COTISATION", object) and object.getOrganizationId() == user.getOrganization().getId()',
+        provider: CotisationProvider::class
+    )
+])]
 class Cotisation implements ApiResourcesInterface
 {
     #[ApiProperty(identifier: true)]
     private int $organizationId;
 
-    #[Assert\Choice(callback: ['\App\Enum\Cotisation\AlertStateEnum', 'toArray'], message: 'invalid-alert-state-enum')]
+    #[Assert\Choice(callback: ['\\App\\Enum\\Cotisation\\AlertStateEnum', 'toArray'], message: 'invalid-alert-state-enum')]
     private ?string $alertState = null;
 
     private ?int $cotisationYear = null;
@@ -62,7 +63,6 @@ class Cotisation implements ApiResourcesInterface
     public function setCotisationYear(?int $cotisationYear): self
     {
         $this->cotisationYear = $cotisationYear;
-
         return $this;
     }
 }

+ 15 - 16
src/ApiResources/Dolibarr/DolibarrAccount.php

@@ -1,11 +1,14 @@
 <?php
-declare(strict_types=1);
+declare (strict_types=1);
 
 namespace App\ApiResources\Dolibarr;
 
-use ApiPlatform\Core\Annotation\ApiProperty;
-use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Metadata\Link;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\ApiProperty;
 use App\ApiResources\ApiResourcesInterface;
+use App\State\Provider\Dolibarr\DolibarrAccountProvider;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use JetBrains\PhpStorm\Pure;
@@ -16,23 +19,19 @@ use Symfony\Component\Serializer\Annotation\Groups;
  * (aussi nommé 'ThirdParty' ou 'Society' dans dolibarr)
  */
 #[ApiResource(
-    collectionOperations:[],
-    itemOperations: [
-        'get' => [
-            'security' => '(is_granted("ROLE_ADMIN_CORE") or 
+    operations: [
+        new Get(
+            uriTemplate: '/dolibarr/account/{organizationId}',
+            requirements: ['organizationId' => '\\d+'],
+            normalizationContext: ['groups' => ['dolibarr_get']],
+            security: '(is_granted("ROLE_ADMIN_CORE") or 
                             is_granted("ROLE_ADMINISTRATIF_MANAGER_CORE") or 
                             is_granted("ROLE_PEDAGOGICS_MANAGER_CORE") or 
                             is_granted("ROLE_FINANCIAL_MANAGER_CORE")
                            ) and object.getOrganizationId() == user.getOrganization().getId()',
-            'method' => 'GET',
-            'path' => '/dolibarr/account/{organizationId}',
-            'requirements' => ['organizationId' => '\d+'],
-            'normalization_context' => [
-                'groups' => ['dolibarr_get']
-            ],
-        ],
-    ],
-    compositeIdentifier: false,
+            provider: DolibarrAccountProvider::class
+        )
+    ]
 )]
 class DolibarrAccount implements ApiResourcesInterface
 {

+ 6 - 7
src/ApiResources/Dolibarr/DolibarrBill.php

@@ -1,10 +1,11 @@
 <?php
-declare(strict_types=1);
+declare (strict_types=1);
 
 namespace App\ApiResources\Dolibarr;
 
-use ApiPlatform\Core\Annotation\ApiProperty;
-use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\ApiProperty;
 use App\ApiResources\ApiResourcesInterface;
 use Symfony\Component\Serializer\Annotation\Groups;
 
@@ -12,9 +13,8 @@ use Symfony\Component\Serializer\Annotation\Groups;
  * Bill of a society, retrieved from dolibarr
  */
 #[ApiResource(
-    collectionOperations:[],
-    itemOperations: [
-        'get'
+    operations: [
+        new Get()
     ]
 )]
 class DolibarrBill implements ApiResourcesInterface
@@ -133,5 +133,4 @@ class DolibarrBill implements ApiResourcesInterface
         $this->paid = $paid;
         return $this;
     }
-
 }

+ 6 - 6
src/ApiResources/Dolibarr/DolibarrContract.php

@@ -1,10 +1,11 @@
 <?php
-declare(strict_types=1);
+declare (strict_types=1);
 
 namespace App\ApiResources\Dolibarr;
 
-use ApiPlatform\Core\Annotation\ApiProperty;
-use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\ApiProperty;
 use App\ApiResources\ApiResourcesInterface;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
@@ -15,9 +16,8 @@ use Symfony\Component\Serializer\Annotation\Groups;
  * Contract of a society, retrieved from dolibarr
  */
 #[ApiResource(
-    collectionOperations:[],
-    itemOperations: [
-        'get'
+    operations: [
+        new Get()
     ]
 )]
 class DolibarrContract implements ApiResourcesInterface

+ 6 - 6
src/ApiResources/Dolibarr/DolibarrContractLine.php

@@ -1,10 +1,11 @@
 <?php
-declare(strict_types=1);
+declare (strict_types=1);
 
 namespace App\ApiResources\Dolibarr;
 
-use ApiPlatform\Core\Annotation\ApiProperty;
-use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\ApiProperty;
 use App\ApiResources\ApiResourcesInterface;
 use Symfony\Component\Serializer\Annotation\Groups;
 
@@ -12,9 +13,8 @@ use Symfony\Component\Serializer\Annotation\Groups;
  *  Lines (services) included in a society contract, as retrieved from dolibarr
  */
 #[ApiResource(
-    collectionOperations:[],
-    itemOperations: [
-        'get'
+    operations: [
+        new Get()
     ]
 )]
 class DolibarrContractLine implements ApiResourcesInterface

+ 0 - 44
src/ApiResources/DownloadRequest.php

@@ -1,44 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\ApiResources;
-
-use ApiPlatform\Core\Annotation\ApiProperty;
-use ApiPlatform\Core\Annotation\ApiResource;
-
-/**
- * A request for a file from the LocalStorage
- */
-#[ApiResource(
-    collectionOperations:[],
-    itemOperations: [
-        'get' => [
-            'security' => 'is_granted("ROLE_FILE")',
-            'method' => 'GET',
-            'path' => '/download/{fileId}',
-            'requirements' => ['fileId' => '\d+']
-        ],
-    ],
-    compositeIdentifier: false,
-)]
-class DownloadRequest
-{
-    #[ApiProperty(identifier: true)]
-    private int $fileId;
-
-    /**
-     * @return int
-     */
-    public function getFileId(): int
-    {
-        return $this->fileId;
-    }
-
-    /**
-     * @param int $fileId
-     */
-    public function setFileId(int $fileId): void
-    {
-        $this->fileId = $fileId;
-    }
-}

+ 20 - 11
src/ApiResources/Enum/Enum.php

@@ -1,33 +1,37 @@
 <?php
-declare(strict_types=1);
+declare (strict_types=1);
 
 namespace App\ApiResources\Enum;
 
-use ApiPlatform\Core\Annotation\ApiProperty;
-use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\ApiProperty;
 use App\ApiResources\ApiResourcesInterface;
+use App\State\Provider\Enum\EnumProvider;
 
 /**
  * Classe resource qui contient les champs disponibles lors d'un appel à enum.
  */
 #[ApiResource(
-    collectionOperations: [],
-    itemOperations: [
-        'get' => [
-            'method' => 'GET',
-            'path' => '/enum/{name}'
-        ]
+    operations: [
+        new Get(
+            uriTemplate: '/enum/{name}',
+            provider: EnumProvider::class
+        )
     ]
 )]
 class Enum implements ApiResourcesInterface
 {
     #[ApiProperty(identifier: true)]
     private string $name;
+
+    /** @var list<string> $items */
     private array $items = [];
 
     public function __construct()
     {
     }
+
     public function getName(): string
     {
         return $this->name;
@@ -36,17 +40,22 @@ class Enum implements ApiResourcesInterface
     public function setName(string $name): self
     {
         $this->name = $name;
-
         return $this;
     }
 
-
+    /**
+     * @param list<string> $items
+     * @return $this
+     */
     public function setItems(array $items): self
     {
         $this->items = $items;
         return $this;
     }
 
+    /**
+     * @return list<string>
+     */
     public function getItems(): array
     {
         return $this->items;

+ 1 - 1
src/ApiResources/Export/ExportRequest.php

@@ -3,7 +3,7 @@ declare(strict_types=1);
 
 namespace App\ApiResources\Export;
 
-use ApiPlatform\Core\Annotation\ApiProperty;
+use ApiPlatform\Metadata\ApiProperty;
 use Symfony\Component\Validator\Constraints as Assert;
 use App\Enum\Export\ExportFormatEnum;
 

+ 1 - 1
src/ApiResources/Export/ExportRequestInterface.php

@@ -2,7 +2,7 @@
 
 namespace App\ApiResources\Export;
 
-class ExportRequestInterface
+interface ExportRequestInterface
 {
 
 }

+ 33 - 25
src/ApiResources/Export/LicenceCmf/LicenceCmfOrganizationER.php

@@ -1,45 +1,53 @@
 <?php
-declare(strict_types=1);
+
+declare (strict_types=1);
 
 namespace App\ApiResources\Export\LicenceCmf;
 
-use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\Metadata\Delete;
+use ApiPlatform\Metadata\Patch;
+use ApiPlatform\Metadata\Put;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\ApiResource;
 use App\ApiResources\Export\ExportRequest;
+use App\ApiResources\Export\ExportRequestInterface;
+use App\State\Processor\Export\LicenceCmf\ExportRequestProcessor;
 use Symfony\Component\Validator\Constraints as Assert;
 
 /**
- * Requête d'export d'une licence CMF pour l'organization ciblée
- *
- * Appeler avec une requête POST à /export/licence-cmf/organization,
- * avec un body comme :
- *
- *   {"organizationId" : 1}
- */
+* Requête d'export d'une licence CMF pour l'organization ciblée
+*
+* Appeler avec une requête POST à /export/licence-cmf/organization,
+* avec un body comme :
+*
+* {"organizationId" : 1}
+*/
 #[ApiResource(
-    collectionOperations: [
-        'post' => [
-            'security' => '(is_granted("ROLE_ADMIN_CORE") or is_granted("ROLE_ADMINISTRATIF_MANAGER_CORE"))',
-            'method' => 'POST',
-            'path' => '/cmf-licence/organization',
-        ],
-    ],
-    routePrefix: '/export'
+    operations: [
+new Post(
+    uriTemplate: '/cmf-licence/organization',
+    security: '(is_granted("ROLE_ADMIN_CORE") or is_granted("ROLE_ADMINISTRATIF_MANAGER_CORE"))'
+),
+],
+    routePrefix: '/export',
+    processor: ExportRequestProcessor::class
 )]
 class LicenceCmfOrganizationER extends ExportRequest
 {
     /**
-     * Format de sortie attendu (pdf seulement ici)
-     * @var string
-     */
+    * Format de sortie attendu (pdf seulement ici)
+    * @var string
+    */
     #[Assert\EqualTo('pdf')]
     protected string $format = 'pdf';
 
     /**
-     * Retourne l'année de la licence
-     * (toujours l'année courante, sauf dans le cas des tests où on pourra mocker cette méthode)
-     *
-     * @return int
-     */
+    * Retourne l'année de la licence
+    * (toujours l'année courante, sauf dans le cas des tests où on pourra mocker cette méthode)
+    *
+    * @return int
+    */
     public function getYear(): int
     {
         return (int)date('Y');

+ 12 - 11
src/ApiResources/Mobyt/MobytUserStatus.php

@@ -1,29 +1,30 @@
 <?php
-declare(strict_types=1);
+declare (strict_types=1);
 
 namespace App\ApiResources\Mobyt;
 
-use ApiPlatform\Core\Annotation\ApiProperty;
-use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\ApiProperty;
 use App\ApiResources\ApiResourcesInterface;
+use App\State\Provider\Mobyt\MobytUserStatusProvider;
 
 /**
  * Statut de l'utilisateur Mobyt correspondant à l'organization donnée en paramètre
  */
 #[ApiResource(
-    collectionOperations:[],
-    itemOperations: [
-        'get' => [
-            'security' => '(is_granted("ROLE_ADMIN_CORE") or
+    operations: [
+        new Get(
+            uriTemplate: '/mobyt/status/{organizationId}',
+            requirements: ['organizationId' => '\\d+'],
+            security: '(is_granted("ROLE_ADMIN_CORE") or
                             is_granted("ROLE_ADMINISTRATIF_MANAGER_CORE") or
                             is_granted("ROLE_PEDAGOGICS_MANAGER_CORE") or
                             is_granted("ROLE_FINANCIAL_MANAGER_CORE") or
                             is_granted("ROLE_TEXTO")
                            ) and object.getOrganizationId() == user.getOrganization().getId()',
-            'method' => 'GET',
-            'path' => '/mobyt/status/{organizationId}',
-            'requirements' => ['organizationId' => '\d+'],
-        ],
+            provider: MobytUserStatusProvider::class
+        )
     ]
 )]
 class MobytUserStatus implements ApiResourcesInterface

+ 59 - 0
src/ApiResources/OnlineRegistration/RegistrationAvailability.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace App\ApiResources\OnlineRegistration;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use App\ApiResources\ApiResourcesInterface;
+use App\State\Provider\OnlineRegistration\RegistrationAvailabilityProvider;
+
+#[ApiResource(
+    operations: [
+        new Get(
+            uriTemplate: '/online_registration/availability/{accessId}',
+            requirements: ['accessId' => '\\d+'],
+            defaults: ['accessId' => 0],
+            provider: RegistrationAvailabilityProvider::class
+        )
+    ]
+)]
+class RegistrationAvailability implements ApiResourcesInterface
+{
+    #[ApiProperty(identifier: true)]
+    private int $accessId;
+
+    private bool $available;
+
+    private ?string $message = null;
+
+    public function getAccessId(): int
+    {
+        return $this->accessId;
+    }
+
+    public function setAccessId(int $accessId): void
+    {
+        $this->accessId = $accessId;
+    }
+
+    public function isAvailable(): bool
+    {
+        return $this->available;
+    }
+
+    public function setAvailable(bool $available): void
+    {
+        $this->available = $available;
+    }
+
+    public function getMessage(): ?string
+    {
+        return $this->message;
+    }
+
+    public function setMessage(?string $message): void
+    {
+        $this->message = $message;
+    }
+}

+ 48 - 0
src/ApiResources/OnlineRegistration/RegistrationStatus.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace App\ApiResources\OnlineRegistration;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use App\ApiResources\ApiResourcesInterface;
+use App\State\Provider\OnlineRegistration\RegistrationStatusProvider;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[ApiResource(
+    operations: [
+        new Get(
+            uriTemplate: '/online_registration/status/{accessId}',
+            requirements: ['accessId' => '\\d+'],
+            defaults: ['accessId' => 0],
+            provider: RegistrationStatusProvider::class
+        )
+    ]
+)]
+class RegistrationStatus implements ApiResourcesInterface
+{
+    #[ApiProperty(identifier: true)]
+    private int $accessId;
+
+    private ?string $status = null;
+
+    public function getAccessId(): int
+    {
+        return $this->accessId;
+    }
+
+    public function setAccessId(int $accessId): void
+    {
+        $this->accessId = $accessId;
+    }
+
+    public function getStatus(): ?string
+    {
+        return $this->status;
+    }
+
+    public function setStatus(?string $status): void
+    {
+        $this->status = $status;
+    }
+}

+ 34 - 25
src/ApiResources/Profile/AccessProfile.php

@@ -1,11 +1,16 @@
 <?php
-declare(strict_types=1);
+declare (strict_types=1);
 
 namespace App\ApiResources\Profile;
 
-use ApiPlatform\Core\Annotation\ApiProperty;
-use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\Put;
 use App\ApiResources\ApiResourcesInterface;
+use App\State\Processor\Access\AdminAccessProcessor;
+use App\State\Provider\Access\AccessProfileProvider;
+use App\State\Provider\Access\AdminAccessProvider;
 use JetBrains\PhpStorm\Pure;
 use Symfony\Component\Serializer\Annotation\Groups;
 use Doctrine\Common\Collections\ArrayCollection;
@@ -15,16 +20,13 @@ use Doctrine\Common\Collections\Collection;
  * Classe resource qui contient les champs disponibles lors d'un appel à my_profile.
  */
 #[ApiResource(
-    collectionOperations:[],
-    itemOperations: [
-        'get' => [
-            'normalization_context' => [
-                'groups' => ['access_profile_read']
-            ],
-            'method' => 'GET',
-            'path' => '/my_profile/{id}',
-            'defaults' => ['id' => 0]
-        ]
+    operations: [
+        new Get(
+            uriTemplate: '/my_profile/{id}',
+            defaults: ['id' => 0],
+            normalizationContext: ['groups' => ['access_profile_read']],
+            provider: AccessProfileProvider::class
+        )
     ]
 )]
 class AccessProfile implements ApiResourcesInterface
@@ -51,14 +53,16 @@ class AccessProfile implements ApiResourcesInterface
     #[Groups('access_profile_read')]
     private ?int $avatarId = null;
 
+    /** @var list<string> $roles */
     #[Groups('access_profile_read')]
     private ?array $roles = [];
 
     #[Groups('access_profile_read')]
     private ?int $activityYear = null;
 
+    /** @var bool[] $historical */
     #[Groups('access_profile_read')]
-    private ?array $historical=[];
+    private ?array $historical = [];
 
     #[Groups('access_profile_read')]
     private bool $isGuardian = false;
@@ -78,7 +82,8 @@ class AccessProfile implements ApiResourcesInterface
     #[Groups('access_profile_read')]
     private ?AccessProfile $originalAccess = null;
 
-    #[Pure] public function __construct()
+    #[Pure]
+    public function __construct()
     {
         $this->multiAccesses = new ArrayCollection();
         $this->familyAccesses = new ArrayCollection();
@@ -92,7 +97,6 @@ class AccessProfile implements ApiResourcesInterface
     public function setId(?int $id): self
     {
         $this->id = $id;
-
         return $this;
     }
 
@@ -104,7 +108,6 @@ class AccessProfile implements ApiResourcesInterface
     public function setIsAdminAccess(bool $isAdminAccess): self
     {
         $this->isAdminAccess = $isAdminAccess;
-
         return $this;
     }
 
@@ -116,7 +119,6 @@ class AccessProfile implements ApiResourcesInterface
     public function setIsSuperAdminAccess(bool $isSuperAdminAccess): self
     {
         $this->isSuperAdminAccess = $isSuperAdminAccess;
-
         return $this;
     }
 
@@ -128,7 +130,6 @@ class AccessProfile implements ApiResourcesInterface
     public function setOrganization(?OrganizationProfile $organization): self
     {
         $this->organization = $organization;
-
         return $this;
     }
 
@@ -140,7 +141,6 @@ class AccessProfile implements ApiResourcesInterface
     public function setName(?string $name): self
     {
         $this->name = $name;
-
         return $this;
     }
 
@@ -152,7 +152,6 @@ class AccessProfile implements ApiResourcesInterface
     public function setGivenName(?string $givenName): self
     {
         $this->givenName = $givenName;
-
         return $this;
     }
 
@@ -178,13 +177,19 @@ class AccessProfile implements ApiResourcesInterface
         return $this;
     }
 
+    /**
+     * @return list<string>
+     */
     public function getRoles(): array
     {
         $roles = $this->roles;
-
         return array_unique($roles);
     }
 
+    /**
+     * @param list<string> $roles
+     * @return $this
+     */
     public function setRoles(array $roles): self
     {
         $this->roles = $roles;
@@ -199,7 +204,6 @@ class AccessProfile implements ApiResourcesInterface
     public function setIsGuardian(bool $isGuardian): self
     {
         $this->isGuardian = $isGuardian;
-
         return $this;
     }
 
@@ -211,7 +215,6 @@ class AccessProfile implements ApiResourcesInterface
     public function setIsPayor(bool $isPayor): self
     {
         $this->isPayor = $isPayor;
-
         return $this;
     }
 
@@ -272,15 +275,21 @@ class AccessProfile implements ApiResourcesInterface
     public function setActivityYear(?int $activityYear): self
     {
         $this->activityYear = $activityYear;
-
         return $this;
     }
 
+    /**
+     * @return bool[]
+     */
     public function getHistorical(): array
     {
         return $this->historical;
     }
 
+    /**
+     * @param bool[] $historical
+     * @return $this
+     */
     public function setHistorical(array $historical): self
     {
         $this->historical = $historical;

+ 23 - 19
src/ApiResources/Profile/OrganizationProfile.php

@@ -1,21 +1,21 @@
 <?php
-declare(strict_types=1);
+declare (strict_types=1);
 
 namespace App\ApiResources\Profile;
 
-use ApiPlatform\Core\Annotation\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\Get;
 use App\ApiResources\ApiResourcesInterface;
+use App\Entity\Organization\Organization;
 use Symfony\Component\Serializer\Annotation\Groups;
-use ApiPlatform\Core\Annotation\ApiResource;
 
 /**
  * Classe resource qui contient les champs relatifs aux organizations présentent dans la requete my_profile.
  */
-
 #[ApiResource(
-    collectionOperations:[],
-    itemOperations: [
-        'get'
+    operations: [
+        new Get()
     ]
 )]
 class OrganizationProfile implements ApiResourcesInterface
@@ -33,18 +33,21 @@ class OrganizationProfile implements ApiResourcesInterface
     #[Groups('access_profile_read')]
     private ?string $legalStatus = null;
 
+    /** @var list<string> $networks  */
     #[Groups('access_profile_read')]
     private array $networks = [];
 
     #[Groups('access_profile_read')]
     private ?string $website = null;
 
+    /** @var list<string> $modules  */
     #[Groups('access_profile_read')]
     private ?array $modules = [];
 
     #[Groups('access_profile_read')]
     private bool $hasChildren = false;
 
+    /** @var list<OrganizationProfile>  */
     #[Groups('access_profile_read')]
     private ?array $parents = [];
 
@@ -65,7 +68,6 @@ class OrganizationProfile implements ApiResourcesInterface
     public function setId(?int $id): self
     {
         $this->id = $id;
-
         return $this;
     }
 
@@ -77,7 +79,6 @@ class OrganizationProfile implements ApiResourcesInterface
     public function setName(?string $name): self
     {
         $this->name = $name;
-
         return $this;
     }
 
@@ -89,7 +90,6 @@ class OrganizationProfile implements ApiResourcesInterface
     public function setProduct(?string $product): self
     {
         $this->product = $product;
-
         return $this;
     }
 
@@ -101,10 +101,12 @@ class OrganizationProfile implements ApiResourcesInterface
     public function setLegalStatus(?string $legalStatus): self
     {
         $this->legalStatus = $legalStatus;
-
         return $this;
     }
 
+    /**
+     * @return list<string>
+     */
     public function getNetworks(): array
     {
         return $this->networks;
@@ -113,7 +115,6 @@ class OrganizationProfile implements ApiResourcesInterface
     public function addNetwork(?string $network): self
     {
         $this->networks[] = $network;
-
         return $this;
     }
 
@@ -125,20 +126,25 @@ class OrganizationProfile implements ApiResourcesInterface
     public function setWebsite(?string $website): self
     {
         $this->website = $website;
-
         return $this;
     }
 
+    /**
+     * @return list<string>
+     */
     public function getModules(): array
     {
         $modules = $this->modules;
         return array_unique($modules);
     }
 
+    /**
+     * @param list<string> $modules
+     * @return $this
+     */
     public function setModules(array $modules): self
     {
         $this->modules = $modules;
-
         return $this;
     }
 
@@ -150,10 +156,12 @@ class OrganizationProfile implements ApiResourcesInterface
     public function setHasChildren(bool $hasChildren): self
     {
         $this->hasChildren = $hasChildren;
-
         return $this;
     }
 
+    /**
+     * @return list<OrganizationProfile>
+     */
     public function getParents(): array
     {
         return $this->parents;
@@ -173,7 +181,6 @@ class OrganizationProfile implements ApiResourcesInterface
     public function setShowAdherentList(bool $showAdherentList): self
     {
         $this->showAdherentList = $showAdherentList;
-
         return $this;
     }
 
@@ -185,11 +192,9 @@ class OrganizationProfile implements ApiResourcesInterface
     public function setCurrentYear(?int $currentYear): self
     {
         $this->currentYear = $currentYear;
-
         return $this;
     }
 
-
     public function getParametersId(): ?int
     {
         return $this->parametersId;
@@ -198,7 +203,6 @@ class OrganizationProfile implements ApiResourcesInterface
     public function setParametersId(?int $parametersId): self
     {
         $this->parametersId = $parametersId;
-
         return $this;
     }
 }

+ 16 - 16
src/ApiResources/Utils/GpsCoordinate.php

@@ -1,29 +1,25 @@
 <?php
-declare(strict_types=1);
+declare (strict_types=1);
 
 namespace App\ApiResources\Utils;
 
-use ApiPlatform\Core\Annotation\ApiProperty;
-use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Link;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\ApiProperty;
 use App\ApiResources\ApiResourcesInterface;
+use App\State\Provider\Utils\GpsCoordinateSearchingProvider;
 
 /**
  * Classe resource qui contient les champs de recherche des coordonnées GPS d'une adresse
  */
 #[ApiResource(
-    collectionOperations: [
-        'get' => [
-            'method' => 'GET',
-            'path' => '/gps-coordinate-searching'
-        ]
+    operations: [
+        new Get(uriTemplate: '/gps-coordinate-reverse/{latitude}/{longitude}'),
+        new GetCollection(uriTemplate: '/gps-coordinate-searching')
     ],
-    itemOperations: [
-        'get' => [
-            'method' => 'GET',
-            'path' => '/gps-coordinate-reverse/{latitude}/{longitude}'
-        ]
-    ],
-    compositeIdentifier: false
+    provider: GpsCoordinateSearchingProvider::class
 )]
 class GpsCoordinate implements ApiResourcesInterface
 {
@@ -34,10 +30,15 @@ class GpsCoordinate implements ApiResourcesInterface
     private float $longitude;
 
     private ?string $streetAddress = null;
+
     private ?string $streetAddressSecond = null;
+
     private ?string $streetAddressThird = null;
+
     private ?string $cp = null;
+
     private ?string $city = null;
+
     private ?string $country = null;
 
     public function __construct()
@@ -57,7 +58,6 @@ class GpsCoordinate implements ApiResourcesInterface
         return $this->latitude;
     }
 
-
     public function getLongitude(): ?float
     {
         return $this->longitude;

+ 10 - 13
src/ApiResources/Utils/Siret.php

@@ -1,23 +1,23 @@
 <?php
-declare(strict_types=1);
+declare (strict_types=1);
 
 namespace App\ApiResources\Utils;
 
-use ApiPlatform\Core\Annotation\ApiProperty;
-use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\ApiProperty;
 use App\ApiResources\ApiResourcesInterface;
+use App\State\Provider\Utils\SiretProvider;
 
 /**
  * Classe resource qui contient les champs de vérification d'un siret
  */
-
 #[ApiResource(
-    collectionOperations: [],
-    itemOperations: [
-        'get' => [
-            'method' => 'GET',
-            'path' => '/siret-checking/{number}'
-        ]
+    operations: [
+        new Get(
+            uriTemplate: '/siret-checking/{number}',
+            provider: SiretProvider::class
+        )
     ]
 )]
 class Siret implements ApiResourcesInterface
@@ -27,9 +27,6 @@ class Siret implements ApiResourcesInterface
 
     private bool $isCorrect = false;
 
-    public function __construct()
-    {
-    }
     public function getNumber(): ?string
     {
         return $this->number;

+ 22 - 0
src/Attribute/ActivityYearConstraintAware.php

@@ -0,0 +1,22 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Attribute;
+
+/**
+ * Classe ActivityYearConstraintAware qui gère l'annotation pour le Doctrine filter
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+final class ActivityYearConstraintAware
+{
+    public string $startYearFieldName;
+    public string $endYearFieldName;
+
+    public function __construct(
+         string $startYearFieldName,
+         string $endYearFieldName
+    ){
+        $this->startYearFieldName = $startYearFieldName;
+        $this->endYearFieldName = $endYearFieldName;
+    }
+}

+ 7 - 4
src/Annotation/BillingSettingDefaultValue.php → src/Attribute/BillingSettingDefaultValue.php

@@ -1,15 +1,18 @@
 <?php
 declare(strict_types=1);
 
-namespace App\Annotation;
-
-use Attribute;
+namespace App\Attribute;
 
 /**
  * Classe BillingSettingDefaultValue qui gère l'annotation pour mettre le billing setting de l'organization comme valeur par défaut
  */
-#[Attribute(Attribute::TARGET_CLASS)]
+#[\Attribute(\Attribute::TARGET_CLASS)]
 final class BillingSettingDefaultValue
 {
     public string $fieldName;
+
+    public function __construct(string $fieldName)
+    {
+        $this->fieldName = $fieldName;
+    }
 }

+ 22 - 0
src/Attribute/DateTimeConstraintAware.php

@@ -0,0 +1,22 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Attribute;
+
+/**
+ * Classe DateTimeConstraintAware qui gère l'annotation pour le Doctrine filter
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+final class DateTimeConstraintAware
+{
+    public string $startDateFieldName;
+    public string $endDateFieldName;
+
+    public function __construct(
+        string $startDateFieldName,
+        string $endDateFieldName
+    ){
+        $this->startDateFieldName = $startDateFieldName;
+        $this->endDateFieldName = $endDateFieldName;
+    }
+}

+ 5 - 4
src/Annotation/OrganizationDefaultValue.php → src/Attribute/OrganizationDefaultValue.php

@@ -1,15 +1,16 @@
 <?php
 declare(strict_types=1);
 
-namespace App\Annotation;
-
-use Attribute;
+namespace App\Attribute;
 
 /**
  * Classe OrganizationDefaultValue qui gère l'annotation pour mettre l'organization comme valeur par défaut
  */
-#[Attribute(Attribute::TARGET_CLASS)]
+#[\Attribute(\Attribute::TARGET_CLASS)]
 final class OrganizationDefaultValue
 {
     public string $fieldName;
+    public function __construct(string $fieldName){
+        $this->fieldName = $fieldName;
+    }
 }

+ 53 - 0
src/Commands/AddSubdomainCommand.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Commands;
+
+use App\Repository\Organization\OrganizationRepository;
+use App\Service\Typo3\SubdomainService;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Exception\InvalidArgumentException;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand(
+    name: 'ot:subdomain:add',
+    description: 'Add a subdomain to the given organization and make it the active one'
+)]
+class AddSubdomainCommand extends Command
+{
+    public function __construct(
+        private readonly OrganizationRepository $organizationRepository,
+        private readonly SubdomainService $subdomainService
+    ) {
+        parent::__construct();
+    }
+
+    /**
+     * @return void
+     */
+    protected function configure(): void
+    {
+        $this->addArgument('organization-id', InputArgument::REQUIRED, 'Id of the organization');
+        $this->addArgument('subdomain', InputArgument::REQUIRED, 'The new active subdomain');
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $organizationId = $input->getArgument('organization-id');
+        if (!is_numeric($organizationId)) {
+            throw new InvalidArgumentException('Invalid organization id : ' . $organizationId);
+        }
+        $organization = $this->organizationRepository->find((int)$organizationId);
+
+        $subdomainValue = $input->getArgument('subdomain');
+
+        $output->writeln("Setting up a new subdomain for organization " . $organizationId . " : " . $subdomainValue);
+
+        $this->subdomainService->addNewSubdomain($organization, $subdomainValue, true);
+
+        $output->writeln("New subdomain added and activated");
+        return Command::SUCCESS;
+    }
+}

+ 216 - 0
src/Commands/CronCommand.php

@@ -0,0 +1,216 @@
+<?php
+
+namespace App\Commands;
+
+use App\Service\Cron\CronjobInterface;
+use App\Service\Cron\UI\ConsoleUI;
+use App\Service\ServiceIterator\CronjobIterator;
+use Psr\Log\LoggerInterface;
+use RuntimeException;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Command\LockableTrait;
+use Symfony\Component\Console\Helper\FormatterHelper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Contracts\Service\Attribute\Required;
+
+/**
+ * CLI Command to run the cron-jobs
+ *
+ * Ex:
+ *
+ *     bin/console ot:cron list
+ *     bin/console ot:cron run clean-db --preview
+ *     bin/console ot:cron run clean-db
+ *
+ * @see ~/src/Service/Cron/Readme.md
+ */
+#[AsCommand(
+    name: 'ot:cron',
+    description: 'Executes cron jobs'
+)]
+class CronCommand extends Command
+{
+    use LockableTrait;
+
+    private const ACTION_LIST = 'list';
+    private const ACTION_RUN = 'run';
+    private const ACTION_RUN_ALL = 'all';
+
+    private const ACTIONS = [
+        self::ACTION_LIST => 'List registered jobs',
+        self::ACTION_RUN => 'Run the given job',
+        self::ACTION_RUN_ALL => 'Successively run all the registered cron jobs'
+    ];
+
+    private OutputInterface $output;
+    private LoggerInterface $logger;
+    private CronjobIterator $cronjobIterator;
+
+    #[Required]
+    /** @see https://symfony.com/doc/current/logging/channels_handlers.html#how-to-autowire-logger-channels */
+    public function setLoggerInterface(LoggerInterface $cronLogger): void { $this->logger = $cronLogger; }
+    #[Required]
+    public function setCronjobIterator(CronjobIterator $cronjobIterator): void { $this->cronjobIterator = $cronjobIterator; }
+
+    /**
+     * Configures the command
+     */
+    protected function configure(): void
+    {
+        $this->addArgument(
+            'action',
+            InputArgument::REQUIRED,
+            'Action to execute among : ' .
+            implode(
+                ', ',
+                array_map(
+                    static function ($v, $k) { return "'" . $k . "' (" . $v . ")"; },
+                    self::ACTIONS,
+                    array_keys(self::ACTIONS)
+                )
+            )
+        );
+        $this->addArgument(
+            'jobs',
+            InputArgument::OPTIONAL,
+            "Name(s) of the cron-job(s) to execute with '" . self::ACTION_RUN . "' (comma-separated)"
+        );
+        $this->addOption(
+            'preview',
+            'p',
+            InputOption::VALUE_NONE,
+            'Only preview the operations instead of executing them'
+        );
+    }
+
+    /**
+     * Executes the command
+     *
+     * @param InputInterface $input
+     * @param OutputInterface $output
+     * @return int
+     */
+    final protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $this->output = $output;
+
+        /** @var FormatterHelper $formatter */
+        $formatter = $this->getHelper('formatter');
+
+        $action = $input->getArgument('action');
+        $jobNames = $input->getArgument('jobs');
+        $preview = $input->getOption('preview');
+        $jobs = [];
+
+        if (!array_key_exists($action, self::ACTIONS)) {
+            $this->output->writeln($formatter->formatBlock('Error: unrecognized action', 'error'));
+            return Command::INVALID;
+        }
+
+        if ($action === self::ACTION_LIST) {
+            $this->listJobs();
+            return Command::SUCCESS;
+        }
+
+        if ($action === self::ACTION_RUN_ALL) {
+            $jobs = $this->cronjobIterator->getAll();
+        }
+
+        if ($action === self::ACTION_RUN) {
+            foreach (explode(',', $jobNames) as $name) {
+                try {
+                    $jobs[] = $this->cronjobIterator->getByName($name);
+                } catch (\RuntimeException $e) {
+                    $this->output->writeln($e->getMessage());
+                    $this->listJobs();
+                    return Command::INVALID;
+                }
+            }
+        }
+
+        $this->logger->info(
+            'CronCommand will ' .
+            ($preview ? 'preview' : 'execute') . ' ' .
+            implode(', ', array_map(static function($job) { return $job->name(); }, $jobs))
+        );
+
+        $results = [];
+
+        foreach ($jobs as $job) {
+            $results[] = $this->runJob($job, $preview);
+        }
+
+        return (int) max($results); // If there is one failure result, the whole command is shown as a failure too
+    }
+
+    /**
+     * List all available cron jobs
+     */
+    private function listJobs(): void {
+        $availableJobs = $this->cronjobIterator->getAll();
+
+        if (empty($availableJobs)) {
+            $this->output->writeln('No cronjob found');
+            return;
+        }
+
+        $this->output->writeln('Available cron jobs : ');
+        foreach ($this->cronjobIterator->getAll() as $job) {
+            $this->output->writeln('* ' . $job->name());
+        }
+    }
+
+    /**
+     * Run one Cronjob
+     *
+     * @param CronJobInterface $job
+     * @param bool $preview
+     * @return int
+     */
+    private function runJob(CronjobInterface $job, bool $preview = false): int
+    {
+        /** @var FormatterHelper $formatter */
+        $formatter = $this->getHelper('formatter');
+
+        if (!$this->lock($job->name())) {
+            $msg = 'The command ' . $job->name() . ' is already running in another process. Abort.';
+            $this->output->writeln($formatter->formatBlock($msg, 'error'));
+            $this->logger->error($msg);
+            return Command::FAILURE;
+        }
+
+        $t0 = microtime(true);
+
+        $this->output->writeln(
+            $formatter->formatSection($job->name(),"Start" . ($preview ? ' [PREVIEW MODE]' : ''))
+        );
+
+        // Establish communication between job and the console
+        $ui = new ConsoleUI($this->output);
+        $job->setUI($ui);
+
+        try {
+            if ($preview) {
+                $job->preview();
+            } else {
+                $job->execute();
+            }
+        } catch (RuntimeException $e) {
+            $this->logger->critical($e);
+            $this->output->write("An error happened while running the process : " . $e);
+            return Command::FAILURE;
+        }
+
+        $t1 = microtime(true);
+
+        $msg = "Job has been successfully executed (" . round($t1 - $t0, 2) . " sec.)" . ($preview ? ' [PREVIEW MODE]' : '');
+        $this->output->writeln($formatter->formatSection($job->name(), $msg));
+        $this->logger->info($job->name() . ' - ' . $msg);
+
+        return Command::SUCCESS;
+    }
+}

+ 1 - 1
src/Commands/Doctrine/SchemaUpdateCommand.php

@@ -14,7 +14,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
  */
 class SchemaUpdateCommand extends UpdateSchemaDoctrineCommand
 {
-    protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui)
+    protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): ?int
     {
         $output->writeln('-- Executing pre-update scripts');
 

+ 1 - 1
src/Commands/DolibarrSyncCommand.php

@@ -30,7 +30,7 @@ class DolibarrSyncCommand extends Command
         parent::__construct();
     }
 
-    protected function configure()
+    protected function configure(): void
     {
         $this->addOption(
             'preview',

+ 3 - 3
src/Commands/PostUpgrade/V0_2/PostUpgradeCommand.php

@@ -39,7 +39,7 @@ class PostUpgradeCommand extends Command
         return Command::SUCCESS;
     }
 
-    private function getOpentalentConnexion() {
+    private function getOpentalentConnexion(): PDO {
         $dbUrl = $_ENV['DATABASE_URL'];
         $matches = [];
         preg_match(
@@ -154,7 +154,7 @@ class PostUpgradeCommand extends Command
         }
     }
 
-    private function genEventsUuid() {
+    private function genEventsUuid(): void {
         $opentalentCnn = $this->getOpentalentConnexion();
 
         $opentalentCnn->beginTransaction();
@@ -178,7 +178,7 @@ class PostUpgradeCommand extends Command
         }
     }
 
-    private function updateFilesStatuses() {
+    private function updateFilesStatuses():void  {
         $opentalentCnn = $this->getOpentalentConnexion();
 
         $opentalentCnn->beginTransaction();

+ 0 - 26
src/Controller/AuditController.php

@@ -1,26 +0,0 @@
-<?php
-
-namespace App\Controller;
-
-use App\Entity\Organization\Parameters;
-use DH\Auditor\Provider\Doctrine\DoctrineProvider;
-use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\SimpleFilter;
-use DH\Auditor\Provider\Doctrine\Persistence\Reader\Reader;
-use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\Routing\Annotation\Route;
-
-class AuditController extends AbstractController
-{
-    #[Route('/audit_test', name: 'audit_test')]
-    public function index(DoctrineProvider $doctrineProvider): Response
-    {
-        $reader = new Reader($doctrineProvider);
-        $query = $reader
-            ->createQuery(Parameters::class)
-            ->addFilter(new SimpleFilter('object_id', 5755));
-
-        dd($query->execute());
-        return new Response('ok');
-    }
-}

+ 26 - 0
src/Controller/AuditController.php.ori

@@ -0,0 +1,26 @@
+<?php
+//
+//namespace App\Controller;
+//
+//use App\Entity\Organization\Parameters;
+//use DH\Auditor\Provider\Doctrine\DoctrineProvider;
+//use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\SimpleFilter;
+//use DH\Auditor\Provider\Doctrine\Persistence\Reader\Reader;
+//use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+//use Symfony\Component\HttpFoundation\Response;
+//use Symfony\Component\Routing\Annotation\Route;
+//
+//class AuditController extends AbstractController
+//{
+//    #[Route('/audit_test', name: 'audit_test')]
+//    public function index(DoctrineProvider $doctrineProvider): Response
+//    {
+//        $reader = new Reader($doctrineProvider);
+//        $query = $reader
+//            ->createQuery(Parameters::class)
+//            ->addFilter(new SimpleFilter('object_id', 5755));
+//
+//        dd($query->execute());
+//        return new Response('ok');
+//    }
+//}

+ 17 - 0
src/DataFixtures/AppFixtures.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\DataFixtures;
+
+use Doctrine\Bundle\FixturesBundle\Fixture;
+use Doctrine\Persistence\ObjectManager;
+
+class AppFixtures extends Fixture
+{
+    public function load(ObjectManager $manager): void
+    {
+        // $product = new Product();
+        // $manager->persist($product);
+
+        $manager->flush();
+    }
+}

Some files were not shown because too many files changed in this diff