PHPで「お問い合わせ画面」を実装してみた


|[../](https://qiita.com/taconana/items/3f5424f9abfc755772c2) |
|---|

PHPの練習のため、お問い合わせの画面を実装してみた。画面遷移は以下の感じ。
![contact-states.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39200/9e4058f0-8057-491d-6dd8-e4cac35dd5c3.png)
オブジェクト指向でモデルを実装する。クラス図は以下の感じ。
![contact-classes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39200/5d9686ab-635e-83ca-87b1-89f0ab3f97a3.png)

問い合わせのエンティティ(Contact)は、とりあえずシンプルに名前、メールアドレス、問い合わせの内容の3属性とした。setter/getterは割愛。今回は永続化していないので、エンティティは単に編集フォームとして利用しているだけ。
```php:Contact.php
<?php
namespace contact\domain;

class Contact {
  
  public $name;
  public $mail;
  public $contents;
  
  public function __construct(){
    $this->init();
  }
  
  public function init(){
    $this->name = '';
    $this->mail = '';
    $this->contents = '';
  }
}
```
今回は永続化はしないので、スタブのみ。ContactDataAccessorは以下の感じ。
```php:ContactDataAccessor.php
<?php
namespace contact\integration;

require_once(__DIR__."/../domain/Contact.php");
use contact\domain\Contact;

class ContactDataAccessor {
  
  public function registerContact(Contact &$form, $validContents) : int {
    // TODO スタブ
    error_log($form->name . "さんからの問い合わせを処理しました。");
    return 1;
  }
}
```
ドメインロジックのContactManagerは以下の感じ。isValidContact()は、[確認]時にも、[送信]時にも呼ばれるようにしておく。
```php:ContactManager.php
<?php
namespace contact\domain;

require_once(__DIR__."/../integration/ContactDataAccessor.php");
use contact\integration\ContactDataAccessor;

class ContactManager {
  
  private static $instance;
  private $cda;
  
  public static function getInstance(): ContactManager {
    if (empty(self::$instance)) {
      self::$instance = new ContactManager();
    }
    return self::$instance;
  }
  
  private function __construct() {
    $this->cda = new ContactDataAccessor();
  }
  
  private function isValidName(string $name) : bool {
    if (empty($name) || strlen($name) > 20) return false;
    return true;
  }
  
  private function isValidMail(string $mail) : bool {
    if (empty($mail) || strlen($mail) > 50) return false;
    $pattern = "/^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/";
    return preg_match($pattern, $mail);
  }
  
  private function isValidContents(string $contents) : bool {
    if (empty($contents) || strlen($contents) > 2000) return false;
    return true;
  }
  
  /**
   * 問い合わせの入力値をチェックする。
   * @param Contact $form 問い合わせフォーム
   * @param array $errors エラーコンテナ
   * @return bool 全ての入力値が適正ならtrue
   */
  public function isValidContact(Contact &$form, array &$errors): bool {
    if (!$this->isValidName($form->name)){
      $errors['name'] = '※20文字以内';
    }
    if (!$this->isValidMail($form->mail)){
      $errors['mail'] = '※50文字以内で@を含む';
    }
    if (!$this->isValidContents($form->contents)){
      $errors['contents'] = '※2,000文字以内';
    }
    $b = empty($errors);
    if (!$b){
      $errors['_default'] = '入力値を修正ください。';
    }
    return $b;
  }
  
  /**
   * 問い合わせ情報を登録または送信する。
   * @param Contact $form 問い合わせフォーム
   * @param array $errors エラーコンテナ
   * @return int 処理した件数またはエラーステータス
   */
  public function registerContact(Contact &$form, array &$errors) : int {
    if ($this->isValidContact($form, $errors) == false){
      // 入力値にエラーがある
      return -1;
    }
    
    // 問い合わせ内容のみ参照表現に対応
    $validContents = nl2br(htmlspecialchars($form->contents, ENT_QUOTES, 'UTF-8'));
    
    return $this->cda->registerContact($form, $validContents);
  }
}
```
プレゼンテーションでは、`$_SESSION`に保持するコンテキストを実装しておく。Contactの編集フォームとエラーコンテナを保持する。別々に保持してもいいのだが、`$_SESSION`に保持するコンテキストはできるだけまとめておく方針。
```php:ContactContext.php
<?php
namespace contact\presentation;
require_once(__DIR__."/../domain/Contact.php");
use contact\domain\Contact;

class ContactContext {
  
  const THIS_CONTEXT = "contact-context";
  private $form;
  private $errors;
  
  public function __construct() {
    $this->setForm();
    $this->setErrors();
  }

  public static function get(): ContactContext {
    if (!isset($_SESSION)){
      session_start();
    }
    $context = null;
    if (isset($_SESSION[self::THIS_CONTEXT])){
      $context = $_SESSION[self::THIS_CONTEXT];
    }
    if ($context == null || !($context instanceof ContactContext)){
      $context = new ContactContext();
      $_SESSION[self::THIS_CONTEXT] = $context;
    }
    return $context;
  }
  
  public function getForm(): Contact {
    if (empty($this->form)){
      $this->form = new Contact();
    }
    return $this->form;
  }
  
  public function setForm(Contact $form = null): void {
    $this->form = $form;
  }
  
  public function getErrors() {
    return $this->errors;
  }
  
  public function setErrors(array $errors = null): void {
    if ($errors == null){
      $this->errors = array();
    } else {
      $this->errors = $errors;
    }
  }
  
  public function getErrorMessage(string $key = '_default'): string {
    foreach ($this->errors as $k => $v) {
      if ($key == $k){
        return '<font color="red">' . $v . '</font>';
      }
    }
    return '<br>';
  }
}
```
以下、画面を並べておく。まず、index.phpから。
```php:index.php
<?php namespace contact; ?>
<a href="contact-input.php">お問い合わせ</a>
```
次に、お問い合わせ入力画面。
```php:contact-input.php
<?php 
namespace contact;
require_once(__DIR__."/domain/Contact.php");
require_once(__DIR__."/presentation/ContactContext.php");
use contact\presentation\ContactContext;

$context = ContactContext::get();
$referer = $_SERVER['HTTP_REFERER'];
if (!strstr($referer, "contact-input.php") && !strstr($referer, "contact-confirm.php")){
  $context->setForm();
  $context->setErrors();
}
$form = $context->getForm();
?>
<script type="text/javascript">
  function doReset(form){
    form.operation.value = "reset";
    form.submit();
    return false;
  }
</script>
<div>
  <p>問い合わせの内容を入力し、[確認]してください。</p>
  <p><?= $context->getErrorMessage() ?></p>       
  <form action="contact-actions.php" method="post">
	<input type="hidden" name="operation" value="validate"/>
    <div>
      お名前：<br><input type="text" name="name" value="<?= $form->name ?>" placeholder="山田太郎" >
      <?= $context->getErrorMessage('name') ?>
    </div>
    <div>
      メールアドレス：<br><input type="text" name="mail" value="<?= $form->mail ?>" placeholder="yamada@xxx.com">
      <?= $context->getErrorMessage('mail') ?>
    </div>
  	<div>  
      問い合わせの内容：<br><textarea name="contents" rows="3" cols="30"><?= $form->contents ?></textarea>
      <?= $context->getErrorMessage('contents') ?>
  	</div>
  	<div>
  	  <br>
  	  <input type="submit" value="キャンセル" formaction="index.php" >
  	  <input type="submit" value="リセット" onclick="return doReset(this.form)" >
  	  <input type="submit" value="確認" style="background-color:lightpink;" >
  	</div>
  </form>
</div>
<?= $context->setErrors(); ?>
```
次に、お問い合わせ確認画面。
```php:contact-confirm.php
<?php
namespace contact;
require_once(__DIR__."/domain/Contact.php");
require_once(__DIR__."/presentation/ContactContext.php");
use contact\presentation\ContactContext;

$referer = $_SERVER['HTTP_REFERER'];
if (!strstr($referer, "contact-input.php")){
  header("Location:contact-input.php");
  return;
}
$context = ContactContext::get();
$form = $context->getForm();
?>
<div>
  <p>問い合わせの内容を確認し、[送信]してください。</p>
  <p><br></p>
  <form action="contact-actions.php" method="post">
    <input type="hidden" name="operation" value="register"/>
    <div>
      お名前：<br><input type="text" value="<?= $form->name ?>" disabled="disabled" /><br>
    </div>
    <div>
      メールアドレス：<br><input type="text" value="<?= $form->mail ?>" disabled="disabled" /><br>
    </div>
    <div>
      問い合わせの内容：<br/><textarea rows="3" cols="30" disabled="disabled"><?= $form->contents ?></textarea><br>
    </div>
    <div>
      <br>
      <input type="submit" value="入力に戻る" formaction="contact-input.php">
      <input type="submit" value="送信" style="background-color:lightpink;" >
    </div>
  </form>
</div>
```
次に、お問い合わせ完了画面。
```php:contact-complete.php
<?php namespace contact; ?>
<div>
  <p>お問い合わせをいただき、ありがとうございます。<br/>
  数日内に回答させていただきます</p>
  <p><br></p>
  <p><a href="index.php">トップへ</a></p>
</div>
```
最後に画面遷移のためのcontact-actions.phpを。[確認]ボタン、[送信]ボタン押下時のアクションを記述している。[リセット]でも利用してみた。
```php:contact-actions.php
<?php
namespace contact;
require_once(__DIR__."/domain/ContactManager.php");
require_once(__DIR__."/presentation/ContactContext.php");
use contact\domain\ContactManager;
use contact\presentation\ContactContext;

$referer = $_SERVER['HTTP_REFERER'];
if (!strstr($referer, "contact-input.php") && !strstr($referer, "contact-confirm.php")){
  header("Location:contact-input.php");
  return;
}

$context = ContactContext::get();
$form = $context->getForm();
$errors = array();
$location = "contact-input.php";

$operation = $_POST['operation'];
if ($operation == 'reset'){
  
  // [リセット]ボタン押下時
  
  $form->init();
  
} else if ($operation == 'validate'){
  
  // [確認]ボタン押下時
  
  $form->name = $_POST['name'];
  $form->mail = $_POST['mail'];
  $form->contents = $_POST['contents'];
  
  $manager = ContactManager::getInstance();
  $state = $manager->isValidContact($form, $errors);
  if ($state){
    $location = "contact-confirm.php";
  }
  
} else if ($operation == 'register'){
  
  // [送信]ボタン押下時
  
  $manager = ContactManager::getInstance();
  $cnt = $manager->registerContact($form, $errors);
  if ($cnt > 0){
    $form->init();
    $location = "contact-complete.php";
  }
}
$context->setErrors($errors);
header("Location:" . $location);
?>
```
以上
|[../](https://qiita.com/taconana/items/3f5424f9abfc755772c2) |
|---|