カレンダー共有のためのテーブル設計(案)

日付の確認や予定の確認・追加・編集などが行えるカレンダー機能。
現在、このカレンダーを複数人で共有するためのテーブル設計について考えています。

要件
  1. ユーザは複数のカレンダーを作成することができる
  2. ユーザは作成したカレンダーを他人と共有することができる(別のユーザを招待)
  3. ユーザは自分のカレンダーか、招待を受諾済みのカレンダーにのみアクセスすることができる
  4. カレンダーへの招待は通知で知らせ、その際、カレンダー名とオーナーのユーザ名を表示したい
     例: 〇〇さんから「共有カレンダー1」への招待が来ています という通知
  5. /calendar/xxxxxで対象のカレンダーにアクセスする
テーブル設計案

f:id:YYProgramming:20210202185647p:plain

部分関数従属や推移的関数従属がなく第3正規形にできており、以下のようなクエリで 「招待されているが、まだ招待を受諾していないカレンダー」の名前や招待者(作成者)の名前が 取得できることから要件も満たせそうです。

SELECT users.name AS owner, calendars.name
FROM users_calendars
    INNER JOIN calendars
    ON users_calendars.calendar_id = calendars.id
    AND users_calendars.user_id = 2 
    AND f_approval = 0
        INNER JOIN users
        ON  calendars.user_id = users.id;



この設計で気になっているのは以下の2点です。

1. リレーションがループしているが問題ないのか
2. 結合2回はコストが大きいのではないか

このテーブルの作成方法だと、どうしてもユーザとカレンダーの間に多対多と1対多の2つの関係が 成り立ってしまいます。
①ユーザは複数のカレンダーに参加でき、カレンダーは複数ユーザの参加を許可している
②ユーザは複数カレンダーを作成できるが、同じカレンダーを複数ユーザが作成することはできない

そのため、解決するとしたら別のテーブルを追加したりする必要があるように思います。
現状ではほかの設計が思いつかないので、実装していく中で改善案があればまた載せたいです。


Facadeパターン(?)

ごちゃごちゃしたコードを弄っている中で、デザインパターンの1つであるFacadeパターンを使えばコードを改善できそうだったので内容をメモしときます。
タイトルに疑問符がついているのはこれが本当にFacadeパターンになっているのか確信が持てなかったためです…。

Facadeパターンについてはコチラ
15.Facadeパターン | TECHSCORE(テックスコア)

今回ターゲットとしている処理は、

メールアドレスで仮登録 → メールにて送付されたトークン付URLをクリック → パスワードを入力して本登録

という流れの中の、本登録処理。

改善前の疑似コードは以下の通り。

// Usersクラス

public function register($token, $password) {
    
    // 仮登録ユーザテーブルpre_usersから、
    // $tokenを持つユーザ情報を取得(select)するクエリを発行

    // 本登録ユーザテーブルusersに、メールアドレスとパスワードなどを
    // 挿入(insert)してユーザ情報を新規作成するクエリを発行

    // 仮登録ユーザテーブルpre_usersで、ユーザが
    // 本登録済みであることを示すフラグをセット(update)するクエリを発行

}         


このコードで気になった点は、

  • 1つのメソッドに複数の処理(DBに対するselect、insert、update)が書かれている
  • 本登録ユーザ関連のデータと処理をまとめたはずのUsersというクラスの中で
    仮登録ユーザテーブル(pre_users)を操作している
  • 他所で同様のクエリを発行したい場合、類似コードが出てきてしまう
    ⇒ 再利用性のなさ

この3点です。

この問題を解消するために、各クエリ発行の処理を別メソッドに分け、さらにFacadeパターンを適用してみました。

AccountManagerクラスの中でregister( )メソッドを用意し、そのメソッド内で、分割したUsersクラスの各メソッドを呼び出します。
こうすることで情報取得のためのクエリなどが再利用しやすくなり、Usersクラスが自身に関係する処理のみを実行できるようになりました。

利用する側(Controllerクラスなど)はregister( )メソッドを呼び出すだけでよく、
難しいことを考える必要はなくなり、さらにregister( )を使いまわす(可能性は低いですが)ことも可能になりました。

// PreUsersクラス

public function get($token) {
    
    // 仮登録ユーザテーブルpre_usersから、
    // $tokenを持つユーザ情報を取得するクエリ発行

}

public function setRegisteredFlag( ) {

    // 仮登録ユーザテーブルpre_usersで、ユーザが本登録済みであることを
    // 示すフラグをセットするクエリ発行

}

// Usersクラス

public function createUser( ) {

    // 本登録ユーザテーブルusersに、メールアドレスとパスワードなどを
    // 挿入してユーザ情報を新規作成するクエリ発行

} 

// AccountManagerクラス

public function register( ) {

    $preUser = new PreUsers( ) ; 
    $user = new Users( ) ; 

    $preUser->get( ) ;
    $user->create( ) ;
    $preUser->setRegisteredFlag( ) ;

} 



開発環境の整理

現時点(2021/1/22)でのweb開発用の環境を整理し、メモしておきます。
導入方法ではなく構成を記録するための記事です。

環境

仮想環境

Windows10 Home バージョン2004
Vagrant : 2.2.14
VirtualBox : 6.1.18
Docker : 20.10.2
docker-compose : 1.22.0

webシステム

PHP : 8.0.1 (Docker)
Apache : 2.4.38 (Docker)
MySQL : 8.0 (Docker)

パッケージ管理

Composer : 1.10 (Docker)

メール送信

mailhog : 1.0.1 (Docker)
PHPMailer : 6.2 (Composer)

テスト

phpunit/phpunit : 9.5 (Composer)
php-webdriver/webdriver : 1.9 (Composer)
selenium/standalone-chrome-debug : 3.141.59 (Docker)

ルーティング

altorouter/altorouter : 2.0 (Composer)

ログ

monolog/monolog : 2.2 (Composer)

環境変数

vlucas/phpdotenv : 5.2 (Composer)

ディレクトリ構成

.
├── app
│   ├── composer.json
│   ├── composer.lock
│   ├── index.php
│   ├── logs
│   ├── route.php
│   ├── src
│   ├── tests
│   └── vendor
├── docker-compose.yml
├── env
│   ├── mysql
│   │ 
│   └── php-apache
│       ├── apache2.conf
│       ├── Dockerfile
│       └── php.ini
└── Vagrantfile



【PHPMailer】メール送信の際の文字エンコードについて

PHPMailerを利用したメール送信について調べていたときに、
文字エンコードの指定を「ISO-2022-JP」にしているパターンと「UTF-8」にしているパターンの2種類を見かけました。

 $mail = new PHPMailer(true);

 // ISO-2022-JPを使うパターン 
 $mail->CharSet = "iso-2022-jp";
 $mail->Encoding = "7bit";
 
 // UTF-8を使うパターン
 $mail->CharSet = "UTF-8";


調べてみると、歴史的な理由から日本では「ISO-2022-JP」が使われてきたのですが、 現在は有名どころ含めメーラが「UTF-8」に対応してきているとのことです。

基本的に「UTF-8」を指定し、古いメーラに対応しなければいけない場合は「ISO-2022-JP」を指定するのが よさそうです。

参考

日本語メールの仕組み | SendGridブログ
体系的に学ぶ 安全なWebアプリケーションの作り方 第2版


【MailHog&Docker】ローカル開発環境でメール送受信をテストする

ローカル開発環境内でのメールの送受信・内容確認がしたく調べていたところ、
MailHogを利用するのが簡単そうでした。
導入についてメモしておきます。

環境

Docker
PHP : 8.0
Apache : 2.4
MailHog : 0.2.0

導入

ファイル構成

./
 ├ app/
 ├ env/
 │ └ php-apache/
 │    ├ Dockerfile
 │   └ php.ini
 └ docker-compose.yml

docker-compose.yml

version: '3.3'
services:
    app:
        build: ./env/php-apache
        volumes:
            - ./app:/var/www/html
        ports:
            - 80:80
    mailhog:
      image: mailhog/mailhog
      ports:
        - 1025:1025
        - 8025:8025

Dockerfile

FROM php:8.0-rc-apache
RUN apt-get update
RUN apt-get install -y vim
RUN curl -sSL https://github.com/mailhog/mhsendmail/releases/download/v0.2.0/mhsendmail_linux_amd64 -o mhsendmail
RUN chmod +x mhsendmail
RUN mv mhsendmail /usr/local/bin/mhsendmail
COPY ./php.ini /usr/local/etc/php/php.ini

php.ini

[Date]
date.timezone = "Asia/Tokyo"
[mbstring]
mbstring.internal_encoding = "UTF-8"
mbstring.language = "Japanese"
[mail function]
sendmail_path = "/usr/local/bin/mhsendmail --smtp-addr=mailhog:1025"


これらの設定を行ったうえでコンテナを起動し、

mail($to, $subject, $message, $headers);

mail関数を呼び出せばメールの送信が可能になります。

送受信したメールはブラウザで8025番ポートにアクセスすると確認ができます。

【MySQL】Date型の日付情報の一部をWHERE句の条件として利用する【DATE_FORMAT関数】

下のような日付情報を含むテーブルからWHERE句を使ってデータを抽出する際に、
年月日全体(例:2020-11-11)ではなく、年や月だけで絞り込みをする方法をメモしておきます。

id date
1 2020-11-11
2 2020-11-12
3 2020-10-10
4 2021-12-10
環境


MySQL : 8.0

解決策


DATE_FORMAT関数を使うと年や月だけで絞り込みができます。

SELECT * FROM sample_tables WHERE DATE_FORMAT(date, '%Y') = '2020';

とすれば、西暦が2020年であるデータ(id:1~3)のみが抽出できます。

id date
1 2020-11-11
2 2020-11-12
3 2020-10-10


DATE_FORMAT関数の引数には、DATE型のカラム名(例:date)とフォーマットを表現する文字列(例:'%Y')を指定します。 指定できるフォーマットは公式ドキュメントに記載されています。

MySQL :: MySQL 8.0 Reference Manual :: 12.7 Date and Time Functions

複数のフォーマットを同時に指定することも可能です。

SELECT * FROM sample_tables WHERE DATE_FORMAT(date, '%Y-%c') = '2020-11';
id date
1 2020-11-11
2 2020-11-12


日付関連の変数名について

日付関連の変数(年月日や曜日など)の命名をどうするかよく悩みます。
現状しっくりときている命名法をメモしておきます。

// 年
$year = 2020;

// 月
$month = 10;

// 日
$date = 23;

// 曜日
$day = "月";
$day = 1;

// 年月日を結合した文字列
$dateStr = "2020-11-11";
$dateStr = "2020/11/11";

// JavaScriptではDateオブジェクトも扱う
let dateObj = new Date();