From c9028a4e318350a636f9dd785a24605c4868ff65 Mon Sep 17 00:00:00 2001 From: Divyajose <75604843+divya-intelli@users.noreply.github.com> Date: Wed, 16 Nov 2022 20:51:45 +0530 Subject: [PATCH 1/6] Team member sync (#745) * Team member sync --- .../apigee_edge_teams.links.task.yml | 5 + .../apigee_edge_teams.routing.yml | 24 ++ .../apigee_edge_teams.services.yml | 4 + modules/apigee_edge_teams/composer.json | 9 +- modules/apigee_edge_teams/drush.services.yml | 6 + modules/apigee_edge_teams/src/CliService.php | 73 ++++++ .../src/CliServiceInterface.php | 39 ++++ .../src/Commands/ApigeeEdgeCommands.php | 61 +++++ .../Controller/TeamMemberSyncController.php | 209 ++++++++++++++++++ .../src/Entity/TeamAccessHandler.php | 2 +- .../src/Form/TeamMemberSyncForm.php | 160 ++++++++++++++ .../src/Job/TeamMemberCreateUpdate.php | 56 +++++ .../src/Job/TeamMemberSync.php | 79 +++++++ .../src/Job/TeamMemberUpdate.php | 36 +++ .../src/TeamPermissionHandler.php | 2 +- 15 files changed, 762 insertions(+), 3 deletions(-) create mode 100644 modules/apigee_edge_teams/drush.services.yml create mode 100644 modules/apigee_edge_teams/src/CliService.php create mode 100644 modules/apigee_edge_teams/src/CliServiceInterface.php create mode 100644 modules/apigee_edge_teams/src/Commands/ApigeeEdgeCommands.php create mode 100644 modules/apigee_edge_teams/src/Controller/TeamMemberSyncController.php create mode 100644 modules/apigee_edge_teams/src/Form/TeamMemberSyncForm.php create mode 100644 modules/apigee_edge_teams/src/Job/TeamMemberCreateUpdate.php create mode 100644 modules/apigee_edge_teams/src/Job/TeamMemberSync.php create mode 100644 modules/apigee_edge_teams/src/Job/TeamMemberUpdate.php diff --git a/modules/apigee_edge_teams/apigee_edge_teams.links.task.yml b/modules/apigee_edge_teams/apigee_edge_teams.links.task.yml index f06a1608..72c8815a 100644 --- a/modules/apigee_edge_teams/apigee_edge_teams.links.task.yml +++ b/modules/apigee_edge_teams/apigee_edge_teams.links.task.yml @@ -106,3 +106,8 @@ apigee_edge_teams.team_app.analytics: title: 'Analytics' base_route: entity.team_app.canonical weight: -1 + +apigee_edge_teams.settings.team_member.sync: + route_name: apigee_edge_teams.settings.team_member.sync + title: 'Sync' + base_route: apigee_edge_teams.settings.team diff --git a/modules/apigee_edge_teams/apigee_edge_teams.routing.yml b/modules/apigee_edge_teams/apigee_edge_teams.routing.yml index 26ac0555..ecfa0df3 100644 --- a/modules/apigee_edge_teams/apigee_edge_teams.routing.yml +++ b/modules/apigee_edge_teams/apigee_edge_teams.routing.yml @@ -71,3 +71,27 @@ apigee_edge_teams.settings.team_app.cache: _title: 'Caching' requirements: _permission: 'administer team' + +apigee_edge_teams.settings.team_member.sync: + path: '/admin/config/apigee-edge/app-settings/team-settings/sync' + defaults: + _form: '\Drupal\apigee_edge_teams\Form\TeamMemberSyncForm' + _title: 'Team Member Synchronization' + requirements: + _permission: 'administer team' + +apigee_edge_teams.team_member.run: + path: '/admin/config/apigee-edge/app-settings/team-settings/sync/run' + defaults: + _controller: '\Drupal\apigee_edge_teams\Controller\TeamMemberSyncController::run' + requirements: + _permission: 'administer team' + _csrf_token: 'TRUE' + +apigee_edge_teams.team_member.schedule: + path: '/admin/config/apigee-edge/app-settings/team-settings/sync/schedule' + defaults: + _controller: '\Drupal\apigee_edge_teams\Controller\TeamMemberSyncController::schedule' + requirements: + _permission: 'administer team' + _csrf_token: 'TRUE' diff --git a/modules/apigee_edge_teams/apigee_edge_teams.services.yml b/modules/apigee_edge_teams/apigee_edge_teams.services.yml index 6b304a35..5246435e 100644 --- a/modules/apigee_edge_teams/apigee_edge_teams.services.yml +++ b/modules/apigee_edge_teams/apigee_edge_teams.services.yml @@ -130,3 +130,7 @@ services: arguments: ['@entity_type.manager', '@logger.channel.apigee_edge_teams'] tags: - { name: paramconverter } + + apigee_edge_teams.cli: + class: Drupal\apigee_edge_teams\CliService + arguments: ['@apigee_edge.apigee_edge_mgmt_cli_service'] diff --git a/modules/apigee_edge_teams/composer.json b/modules/apigee_edge_teams/composer.json index 50d6e9db..ca2e104f 100644 --- a/modules/apigee_edge_teams/composer.json +++ b/modules/apigee_edge_teams/composer.json @@ -11,5 +11,12 @@ "sort-packages": true }, "minimum-stability": "dev", - "prefer-stable": true + "prefer-stable": true, + "extra": { + "drush": { + "services": { + "drush.services.yml": "^9" + } + } + } } diff --git a/modules/apigee_edge_teams/drush.services.yml b/modules/apigee_edge_teams/drush.services.yml new file mode 100644 index 00000000..60f1a3f7 --- /dev/null +++ b/modules/apigee_edge_teams/drush.services.yml @@ -0,0 +1,6 @@ +services: + apigee_edge_teams.commands: + class: \Drupal\apigee_edge_teams\Commands\ApigeeEdgeCommands + arguments: ['@apigee_edge_teams.cli', '@apigee_edge.apigee_edge_mgmt_cli_service'] + tags: + - { name: drush.command } diff --git a/modules/apigee_edge_teams/src/CliService.php b/modules/apigee_edge_teams/src/CliService.php new file mode 100644 index 00000000..3f5a8d5c --- /dev/null +++ b/modules/apigee_edge_teams/src/CliService.php @@ -0,0 +1,73 @@ +apigeeEdgeManagementCliService = $apigeeEdgeManagementCliService; + } + + /** + * {@inheritdoc} + */ + public function sync(StyleInterface $io, callable $t) { + $io->title($t('Team Member synchronization')); + $batch = TeamMemberSyncController::getBatch(); + $last_message = ''; + + foreach ($batch['operations'] as $operation) { + $context = [ + 'finished' => 0, + ]; + + while ($context['finished'] < 1) { + call_user_func_array($operation[0], array_merge($operation[1], [&$context])); + if (isset($context['message']) && $context['message'] !== $last_message) { + $io->text($t($context['message'])); + } + $last_message = $context['message']; + + gc_collect_cycles(); + } + } + } + +} diff --git a/modules/apigee_edge_teams/src/CliServiceInterface.php b/modules/apigee_edge_teams/src/CliServiceInterface.php new file mode 100644 index 00000000..9f4c4d0c --- /dev/null +++ b/modules/apigee_edge_teams/src/CliServiceInterface.php @@ -0,0 +1,39 @@ +cliService = $cli_service; + } + + /** + * Team Member synchronization. + * + * @command apigee-edge-teams:sync + * + * @usage drush apigee-edge-teams:sync + * Starts the team member synchronization. + */ + public function sync() { + $this->cliService->sync($this->io(), 'dt'); + } + +} diff --git a/modules/apigee_edge_teams/src/Controller/TeamMemberSyncController.php b/modules/apigee_edge_teams/src/Controller/TeamMemberSyncController.php new file mode 100644 index 00000000..948d8222 --- /dev/null +++ b/modules/apigee_edge_teams/src/Controller/TeamMemberSyncController.php @@ -0,0 +1,209 @@ +executor = $executor; + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('apigee_edge.job_executor'), + $container->get('messenger') + ); + } + + /** + * Generates a job tag. + * + * @param string $type + * Tag type. + * + * @return string + * Job tag. + */ + protected static function generateTag(string $type): string { + return "team_member_sync_{$type}_" . \Drupal::service('password_generator')->generate(); + } + + /** + * Returns the team member sync filter. + * + * @return null|string + * Filter condition or null if not set. + */ + protected static function getFilter(): ?string { + return ((string) \Drupal::config('apigee_edge.sync')->get('filter')) ?: NULL; + } + + /** + * Handler for 'apigee_edge_teams.team_member.schedule'. + * + * Runs a team member sync in the background. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The HTTP request. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * HTTP response doing a redirect. + */ + public function schedule(Request $request): RedirectResponse { + $destination = $request->query->get('destination'); + + $job = new TeamMemberSync(static::getFilter()); + $job->setTag($this->generateTag('background')); + apigee_edge_get_executor()->cast($job); + + $this->messenger()->addStatus($this->t('Team Member synchronization is scheduled.')); + + return new RedirectResponse($destination); + } + + /** + * Handler for 'apigee_edge_teams.team_member.run'. + * + * Starts the team member sync batch process. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The HTTP request. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * HTTP response doing a redirect. + */ + public function run(Request $request): RedirectResponse { + $destination = $request->query->get('destination'); + $batch = static::getBatch(); + batch_set($batch); + return batch_process($destination); + } + + /** + * Gets the batch array. + * + * @return array + * The batch array. + */ + public static function getBatch(): array { + $tag = static::generateTag('batch'); + + return [ + 'title' => t('Synchronizing Team Member'), + 'operations' => [ + [[static::class, 'batchGenerateJobs'], [$tag]], + [[static::class, 'batchExecuteJobs'], [$tag]], + ], + 'finished' => [static::class, 'batchFinished'], + ]; + } + + /** + * The first batch operation. + * + * This generates the team member sync jobs for the second operation. + * + * @param string $tag + * Job tag. + * @param array $context + * Batch context. + */ + public static function batchGenerateJobs(string $tag, array &$context) { + $job = new TeamMemberSync(static::getFilter()); + $job->setTag($tag); + apigee_edge_get_executor()->call($job); + + $context['message'] = (string) $job; + $context['finished'] = 1.0; + } + + /** + * The second batch operation. + * + * @param string $tag + * Job tag. + * @param array $context + * Batch context. + */ + public static function batchExecuteJobs(string $tag, array &$context) { + if (!isset($context['sandbox'])) { + $context['sandbox'] = []; + } + + $executor = apigee_edge_get_executor(); + $job = $executor->select($tag); + + if ($job === NULL) { + $context['finished'] = 1.0; + return; + } + + $executor->call($job); + + $context['message'] = (string) $job; + $context['finished'] = $executor->countJobs($tag, [Job::FAILED, Job::FINISHED]) / $executor->countJobs($tag); + } + + /** + * Batch finish callback. + */ + public static function batchFinished() { + \Drupal::messenger()->addStatus(t('Team members are synced in Drupal')); + } + +} diff --git a/modules/apigee_edge_teams/src/Entity/TeamAccessHandler.php b/modules/apigee_edge_teams/src/Entity/TeamAccessHandler.php index 82028d3b..f51335a7 100644 --- a/modules/apigee_edge_teams/src/Entity/TeamAccessHandler.php +++ b/modules/apigee_edge_teams/src/Entity/TeamAccessHandler.php @@ -108,7 +108,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter // Check if current developer is a member of the team and has the permision // to view more than 100 teams. // Should not run for developer with less than 100 teams. - if ($account->hasPermission('view extensive team list') && (count($developer_team_ids) > 100)) { + if ($account->hasPermission('view extensive team list') && (count($developer_team_ids) >= 100)) { $team_members = $this->teamMembershipManager->getMembers($entity->id()); if (in_array($account->getEmail(), $team_members)) { $developer_team_access = TRUE; diff --git a/modules/apigee_edge_teams/src/Form/TeamMemberSyncForm.php b/modules/apigee_edge_teams/src/Form/TeamMemberSyncForm.php new file mode 100644 index 00000000..33e99cb6 --- /dev/null +++ b/modules/apigee_edge_teams/src/Form/TeamMemberSyncForm.php @@ -0,0 +1,160 @@ +sdkConnector = $sdk_connector; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('apigee_edge.sdk_connector') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'apigee_edge_team_member_sync_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + try { + $this->sdkConnector->testConnection(); + } + catch (\Exception $exception) { + $this->messenger()->addError($this->t('Cannot connect to Apigee Edge server. Please ensure that Apigee Edge connection settings are correct.', [ + ':link' => Url::fromRoute('apigee_edge.settings')->toString(), + ])); + return $form; + } + + $form['#attached']['library'][] = 'apigee_edge/apigee_edge.admin'; + + $form['sync'] = [ + '#type' => 'details', + '#title' => $this->t('Synchronize team members'), + '#open' => TRUE, + ]; + + $form['sync']['description'] = [ + '#type' => 'container', + 'p1' => [ + '#type' => 'html_tag', + '#tag' => 'p', + '#value' => $this->t('Team member synchronization will:'), + ], + 'list' => [ + '#theme' => 'item_list', + '#items' => [ + $this->t('Caches team members in Drupal'), + ], + ], + 'p2' => [ + '#type' => 'html_tag', + '#tag' => 'p', + '#value' => $this->t('The "Run team member sync" button will sync the team members, displaying a progress bar on the screen while running. The "Background team member sync" button will run the team member sync process in batches each time cron runs and may take multiple cron runs to complete.', [':cron_url' => Url::fromRoute('system.cron_settings')->toString()]), + ], + 'p3' => [ + '#type' => 'html_tag', + '#tag' => 'p', + '#value' => $this->t('By running the sync, team member detail is stored in members cache table and will have expiry that is set in team caching. To show more than 100 teams for a member enable permission "View extensive teams list". ', [':team_caching' => Url::fromRoute('apigee_edge_teams.settings.team.cache')->toString()]), + ], + ]; + + $form['sync']['sync_submit'] = [ + '#title' => $this->t('Run team member sync'), + '#type' => 'link', + '#url' => $this->buildUrl('apigee_edge_teams.team_member.run'), + '#attributes' => [ + 'class' => [ + 'button', + 'button--primary', + ], + ], + ]; + $form['sync']['background_team_member_sync_submit'] = [ + '#title' => $this->t('Background team member sync'), + '#type' => 'link', + '#url' => $this->buildUrl('apigee_edge_teams.team_member.schedule'), + '#attributes' => [ + 'class' => [ + 'button', + ], + ], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + } + + /** + * Build URL for team member sync processes, using CSRF protection. + * + * @param string $route_name + * The name of the route. + * + * @return \Drupal\Core\Url + * The URL to redirect to. + */ + protected function buildUrl(string $route_name): Url { + $url = Url::fromRoute($route_name); + $token = \Drupal::csrfToken()->get($url->getInternalPath()); + $url->setOptions(['query' => ['destination' => 'admin/config/apigee-edge/app-settings/team-settings/sync', 'token' => $token]]); + return $url; + } + +} diff --git a/modules/apigee_edge_teams/src/Job/TeamMemberCreateUpdate.php b/modules/apigee_edge_teams/src/Job/TeamMemberCreateUpdate.php new file mode 100644 index 00000000..29d33331 --- /dev/null +++ b/modules/apigee_edge_teams/src/Job/TeamMemberCreateUpdate.php @@ -0,0 +1,56 @@ +team_ids = $team_ids; + } + + /** + * {@inheritdoc} + */ + protected function executeRequest() { + $member_controller = \Drupal::service('apigee_edge_teams.team_membership_manager'); + $team_members = $member_controller->getMembers($this->team_ids); + } + +} diff --git a/modules/apigee_edge_teams/src/Job/TeamMemberSync.php b/modules/apigee_edge_teams/src/Job/TeamMemberSync.php new file mode 100644 index 00000000..24bab27f --- /dev/null +++ b/modules/apigee_edge_teams/src/Job/TeamMemberSync.php @@ -0,0 +1,79 @@ +filter = $filter; + } + + /** + * Executes the request itself. + */ + protected function executeRequest(){} + + /** + * {@inheritdoc} + */ + public function execute(): bool { + parent::execute(); + + $team_ids = array_keys(\Drupal::entityTypeManager()->getStorage('team')->loadMultiple()); + + foreach ($team_ids as $team_name) { + $update_team_member_job = new TeamMemberUpdate($team_name); + $update_team_member_job->setTag($this->getTag()); + $this->scheduleJob($update_team_member_job); + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function __toString(): string { + return t('Synchronizing Team Members in Drupal.')->render(); + } + +} diff --git a/modules/apigee_edge_teams/src/Job/TeamMemberUpdate.php b/modules/apigee_edge_teams/src/Job/TeamMemberUpdate.php new file mode 100644 index 00000000..c7547924 --- /dev/null +++ b/modules/apigee_edge_teams/src/Job/TeamMemberUpdate.php @@ -0,0 +1,36 @@ + $this->team_ids, + ])->render(); + } + +} diff --git a/modules/apigee_edge_teams/src/TeamPermissionHandler.php b/modules/apigee_edge_teams/src/TeamPermissionHandler.php index 08dbcd57..f963fe0c 100644 --- a/modules/apigee_edge_teams/src/TeamPermissionHandler.php +++ b/modules/apigee_edge_teams/src/TeamPermissionHandler.php @@ -164,7 +164,7 @@ public function getDeveloperPermissionsByTeam(TeamInterface $team, AccountInterf else { // Check if current developer is a member of the team and has the permision // to view more than 100 teams. - if ($account->hasPermission('view extensive team list') && (count($developer_team_ids) > 100)) { + if ($account->hasPermission('view extensive team list') && (count($developer_team_ids) >= 100)) { $team_members = $this->teamMembershipManager->getMembers($team->id()); if (in_array($account->getEmail(), $team_members)) { $developer_team_access = TRUE; From 243549e187eb59d5dd0b669a419842dbd01cc0d5 Mon Sep 17 00:00:00 2001 From: Raakesh Blokhra Date: Mon, 21 Nov 2022 02:09:12 -0500 Subject: [PATCH 2/6] Update bug_report.md (#749) --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index a4c59a35..1a500663 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,6 +9,9 @@ assignees: '' ## Description A clear and concise description of what the bug is. +## Apigee Info +Please specify if you are using Apigee X, Apigee Edge, or OPDK. + ## Steps to Reproduce Steps to reproduce the behavior: 1. Go to '...' @@ -31,4 +34,3 @@ Add any other context about the problem here. This can be the version you can see on admin/modules in Drupal or the output of this command: `composer show`. Add Drupal core and other version information if needed. - From 09085e55fc5a3703c44dc597f6b8a4ad4b6b1663 Mon Sep 17 00:00:00 2001 From: Shishir <75600200+shishir-intelli@users.noreply.github.com> Date: Mon, 21 Nov 2022 16:51:13 +0530 Subject: [PATCH 3/6] Fix error while edit team app (#752) --- .../apigee_edge_teams/src/Entity/Form/TeamAppEditForm.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/apigee_edge_teams/src/Entity/Form/TeamAppEditForm.php b/modules/apigee_edge_teams/src/Entity/Form/TeamAppEditForm.php index 4a867c26..6ec896bb 100644 --- a/modules/apigee_edge_teams/src/Entity/Form/TeamAppEditForm.php +++ b/modules/apigee_edge_teams/src/Entity/Form/TeamAppEditForm.php @@ -97,8 +97,10 @@ protected function appCredentialController(string $owner, string $app_name): App */ public function form(array $form, FormStateInterface $form_state) { $form = parent::form($form, $form_state); - foreach (Element::children($form['credential']) as $credential) { - $form['credential'][$credential]['api_products'] += $this->nonMemberApiProductAccessWarningElement($form, $form_state); + if (isset($form['credential'])) { + foreach (Element::children($form['credential']) as $credential) { + $form['credential'][$credential]['api_products'] += $this->nonMemberApiProductAccessWarningElement($form, $form_state); + } } return $form; } From 5857361da977b94d6fccc46f473f3f1958f3ab61 Mon Sep 17 00:00:00 2001 From: phdhiren Date: Thu, 15 Dec 2022 14:44:05 +0530 Subject: [PATCH 4/6] Using the "Twig_Loader_Chain" class is deprecated (#763) Using the "Twig_Loader_Chain" class is deprecated since Twig version 2.7, use "Twig\Loader\ChainLoader" instead. --- .../apigee_mock_api_client/apigee_mock_api_client.services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/modules/apigee_mock_api_client/apigee_mock_api_client.services.yml b/tests/modules/apigee_mock_api_client/apigee_mock_api_client.services.yml index 9e771ce4..0f404eed 100644 --- a/tests/modules/apigee_mock_api_client/apigee_mock_api_client.services.yml +++ b/tests/modules/apigee_mock_api_client/apigee_mock_api_client.services.yml @@ -35,7 +35,7 @@ services: arguments: ['@apigee_mock_api_client_twig_json.loader'] apigee_mock_api_client_twig_json.loader: - class: \Twig_Loader_Chain + class: Twig\Loader\ChainLoader public: false tags: - { name: service_collector, tag: apigee_mock_api_client_twig.loader, call: addLoader, required: TRUE } From e0de946115fc5bd50556c013cea17a11ca219e16 Mon Sep 17 00:00:00 2001 From: Divyajose <75604843+divya-intelli@users.noreply.github.com> Date: Mon, 19 Dec 2022 17:00:52 +0530 Subject: [PATCH 5/6] Using "min" option without setting the "allowEmptyString" is deprecated (#765) --- .../apigee_edge_test/src/Entity/OverriddenDeveloperApp.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/modules/apigee_edge_test/src/Entity/OverriddenDeveloperApp.php b/tests/modules/apigee_edge_test/src/Entity/OverriddenDeveloperApp.php index 868596a6..c95c026c 100644 --- a/tests/modules/apigee_edge_test/src/Entity/OverriddenDeveloperApp.php +++ b/tests/modules/apigee_edge_test/src/Entity/OverriddenDeveloperApp.php @@ -39,6 +39,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type): a 'Length' => [ 'min' => 1, 'max' => 30, + 'allowEmptyString' => TRUE ], ]); From d2dc18b0da5310228d2d32065336f13f71f87391 Mon Sep 17 00:00:00 2001 From: phdhiren Date: Fri, 23 Dec 2022 11:04:36 +0530 Subject: [PATCH 6/6] End of support for PHP 7.4 (#768) --- .github/workflows/php.yml | 1 - composer.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index bc83e6e4..d648882b 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -29,7 +29,6 @@ jobs: fail-fast: false matrix: php-version: - - "7.4" - "8.0" - "8.1" drupal-core: diff --git a/composer.json b/composer.json index 5888df9d..7bed312f 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "type": "drupal-module", "description": "Apigee Edge for Drupal.", "require": { - "php": "^7.4 || ^8.0", + "php": "^8.0", "ext-json": "*", "apigee/apigee-client-php": "^2.0.16", "drupal/core": "^9.3",