get('database'), $container->get('current_user'), $container->get('datetime.time'), $container->get('plugin.manager.mail'), $container->get('language_manager'), $container->get('config.factory'), $container->get('logger.factory')->get('event_manager'), $container->get('current_route_match'), ); } /** * Drupalがフォームを識別するためのIDを返す. * * 回答フォームの生成を重複しない値でする必要がある. * * @return string * フォームIDを返す */ public function getFormId() { return 'event_manager_attendance_form'; } /** * 画面に表示する出欠回答フォームを組み立てる. * * @param array $form * Form要素を入れる配列. * @param \Drupal\Core\Form\FormStateInterface $form_state * Formの入力値と状態を管理する. * * @return array * 組み立てたフォーム配列を返す */ public function buildForm(array $form, FormStateInterface $form_state) { $event = $this->getEventNode(); if (!$event) { $this->messenger()->addError($this->t('対象イベントが見つかりません')); return [ 'message' => [ '#markup' => $this->t('対象イベントが見つかりません'), ], ]; } $deadline = $this->getDeadline($event); if (!$deadline) { $this->messenger()->addError($this->t('回答期限が設定されていません')); return [ 'message' => [ '#markup' => $this->t('回答期限が設定されていません'), ], ]; } if ($this->isDeadlineClosed($deadline)) { $this->messenger()->addError($this->t('回答期限を過ぎているため、受付は終了しました')); return [ 'message' => [ '#markup' => $this->t('回答期限を過ぎているため、受付は終了しました'), ], ]; } $uid = (int) $this->currentUser->id(); $submission = $this->getSubmission($uid, (int) $event->id()); if ($submission && $uid !== 1) { $this->messenger()->addStatus($this->t('回答済みです内容を変更する場合は、修正して送信してください')); } $form['attendance'] = [ '#type' => 'radios', '#title' => $this->t('出欠'), '#options' => [ 'attend' => $this->t('参加'), 'absent' => $this->t('不参加'), ], '#default_value' => ($uid === 1) ? NULL : ($submission->attendance ?? NULL), '#required' => TRUE, ]; $form['comment'] = [ '#type' => 'textarea', '#title' => $this->t('任意コメント'), '#default_value' => ($uid === 1) ? '' : ($submission->comment ?? ''), '#required' => FALSE, ]; $form['actions']['submit'] = [ '#type' => 'submit', '#value' => $this->t('送信する'), ]; return $form; } /** * 送信直前に回答対象と回答期限をもう一度確認する. * * @param array $form * Formから送信された配列. * @param \Drupal\Core\Form\FormStateInterface $form_state * Formの入力値とエラー状態を管理する. */ public function validateForm(array &$form, FormStateInterface $form_state) { $event = $this->getEventNode(); if (!$event) { $form_state->setErrorByName('attendance', $this->t('対象イベントが見つかりません')); return; } $deadline = $this->getDeadline($event); if (!$deadline) { $form_state->setErrorByName('attendance', $this->t('回答期限が設定されていません')); return; } if ($this->isDeadlineClosed($deadline)) { $form_state->setErrorByName('attendance', $this->t('回答期限を過ぎているため、受付は終了しました')); } } /** * 送信された出欠回答を保存して集計結果画面へ移動する. * * @param array $form * Formから送信された配列. * @param \Drupal\Core\Form\FormStateInterface $form_state * Formの入力値とリダイレクト先を管理する. */ public function submitForm(array &$form, FormStateInterface $form_state) { $event = $this->getEventNode(); if (!$event) { $this->messenger()->addError($this->t('対象イベントが見つかりません')); return; } $uid = (int) $this->currentUser->id(); $user_name = $this->currentUser->getDisplayName(); $attendance = (string) $form_state->getValue('attendance'); $comment = trim((string) $form_state->getValue('comment')); $submission = $this->getSubmission($uid, (int) $event->id()); // && ($uid !== 1) $is_update = (bool) $submission; if ($is_update) { $this->database->update('event_manager_attendance') ->fields( [ 'user_name' => $user_name, 'attendance' => $attendance, 'comment' => $comment, 'created' => $this->time->getRequestTime(), ] ) ->condition('id', (int) $submission->id) ->execute(); } else { $this->database->insert('event_manager_attendance') ->fields( [ 'uid' => $uid, 'event_id' => (int) $event->id(), 'user_name' => $user_name, 'attendance' => $attendance, 'comment' => $comment, 'created' => $this->time->getRequestTime(), ] ) ->execute(); } // 回答の保存が終わってから、管理画面で設定した宛先へ通知する. $this->sendNotification($event, $user_name, $attendance, $comment); // 保存結果を画面上部のメッセージとして表示する. $this->messenger()->addStatus($is_update ? $this->t('回答を更新しました') : $this->t('回答を送信しました')); // 送信後は同じイベントの集計結果画面へ移動する. $form_state->setRedirect('event_manager.event_chart', ['node' => $event->id()]); } /** * 管理画面で設定された宛先へ出欠回答メールを送る. * * 回答データは先に保存されているため、メール送信に失敗しても * 回答自体は消さず、エラー内容だけをDrupalのログへ残す。 */ private function sendNotification( NodeInterface $event, string $user_name, string $attendance, string $comment, ): void { // 管理画面で保存したメール通知設定を読み込む. $settings = $this->eventConfigFactory->get('event_manager.settings'); // 通知が無効なら、メール処理を行わずに終了する. if ($settings->get('enabled') === FALSE) { return; } // 管理画面で指定された通知先を取得し、前後の空白を取り除く. $recipient = trim((string) $settings->get('recipient')); // 通知先が未設定なら、Drupalのサイトメールアドレスを代わりに使う. if ($recipient === '') { $recipient = trim((string) $this->eventConfigFactory->get('system.site')->get('mail')); } // どちらのメールアドレスも空の場合は送れないため、ログへ記録して終了する. if ($recipient === '') { $this->logger->error('通知先メールアドレスが設定されていません。'); return; } // event_manager_mail()へ渡す、メール本文用のデータをまとめる. $params = [ // 回答対象のイベント名. 'event_title' => $event->label(), // メールからイベントページを開くための絶対URL. 'event_url' => $event->toUrl('canonical', ['absolute' => TRUE])->toString(), // 回答したユーザーの表示名. 'user_name' => $user_name, // DB保存用の値を「参加」「不参加」の表示へ変換する. 'attendance_label' => $this->attendanceLabel($attendance), // フォームに入力された任意コメント. 'comment' => $comment, ]; // Drupalの標準言語をメール作成時の言語として使う. $langcode = $this->languageManager->getDefaultLanguage()->getId(); // メール側でエラーが起きても、保存済みの回答には影響させない. try { // DrupalのMail APIからevent_manager_mail()を呼び出して送信する. $result = $this->mailManager->mail( // メールを作るモジュール名. 'event_manager', // event_manager_mail()内で使うメール種別. 'event_attendance_notification', // 管理画面で設定された送信先. $recipient, // メール作成に使う言語コード. $langcode, // 件名と本文へ差し込むデータ. $params, ); // Mail APIが失敗を返した場合は、管理者が確認できるようログへ残す. if (empty($result['result'])) { $this->logger->error('通知メールの送信に失敗しました。'); } } // メールサーバーなどで例外が起きた場合も、回答画面をエラーにしない. catch (\Throwable $exception) { // 例外の内容をDrupalのログ画面から確認できるようにする. $this->logger->error( '通知メールの送信中にエラーが発生しました: @message', ['@message' => $exception->getMessage()], ); } } /** * 保存用の出欠値をメール表示用の日本語に変換する. */ private function attendanceLabel(string $attendance): string { // 想定外の値が来た場合は、調査できるよう元の値をそのまま返す. return match ($attendance) { 'attend' => '参加', 'absent' => '不参加', default => $attendance, }; } /** * 回答期限を過ぎているか判定する. * * @param \Drupal\Core\Datetime\DrupalDateTime $deadline * Deadlineとして回答期限日時を受け取る. * * @return bool * 期限切れならTRUEを返す */ public function isDeadlineClosed(DrupalDateTime $deadline): bool { return $this->time->getCurrentTime() > $deadline->getTimestamp(); } /** * 同じユーザーが同じイベントへ送った最新回答を取得する. * * @param int $uid * ユーザーIDを受け取る. * @param int $event_id * イベントノードIDを受け取る. * * @return object|null * 回答があれば最新レコードを返しなければNULLを返す */ public function getSubmission(int $uid, int $event_id): ?object { if ($uid <= 0) { return NULL; } $submission = $this->database->select('event_manager_attendance', 'a') ->fields('a', ['id', 'attendance', 'comment']) ->condition('uid', $uid) ->condition('event_id', $event_id) ->orderBy('created', 'DESC') ->orderBy('id', 'DESC') ->range(0, 1) ->execute() ->fetchObject(); return $submission ?: NULL; } /** * イベントノードに設定された回答期限を取得する. * * @param \Drupal\node\NodeInterface $event * Eventノードとして回答対象を受け取る. * * @return \Drupal\Core\Datetime\DrupalDateTime|null * 回答期限があれば日時を返しなければNULLを返す */ public function getDeadline(NodeInterface $event): ?DrupalDateTime { $value = $event->get('field_deadline')->value; if (!$value) { return NULL; } $deadline = new DrupalDateTime($value, DateTimeItemInterface::STORAGE_TIMEZONE); $deadline->setTimezone(new \DateTimeZone(self::DISPLAY_TIMEZONE)); return $deadline; } /** * URLから回答対象のイベントノードを取得する. * * @return \Drupal\node\NodeInterface|null * eventノードなら返し対象外ならNULLを返す */ public function getEventNode(): ?NodeInterface { $node = $this->currentRouteMatch->getParameter('node'); if (!$node instanceof NodeInterface || $node->bundle() !== 'event') { return NULL; } return $node; } }