diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..dc5b4ae --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,25 @@ +#### Link to ticket + +Please add a link to the ticket being addressed by this change. + +#### Description + +Please include a short description of the suggested change and the reasoning behind the approach you have chosen. + +#### Screenshot of the result + +If your change affects the user interface you should include a screenshot of the result with the pull request. + +#### Checklist + +- [ ] My code is covered by test cases. +- [ ] My code passes our test (all our tests). +- [ ] My code passes our static analysis suite. +- [ ] My code passes our continuous integration process. + +If your code does not pass all the requirements on the checklist you have to add a comment explaining why this change +should be exempt from the list. + +#### Additional comments or questions + +If you have any further comments or questions for the reviewer please add them here. diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 0c6303a..d325fbe 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -176,3 +176,14 @@ jobs: sed -i '/^\s*"require":/,/^\s*}/d' composer.json composer --no-interaction install composer code-analysis + + coding-standards-markdown: + name: Markdown coding standards + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@master + + - name: Coding standards + run: | + docker run --rm --volume $PWD:/md peterdavehello/markdownlint markdownlint --ignore vendor --ignore LICENSE.md '**/*.md' diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000..a28c580 --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,13 @@ +{ + "default": true, + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md + "line-length": { + "line_length": 120, + "code_blocks": false, + "tables": false + }, + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md024.md + "no-duplicate-heading": { + "siblings_only": true + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 18800f3..5d1ec26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ about writing changes to this log. ## [Unreleased] +* [PR-14](https://github.com/OS2Forms/os2forms_get_organized/pull/14) + Added support for [Key](https://www.drupal.org/project/key) * [PR-22](https://github.com/OS2Forms/os2forms_get_organized/pull/22) Normalized `composer.json` diff --git a/README.md b/README.md index e07615b..d696005 100644 --- a/README.md +++ b/README.md @@ -11,24 +11,39 @@ vendor/bin/drush pm:enable os2forms_get_organized ## Settings -Set GetOrganized `username`, `password` and `base url` -on `/admin/os2forms_get_organized/settings`. - -You can also test that the provided -details work on `/admin/os2forms_get_organized/settings`. +Go to `/admin/os2forms_get_organized/settings` and configure the module. ## Coding standards -Check coding standards: +Our coding are checked by GitHub Actions (cf. [.github/workflows/pr.yml](.github/workflows/pr.yml)). Use the commands +below to run the checks locally. -```sh -docker run --rm --interactive --tty --volume ${PWD}:/app itkdev/php8.1-fpm:latest composer install -docker run --rm --interactive --tty --volume ${PWD}:/app itkdev/php8.1-fpm:latest composer coding-standards-check +### PHP + +```shell +# Update to make sure that we use the latest versions of tools. +docker run --rm --volume ${PWD}:/app --workdir /app itkdev/php8.3-fpm composer update +# Fix (some) coding standards issues +docker run --rm --volume ${PWD}:/app --workdir /app itkdev/php8.3-fpm composer coding-standards-apply +# Check that code adheres to the coding standards +docker run --rm --volume ${PWD}:/app --workdir /app itkdev/php8.3-fpm composer coding-standards-check +``` + +### Markdown + +```shell +docker pull peterdavehello/markdownlint +docker run --rm --volume $PWD:/md peterdavehello/markdownlint markdownlint --ignore vendor --ignore LICENSE.md '**/*.md' --fix +docker run --rm --volume $PWD:/md peterdavehello/markdownlint markdownlint --ignore vendor --ignore LICENSE.md '**/*.md' ``` -Apply coding standards: +## Code analysis + +We use [PHPStan](https://phpstan.org/) for static code analysis. + +Running statis code analysis on a standalone Drupal module is a bit tricky, so we use a helper script to run the +analysis: ```shell -docker run --rm --interactive --tty --volume ${PWD}:/app itkdev/php8.1-fpm:latest composer coding-standards-apply -docker run --rm --interactive --tty --volume ${PWD}:/app node:18 yarn --cwd /app coding-standards-apply +docker run --rm --volume ${PWD}:/app --workdir /app itkdev/php8.3-fpm ./scripts/code-analysis ``` diff --git a/composer.json b/composer.json index 2b6832c..47d67b8 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ ], "require": { "drupal/advancedqueue": "^1.2", + "drupal/key": "^1.20", "drupal/webform": "^6.2", "itk-dev/getorganized-api-client-php": "^1.2.2", "os2forms/os2forms": "^3.17 || ^4.0", diff --git a/drush.services.yml b/drush.services.yml index 9114ab0..da524b0 100644 --- a/drush.services.yml +++ b/drush.services.yml @@ -1,7 +1,12 @@ services: - drush_command_example.commands: - class: \Drupal\os2forms_get_organized\Commands\ArchiveCommands + Drupal\os2forms_get_organized\Commands\ArchiveCommands: + arguments: + - '@Drupal\os2forms_get_organized\Helper\ArchiveHelper' tags: - { name: drush.command } + + Drupal\os2forms_get_organized\Drush\Commands\GetOrganizedTestCommands: arguments: - '@Drupal\os2forms_get_organized\Helper\ArchiveHelper' + tags: + - { name: drush.command } diff --git a/os2forms_get_organized.info.yml b/os2forms_get_organized.info.yml index ff78562..afdae81 100644 --- a/os2forms_get_organized.info.yml +++ b/os2forms_get_organized.info.yml @@ -4,9 +4,11 @@ description: 'GetOrganized integration' package: OS2Forms core_version_requirement: ^9 || ^10 dependencies: - - drupal:webform - drupal:advancedqueue + - drupal:key + - drupal:webform - drupal:webform_entity_print_attachment - os2forms:os2forms_attachment - os2web:os2web_audit + configure: os2forms_get_organized.admin.settings diff --git a/os2forms_get_organized.install b/os2forms_get_organized.install new file mode 100644 index 0000000..4f09293 --- /dev/null +++ b/os2forms_get_organized.install @@ -0,0 +1,15 @@ +install([ + 'key', + ], TRUE); +} diff --git a/os2forms_get_organized.services.yml b/os2forms_get_organized.services.yml index 6ef030e..37d813c 100644 --- a/os2forms_get_organized.services.yml +++ b/os2forms_get_organized.services.yml @@ -1,7 +1,8 @@ services: Drupal\os2forms_get_organized\Helper\Settings: arguments: - - "@keyvalue" + - "@config.factory" + - "@key.repository" Drupal\os2forms_get_organized\Helper\ArchiveHelper: arguments: diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..8b1d88e --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,11 @@ +parameters: + level: 6 + paths: + - src + + ignoreErrors: + - '#Unsafe usage of new static\(\).#' + +# Local Variables: +# mode: yaml +# End: diff --git a/scripts/code-analysis b/scripts/code-analysis new file mode 100755 index 0000000..2ef69d0 --- /dev/null +++ b/scripts/code-analysis @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +script_dir=$(pwd) +module_name=$(basename "$script_dir") +drupal_dir=vendor/drupal-module-code-analysis +# Relative to $drupal_dir +module_path=web/modules/contrib/$module_name + +cd "$script_dir" || exit + +drupal_composer() { + composer --working-dir="$drupal_dir" --no-interaction "$@" +} + +# Create new Drupal 9 project +if [ ! -f "$drupal_dir/composer.json" ]; then + composer --no-interaction create-project drupal/recommended-project:^9 "$drupal_dir" +fi +# Copy our code into the modules folder + +# Clean up +rm -fr "${drupal_dir:?}/$module_path" + +# https://stackoverflow.com/a/15373763 +# rsync --archive --compress . --filter=':- .gitignore' --exclude "$drupal_dir" --exclude .git "$drupal_dir/$module_path" + +# The rsync command in not available in itkdev/php8.1-fpm + +git config --global --add safe.directory /app +# Copy all module files not ignored by git into module path +# (cf. https://stackoverflow.com/a/77197460) +for f in $(git ls-files --cached --others --exclude-standard); do + mkdir -p "$drupal_dir/$module_path/$(dirname "$f")" + cp "$f" "$drupal_dir/$module_path/$f" +done + +drupal_composer config minimum-stability dev + +# Allow ALL plugins +# https://getcomposer.org/doc/06-config.md#allow-plugins +drupal_composer config --no-plugins allow-plugins true + +drupal_composer require wikimedia/composer-merge-plugin +drupal_composer config extra.merge-plugin.include "$module_path/composer.json" +# https://www.drupal.org/project/drupal/issues/3220043#comment-14845434 +drupal_composer require --dev symfony/phpunit-bridge + +# Run PHPStan +(cd "$drupal_dir/$module_path" && ../../../../vendor/bin/phpstan) diff --git a/src/Drush/Commands/GetOrganizedTestCommands.php b/src/Drush/Commands/GetOrganizedTestCommands.php new file mode 100644 index 0000000..36ad0ff --- /dev/null +++ b/src/Drush/Commands/GetOrganizedTestCommands.php @@ -0,0 +1,38 @@ +helper->pingApi(); + $this->io()->success('Successfully connected to Get Organized API'); + } + catch (\Throwable $t) { + $this->io()->error($t->getMessage()); + } + + } + +} diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php index ad6ab88..cfd79ca 100644 --- a/src/Form/SettingsForm.php +++ b/src/Form/SettingsForm.php @@ -2,47 +2,59 @@ namespace Drupal\os2forms_get_organized\Form; -use Drupal\Core\Form\FormBase; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\os2forms_get_organized\Helper\Settings; -use ItkDev\GetOrganized\Client; +use Drupal\os2forms_get_organized\Helper\ArchiveHelper; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\OptionsResolver\Exception\ExceptionInterface as OptionsResolverException; /** * GetOrganized settings form. */ -class SettingsForm extends FormBase { +class SettingsForm extends ConfigFormBase { use StringTranslationTrait; - public const GET_ORGANIZED_USERNAME = 'get_organized_username'; - public const GET_ORGANIZED_PASSWORD = 'get_organized_password'; + public const CONFIG_NAME = 'os2forms_get_organized.settings'; + public const GET_ORGANIZED_BASE_URL = 'get_organized_base_url'; + public const KEY = 'key'; - /** - * The settings. - * - * @var \Drupal\os2forms_get_organized\Helper\Settings - */ - private Settings $settings; + public const ACTION_PING_API = 'action_ping_api'; /** - * Constructor. + * {@inheritdoc} */ - public function __construct(Settings $settings) { - $this->settings = $settings; + public function __construct( + ConfigFactoryInterface $config_factory, + private readonly ArchiveHelper $helper, + ) { + parent::__construct($config_factory); } /** * {@inheritdoc} + * + * @phpstan-return self */ - public static function create(ContainerInterface $container): SettingsForm { + public static function create(ContainerInterface $container): self { return new static( - $container->get(Settings::class), + $container->get('config.factory'), + $container->get(ArchiveHelper::class) ); } + /** + * {@inheritdoc} + * + * @phpstan-return array + */ + protected function getEditableConfigNames() { + return [ + self::CONFIG_NAME, + ]; + } + /** * {@inheritdoc} */ @@ -57,40 +69,39 @@ public function getFormId() { * @phpstan-return array */ public function buildForm(array $form, FormStateInterface $form_state): array { - - $form[self::GET_ORGANIZED_USERNAME] = [ - '#type' => 'textfield', - '#title' => $this->t('Username'), - '#required' => TRUE, - '#default_value' => $this->settings->getUsername(), - ]; - - $form[self::GET_ORGANIZED_PASSWORD] = [ - '#type' => 'textfield', - '#title' => $this->t('Password'), + $form = parent::buildForm($form, $form_state); + $config = $this->config(self::CONFIG_NAME); + + $form[self::KEY] = [ + '#type' => 'key_select', + '#key_filters' => [ + 'type' => 'user_password', + ], + '#title' => $this->t('Key'), '#required' => TRUE, - '#default_value' => $this->settings->getPassword(), + '#default_value' => $config->get(self::KEY), ]; $form[self::GET_ORGANIZED_BASE_URL] = [ '#type' => 'textfield', '#title' => $this->t('GetOrganized base url'), '#required' => TRUE, - '#default_value' => $this->settings->getBaseUrl(), + '#default_value' => $config->get(self::GET_ORGANIZED_BASE_URL), '#description' => $this->t('GetOrganized base url. Example: "https://ad.go.aarhuskommune.dk/_goapi"'), ]; - $form['actions']['#type'] = 'actions'; + $form['actions']['ping_api'] = [ + '#type' => 'container', - $form['actions']['submit'] = [ - '#type' => 'submit', - '#value' => $this->t('Save settings'), - ]; + self::ACTION_PING_API => [ + '#type' => 'submit', + '#name' => self::ACTION_PING_API, + '#value' => $this->t('Ping API'), + ], - $form['actions']['testSettings'] = [ - '#type' => 'submit', - '#name' => 'testSettings', - '#value' => $this->t('Test provided information'), + 'message' => [ + '#markup' => $this->t('Note: Pinging the API will use saved config.'), + ], ]; return $form; @@ -101,55 +112,41 @@ public function buildForm(array $form, FormStateInterface $form_state): array { * * @phpstan-param array $form */ - public function submitForm(array &$form, FormStateInterface $formState): void { - $username = $formState->getValue(self::GET_ORGANIZED_USERNAME); - $password = $formState->getValue(self::GET_ORGANIZED_PASSWORD); - $baseUrl = $formState->getValue(self::GET_ORGANIZED_BASE_URL); - - $triggeringElement = $formState->getTriggeringElement(); - if ('testSettings' === ($triggeringElement['#name'] ?? NULL)) { - $this->testSettings($username, $password, $baseUrl); + public function validateForm(array &$form, FormStateInterface $form_state): void { + if (self::ACTION_PING_API === ($form_state->getTriggeringElement()['#name'] ?? NULL)) { return; } - try { - $settings[self::GET_ORGANIZED_USERNAME] = $username; - $settings[self::GET_ORGANIZED_PASSWORD] = $password; - $settings[self::GET_ORGANIZED_BASE_URL] = $baseUrl; - - $this->settings->setSettings($settings); - $this->messenger()->addStatus($this->t('Settings saved')); - } - catch (OptionsResolverException $exception) { - $this->messenger()->addError($this->t('Settings not saved (@message)', ['@message' => $exception->getMessage()])); - } - - $this->messenger()->addStatus($this->t('Settings saved')); + parent::validateForm($form, $form_state); } /** - * Test settings by making some arbitrary call to the GetOrganized API. + * {@inheritdoc} + * + * @phpstan-param array $form */ - private function testSettings(string $username, string $password, string $baseUrl): void { - try { - $client = new Client($username, $password, $baseUrl); - /** @var \ItkDev\GetOrganized\Service\Tiles $tileService */ - $tileService = $client->api('tiles'); - - $result = $tileService->GetTilesNavigation(); - - if (empty($result)) { - $message = $this->t('Error occurred while testing the GetOrganized API with provided settings.'); - $this->messenger()->addError($message); + public function submitForm(array &$form, FormStateInterface $form_state): void { + if (self::ACTION_PING_API === ($form_state->getTriggeringElement()['#name'] ?? NULL)) { + try { + $this->helper->pingApi(); + $this->messenger()->addStatus($this->t('Pinged API successfully.')); } - else { - $this->messenger()->addStatus($this->t('Settings succesfully tested')); + catch (\Throwable $t) { + $this->messenger()->addError($this->t('Pinging API failed: @message', ['@message' => $t->getMessage()])); } + return; } - catch (\Throwable $throwable) { - $message = $this->t('Error testing provided information: %message', ['%message' => $throwable->getMessage()]); - $this->messenger()->addError($message); + + $config = $this->config(self::CONFIG_NAME); + foreach ([ + self::KEY, + self::GET_ORGANIZED_BASE_URL, + ] as $key) { + $config->set($key, $form_state->getValue($key)); } + $config->save(); + + parent::submitForm($form, $form_state); } } diff --git a/src/Helper/ArchiveHelper.php b/src/Helper/ArchiveHelper.php index 693cbe8..42a0fe3 100644 --- a/src/Helper/ArchiveHelper.php +++ b/src/Helper/ArchiveHelper.php @@ -13,9 +13,13 @@ use Drupal\webform\Entity\WebformSubmission; use Drupal\webform_attachment\Element\WebformAttachmentBase; use ItkDev\GetOrganized\Client; +use ItkDev\GetOrganized\ClientInterface; use ItkDev\GetOrganized\Service\Cases; use ItkDev\GetOrganized\Service\Documents; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpClient\Exception\ClientException; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; /** * Helper for archiving documents in GetOrganized. @@ -73,20 +77,15 @@ public function __construct( * @phpstan-param array $handlerConfiguration */ public function archive(string $submissionId, array $handlerConfiguration): void { - // Setup Client and services. - if (NULL === $this->client) { - $this->setupClient(); - } - if (NULL === $this->caseService) { /** @var \ItkDev\GetOrganized\Service\Cases $caseService */ - $caseService = $this->client->api('cases'); + $caseService = $this->client()->api('cases'); $this->caseService = $caseService; } if (NULL === $this->documentService) { /** @var \ItkDev\GetOrganized\Service\Documents $docService */ - $docService = $this->client->api('documents'); + $docService = $this->client()->api('documents'); $this->documentService = $docService; } @@ -99,18 +98,44 @@ public function archive(string $submissionId, array $handlerConfiguration): void elseif ('archive_to_citizen' === $archivingMethod) { $this->archiveToCitizen($submissionId, $handlerConfiguration); } + } + /** + * Ping the Get Organized API. + * + * Interpret a 400 Bad Request response as a success. + * + * @throws \Throwable + */ + public function pingApi(): void { + try { + /** @var \ItkDev\GetOrganized\Service\Documents $service */ + $service = $this->client()->api('documents'); + $request = new \ReflectionMethod($service, 'request'); + $request->invoke($service, Request::METHOD_GET, '/'); + } + catch (\Throwable $t) { + // Throw if it's not a 400 Bad Request exception. + if (!($t instanceof ClientException) + || Response::HTTP_BAD_REQUEST !== $t->getCode()) { + throw $t; + } + } } /** * Sets up Client. */ - private function setupClient(): void { - $username = $this->settings->getUsername(); - $password = $this->settings->getPassword(); - $baseUrl = $this->settings->getBaseUrl(); + private function client(): ClientInterface { + if (NULL === $this->client) { + $username = $this->settings->getUsername(); + $password = $this->settings->getPassword(); + $baseUrl = $this->settings->getBaseUrl(); - $this->client = new Client($username, $password, $baseUrl); + $this->client = new Client($username, $password, $baseUrl); + } + + return $this->client; } /** @@ -159,10 +184,6 @@ private function archiveToCitizen(string $submissionId, array $handlerConfigurat // Step 1: Find/create parent case // Step 2: Find/create subcase // Step 3: Upload to subcase. - if (NULL === $this->client) { - $this->setupClient(); - } - /** @var \Drupal\webform\Entity\WebformSubmission $submission */ $submission = $this->getSubmission($submissionId); diff --git a/src/Helper/Settings.php b/src/Helper/Settings.php index 579b287..37e6d49 100644 --- a/src/Helper/Settings.php +++ b/src/Helper/Settings.php @@ -2,80 +2,103 @@ namespace Drupal\os2forms_get_organized\Helper; -use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; -use Drupal\Core\KeyValueStore\KeyValueStoreInterface; -use Drupal\os2forms_get_organized\Exception\InvalidSettingException; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Config\ImmutableConfig; +use Drupal\key\KeyRepositoryInterface; use Drupal\os2forms_get_organized\Form\SettingsForm; -use Symfony\Component\OptionsResolver\OptionsResolver; /** * General settings for os2forms_get_organized. */ class Settings { /** - * The store. + * The config. * - * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface + * @var \Drupal\Core\Config\ImmutableConfig */ - private KeyValueStoreInterface $store; + private ImmutableConfig $config; /** - * The key value collection name. - * - * @var string + * The constructor. */ - private $collection = 'os2forms_get_organized'; + public function __construct( + ConfigFactoryInterface $configFactory, + private readonly KeyRepositoryInterface $keyRepository, + ) { + $this->config = $configFactory->get(SettingsForm::CONFIG_NAME); + } /** - * Default setting values. - * - * @var array - * @phpstan-var array + * Get key. */ - private $defaultSettings = [ - SettingsForm::GET_ORGANIZED_USERNAME => '', - SettingsForm::GET_ORGANIZED_PASSWORD => '', - SettingsForm::GET_ORGANIZED_BASE_URL => '', - ]; + public function getKey(): ?string { + return $this->get(SettingsForm::KEY); + } /** - * Constructor. + * Get password. + * + * @return string + * The sources. */ - public function __construct(KeyValueFactoryInterface $keyValueFactory) { - $this->store = $keyValueFactory->get($this->collection); + public function getBaseUrl(): string { + $value = $this->get(SettingsForm::GET_ORGANIZED_BASE_URL); + return is_string($value) ? $value : ''; } /** * Get username. * * @return string - * The sources. + * The username. */ public function getUsername(): string { - $value = $this->get(SettingsForm::GET_ORGANIZED_USERNAME); - return is_string($value) ? $value : ''; + return $this->getKeyValue('username'); } /** * Get password. * * @return string - * The sources. + * The password. */ public function getPassword(): string { - $value = $this->get(SettingsForm::GET_ORGANIZED_PASSWORD); - return is_string($value) ? $value : ''; + return $this->getKeyValue('password'); } /** - * Get password. + * Get key value. * - * @return string - * The sources. + * @param string $name + * The value name. + * + * @return null|string + * The value if any. */ - public function getBaseUrl(): string { - $value = $this->get(SettingsForm::GET_ORGANIZED_BASE_URL); - return is_string($value) ? $value : ''; + private function getKeyValue(string $name): ?string { + $key = $this->keyRepository->getKey( + $this->getKey() + ); + + try { + $values = json_decode($key?->getKeyValue() ?? '{}', TRUE, 512, JSON_THROW_ON_ERROR); + + return $values[$name] ?? NULL; + } + catch (\Throwable $exception) { + return NULL; + } + } + + /** + * Get certificate. + */ + public function getCertificate(): ?string { + $key = $this->keyRepository->getKey( + $this->getKey(), + ); + + return $key?->getKeyValue(); } /** @@ -90,44 +113,7 @@ public function getBaseUrl(): string { * The setting value. */ private function get(string $key, mixed $default = NULL): mixed { - $resolver = $this->getSettingsResolver(); - if (!$resolver->isDefined($key)) { - throw new InvalidSettingException(sprintf('Setting %s is not defined', $key)); - } - - return $this->store->get($key, $default); - } - - /** - * Set settings. - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @phpstan-param array $settings - */ - public function setSettings(array $settings): self { - $settings = $this->getSettingsResolver()->resolve($settings); - foreach ($settings as $key => $value) { - $this->store->set($key, $value); - } - - return $this; - } - - /** - * Get settings resolver. - */ - private function getSettingsResolver(): OptionsResolver { - return (new OptionsResolver()) - ->setDefaults($this->defaultSettings) - ->setAllowedTypes(SettingsForm::GET_ORGANIZED_USERNAME, 'string') - ->setAllowedTypes(SettingsForm::GET_ORGANIZED_PASSWORD, 'string') - ->setAllowedTypes(SettingsForm::GET_ORGANIZED_BASE_URL, 'string') - ->setRequired([ - SettingsForm::GET_ORGANIZED_USERNAME, - SettingsForm::GET_ORGANIZED_PASSWORD, - SettingsForm::GET_ORGANIZED_BASE_URL, - ]); + return $this->config->get($key) ?? $default; } }