20190502のPHPに関する記事は14件です。

ドメインモデル、ドメインロジックとは何かをコードを交えて考えてみる

この記事について

オブジェクト指向設計の文脈で度々目にする「ドメインモデル」や「ドメインロジック」というものが具体的に何を指しているのか、ドメイン以外のモデルってあるの?とか、じゃあアプリケーションロジックってなんだろう、というようなことをコードを交えて定義してみようという試みです。

はじめに

コード例は、マルチパラダイムなプログラミング言語 PHP で書きます。

対象読者は「ドメインが何を指すのかよく分からない」と思っている方ですが、オブジェクト指向の猛者の方にも間違いがないか見てもらいたいので読んでほしいです。

そもそも「ドメイン」とは

語義

MACMILLAN DICTIONARY によると

a particular area of activity or life
domain (noun) American English definition and synonyms | Macmillan Dictionary

と定義されています(「活動や生活における特定の領域」)。

ひとまず「領域」と定義してよさそうです。

ソフトウェア開発以外でのドメイン

本題に入る前に、ソフトウェア開発以外でドメインという言葉がどんな意味や用法で使われているかを見てみます。

インターネットドメイン

ネットワーク上の複数のコンピューターを管理するためのグループや組織を表す言葉。
ドメインとは - コトバンク

.jp とか qiita.com みたいなやつですね、ネットワーク上にあるコンピュータの識別情報として使われます。元々は www.qiita.com などとドメインの前にホスト名をつけてウェブサーバーを明示していましたが、最近ではホスト名を省略するケースが多いようです。

「グループや組織」ということで、こちらも「領域」と捉えることができそうです。

事業ドメイン

企業の持続的な成長を可能とする自社特有の事業活動領域のこと
ドメイン | 用語解説 | 野村総合研究所(NRI)

会社に所属しているひとであれば、あなたの会社の事業ドメインはなんですか、という問いに答えらたほうがいいかもしれません。
ようするにどんな活動で収益を上げていますか、ということですかね。

すでに上の定義に「事業活動領域」とあるので、こちらも「領域」を指しています。

数学におけるドメイン

数学における写像の定義域(ていぎいき、英: domain of definition)あるいは始域(しいき、英: domain; 域, 領域)とは、写像の値の定義される引数(「入力」)の取り得る値全体からなる集合である。
定義域 - Wikipedia

集合という言葉が登場しました。

詳しく知りたい方は「定義域」などでぐぐっていただくとして、ここでは「写像」についてのみ補足します。

写像は関数の一種で、入力値の集合 X に対して、何らかの関数 f を用いて変換したとき、X のそれぞれの要素に対応した出力値の集合 Y の要素が定まるようなとき、関数 f は写像である、といいます。

以下は Elixir で表現した写像の例です(最もシンプルにわかりやすく書けそうだったので選びました)。

Enum.map([1, 2, 3], fn x -> x * 2 end)
# [2, 4, 6]

始域([1,2,3])に対して終域([2,4,6])が定まるような関数 fn x -> x * 2 end (入力パラメータを2倍して出力する)は写像です。
(入力値の集合を「始域 (=domain) 」というのに対し、出力値の集合を「終域 (=codomain)」といいます)

つまり、数学におけるドメインとは、なんらかの関数に与える入力値の集合である、といえそうです。

(ここらへん理解が曖昧なので間違っていたらどなたか補足をお願いします)

ソフトウェア開発におけるドメイン

さて、ようやく本題です。

ソフトウェア開発におけるドメインという言葉の初出は寡聞にして知らないんですが、けっこう昔から使われている用語な気がします(少なくとも私が最初に読んだオブジェクト指向について書かれた本「Multi-Paradigm Design for C++ - James O. Coplien」では使われてたと記憶しています。1998年出版)。

本記事ではひとまず有名な Eric Evans の「ドメイン駆動設計」から抜粋します。

A sphere of knowledge, influence, or activity.
Evans, Eric. Domain-Driven Design

日本語では「知識、影響、または活動の領域」となります。

Evans は "sphere" という単語を使っているのでついでにこちらの意味も調べてみましょう。

a particular area of interest, activity, work etc that is one of many parts of life
sphere (noun) American English definition and synonyms | Macmillan Dictionary

「生活の一部となっているような関心事、活動、仕事などの特定の領域」みたいなかんじでしょうか。

"sphere" の定義自体にも "activity" が含まれていますが、"domain" の定義にも含まれていて、なにやら "activity"(= 活動)という概念ががとても重要な気がしてきました。

ここまでのまとめ

  • 「ドメイン」とは、なんらかの「領域」であり、その範囲は使われる文脈によって異なる。
  • ソフトウェアにおけるドメインは、事業ドメインと数学におけるドメインになんか近そう
  • activity (活動)っていう単語/概念がなんか重要そう

といったところでしょうか。

個人的な理解

ソフトウェア開発におけるドメインは、そのソフトウェアがなにをするためのものなのか、という定義のうち、ウェブとかデータベースとかメールとか、そういう外部のソフトウェアや決まりごと(HTTPとかSQLとかSMTPとか)の無関係な部分、なのかな、というのがざっくりした私の理解です。

コードを使って表現してみる

さて、「ドメイン」の定義がなんとなくわかったところで、それを表現してみることにします。

おそらくこれらの定義を何回読み返しても、概念的なことを完全に理解するのは難しいんじゃないかっていう印象で、実際に用例を用いて補ってやる必要があるんじゃないかな、と思います。

本来ならダイアグラムなどを用いて、概念モデルで表現するべきなんでしょうけど、図を書くのがめんどくさいのですっ飛ばしてコードで表現することにします。

お題

複数の異なる用例があったほうがいいと思うので、2つの毛色の異なるサービスをお題にあげようと思います。

  1. メディア(Qiita)
  2. 転職支援サービス(Qiita Jobs)

(注:Qiita Jobs は一部の機能しか使ったことないので、データや振る舞いの部分を想像で補っている部分があります。Qiita 本体に関しても言えることですが、実際のモデルと異なることをご了承ください)

ドメインモデル

オブジェクト指向におけるモデルとは何か、という説明には以下のページに簡潔に記述がありましたので引用します。

モデルとは、何らかの実体があって、それを縮小(あるいは拡大)して細かい部分は省いて本質的なところだけを抽出して形成した形のことである。
オブジェクト指向モデル

「何らかの実体」というのがややミスリーディングなかんじもしますが、ある概念の中から、アプリケーションの中で扱うデータや処理を必要なところだけ抜き出して表現する、という解釈でよさそうです。

(他によい定義や解釈があれば補足をお願いします :bow:

後述しますが、たとえば、「記事」という概念(この場合は実体も存在しうるが、実体がなくても概念として成立すればモデルはつくれるでしょう)を扱うアプリケーションをつくろうとしたとき、その「記事」という概念にはどんな属性があれば成立するでしょうか。

また、それがウェブアプリケーションであろうが、CUI プログラムであろうが変更されない部分、インタフェースやデータ形式に依存しない部分はどこでしょうか。

そういった点に注意しながら、ドメインモデルを探索してみましょう。

個人的には、たとえドメインモデルであってもアプリケーションとして実装されることが前提であり、データベースやフレームワークと完全に切り離して考えることは意味がないとは思っていますが、モデリングの訓練や探求自体には意味はあると思っていて、概念を抽出して名前をつけたり、処理をどの単位で区切るかを考えて名前をつけたり、といったことは役に立つと思っています。

メディアにおけるドメインモデル

メディア(Qiita)におけるドメインモデルには、以下のようなものがありそうです。

  • 記事
  • 執筆者
  • タグ
  • いいね
  • ストック
  • コメント
  • 編集リクエスト

記事を例にとってみます。

Qiita の記事の特徴は、

  • 特定の執筆者にひもづいている(匿名では投稿できない)
  • タイトル、本文、タグ、などの属性がある
  • 編集、削除ができる
  • 限定共有投稿、公開を切り替えられる(ただし、一度公開した記事を非公開にはできない)
  • ページビュー、いいね、ストックの数がひもづいている
  • 最終更新日時から1年以上経過すると「この記事は最終更新日から○年以上が経過しています。」と表示される

といったところでしょうか。

いくつかデータと振る舞いをピックアップして、モデルを表現してみます。

上記のリストから単語を拾って、プロパティおよびメソッドにしてみます。

class Article {
    private $author;
    private $title;
    private $body;
    private $tags;
    private $lastUpdatedAt;

    public function __construct(
        Author $author,
        string $title,
        string $body,
        array $tags
    ) {}

    public function edit(string $title, string $body, string $tags) {}
    public function delete() {}
    public function sharePrivately() {}
    public function publish() {}
}

記事は、執筆者とタイトル、本文、タグがないと公開できないので、日時系のデータを除き、すべてをコンストラクタに渡すようにしました。

編集できるのはタイトルと本文、およびタグなので、 edit メソッドにはその3つを渡すようにしています。

それ以外のメソッドはいずれも引数なしです。

private と public という単語があるので、どうやら公開か非公開か、みたいな状態もプロパティとして持っておいたほうがよさそうなかんじがします。


...といったようなかんじでドメインモデルを表現してみました。

ここまで、URLとかユーザーとか、ウェブに関する単語や、データベースに保存する、みたいな概念を入れずにクラス定義をしました。

簡単ではありますが、ドメイン(Qiita というメディア)において、記事というモデルが、どんなデータを持ち、どんな振る舞いを持つのか、という点だけにフォーカスして表現可能である、ということは分かっていただけたのではないかと思います。

のちほどドメインロジックのところで、以下の2点を Qiita のドメインにおける特徴的な振る舞いとして取り上げようと思います。

  • 限定共有投稿、公開を切り替えられる(ただし、一度公開した記事を非公開(=限定共有)にはできない)
  • 最終更新日時から1年以上経過すると「この記事は最終更新日から○年以上が経過しています。」と表示される

転職支援サービスにおけるドメインモデル

続いて転職支援サービス(Qiita Jobs)では、こんなかんじです。

  • 開発チーム
  • 求職者
  • 募集職種
  • チャット

こちらのドメインでは、開発チームと求職者の関係性(協調)についてモデル化してみようと思います。

まずはデータ部分です。

class DeveloperTeam {
    /** Member[] */
    private $members;
}

class Member {
    private $name;
    private $teamId;
}

class JobSeeker {
    private $name;
}

class Candidate {
    /** JobSeeker */
    private $seeker;
    private $teamId;
}

class Chat {
    /** Message[] */
    private $messages;
}

class Message {
    private $content;
}

これらのモデルには以下の特徴があります。

  • JobSeeker は DeveloperTeam との Chat を開始することができる
  • JobSeeker は 特定の DeveloperTeam と Chat を始めると、Candidate となり、Team とのひもづけが行われる
  • Chat は、JobSeeker 側からしか開始できない
  • Candidate および DeveloperTeam の Member は任意のタイミングで Chat に投稿できる

では、これらの特徴をモデルに落とし込んでみます(実装は省略)。

class JobSeeker {
    private $name;

    public function startChatWith(DeveloperTeam $team): Chat {}
}

class Chat {
    /** Candidate */
    private $candidate;
    /** DeveloperTeam */
    private $team;
    /** Message[] */
    private $messages;

    public function post(Message $message): void {}
}

上のコードで表現できてないことで、ひとつ大事なことを忘れていました。

メッセージを書き込むアクターをどう表現するか、です。

Candidate および Member は任意のタイミングで Chat に投稿できる

とありますので、Candidate と Member 両方が Message の作成者になれそうです。

interface を使って表現してみます。

interface ChatParticipant {
    public function id(): int;
    public function name(): string;
}

class JobSeeker implements ChatParticipant
{
}

class Member implements ChatParticipant
{
}

class Message {
    private $content;
    /** ChatParticipant.id */
    private $creatorId;
}

Chat::post() のインタフェースもちょっと変更します。

class Chat {
    public function post(string $message, ChatParticipant $from): void {
        // メッセージを追加する処理はこんなかんじになりそう
        // $this->addMessage(new Message($message, $from->id()));
    }
}

Message の生成をだれにやらせるかというのを悩みますが、ひとまず Chat 内で生成するようにします。

お気づきと思いますが、JobSeeker と Member という異なるクラスで Message::createId に入る値を ChatParticipant で一意にしなければならないのが問題で、ここらへんはデータベースが絡んでくることになりそうですが、ChatParticipant の実態がアプリケーションのユーザーになったとしても型自体は変わらないので、上のコードを変える必要はなさそうです。

このへんは他にもいくつかやり方ありそうなので、こんな構造にしたほうがよさそう、みたいなご意見があれば、コメントいただけると助かります。

ここまでのまとめ

メディア(Qiita)、転職支援サービス(Qiita Jobs)という2つのドメインについて、それら固有のデータや振る舞いをそれぞれプロパティやメソッドとしてモデル化してみました。

対象のアプリケーションでやりたいことや扱いたい情報を言語化し、その中から名詞や動詞を抜き出してプロパティやメソッドにしていく作業を何度も行って、洗練されたモデルが手に入るんだと思いますが、もしやったことなければ自分が携わっているサービスでもこういった活動をやってみるといいのではないでしょうか。

ドメインロジック

ドメインロジックとはなにか、を考える前に、ドメインモデルの中で表現したデータ以外の部分を振り返ってみます。

たとえば、メディアのモデルにあった「限定共有投稿」という概念を sharePrivately というメソッド名で表現しました。

これはあくまでも名前なので、実際の振る舞いが「限定共有」になっていなければ偽りの名前になってしまいます。

また、転職支援サービスのモデルにあった「Chat は、JobSeeker 側からしか開始できない」という制約を JobSeeker::startChatWith() というメソッドで表現しましたが、 new Chat() を呼んでしまえば、どのクラスからでも生成できてしまいます。

さて、上記を踏まえた上で、ドメインロジックとは何か、を考えてみます。

プログラムにおける処理の内容、手順、方法のこと。
ロジック - Wikipedia

MVC なウェブアプリケーションにおける標準的な「処理の内容」と「手順」は、以下のような感じになると思います。

  1. Controller が Request から入力を受け取る
  2. Model が入力を元になんらかのデータを構築したり変更したりする
  3. Controller が Model の振る舞いを元に View を構築する
  4. View のデータ(HTML)を Response として返す

この手順はウェブアプリケーション固有のものなので、ドメインロジックではありませんが、その他にもデータベースやメールといった外部のソフトウェア(やプロトコル、データ形式など)に関連するもの以外はドメインロジックと呼んでしまってよさそうです。

(こちらも他によい定義があれば補足をお願いします :bow:

メディアにおけるドメインロジック

以下の2つについて、ロジックを考えてみます。

  • 限定共有投稿、公開を切り替えられる(ただし、一度公開した記事を非公開(=限定共有)にはできない)
  • 最終更新日時から1年以上経過すると「この記事は最終更新日から○年以上が経過しています。」と表示される

まずは「一度公開した記事を非公開(=限定共有)にはできない」の部分からです。

呼び出し側では、こんなかんじで使うことになると思います。

$article = $articles->find($articleId);
$article->sharePrivately();

このとき、すでに公開されている記事に対して呼び出された場合には例外を送出するとします。

public function sharePrivately() {
    if ($this->isPublished()) {
        throw new InvalidStateException('すでに公開されている記事に対しては限定共有に戻すことはできません。');
    }
}

InvalidStateException クラスはドメインで定義した例外クラスです)

  • 処理の内容は「限定共有状態にする」
  • 処理の手順は「公開状態かチェックする → 公開状態であれば状態の変更ができない旨クライアントに知らせる」
  • 処理の方法は「if 文で分岐し、例外を投げる」

となります。

続いて「最終更新日時から1年以上経過すると『この記事は最終更新日から○年以上が経過しています。』と表示される」の部分です。

これは Article クラスに処理をさせるかどうか悩むところですが、ここでは別クラスに切り出してみます。

記事の状態によって警告文を出す、という処理なので、 ArticleStateWarning とでもしておきます。

class ArticleStateWarning {
    private $article;

    public function __construct(Article $article) {}
    public function __toString() {
        $diffInYear = Carbon::today()->diffInYear($this->article->lastUpdatedAt();
        if ($diffInYear < 1) {
            return '';
        }
        return "この記事は最終更新日から{$diffInYear}年以上が経過しています。";
    }
}
  • 処理の内容は「警告文を生成する」
  • 処理の手順は「本日と記事の最終更新日との差分を年単位で計算する → 1未満であれば空文字、1以上であれば警告文を生成する」
  • 処理の方法は「Carbon ライブラリを使って差分を計算し、if 文で文字列の生成を分岐させる」

となります。

これらの条件文に書かれたルールはドメイン固有のものであり、他のメディアでもし同様の処理があったとしても、必ずしもルールが一致するとは限りません。

また、クライアントのクラスにどのような形で結果を返すか、というのも、ロジックの一部といっていいでしょう。

転職支援サービスにおけるドメインロジック

同じような要領で転職支援サービスにおけるドメインロジックも考えてみます。

  • Chat は、JobSeeker 側からしか開始できない

前述の

「Chat は、JobSeeker 側からしか開始できない」という制約を JobSeeker::startChatWith() というメソッドで表現しましたが、 new Chat() を呼んでしまえば、どのクラスからでも生成できてしまいます。

という部分について、もうちょっと深掘りしてみましょう。

おそらく中身はこんなイメージになると思います。

// JobSeeker
public function startChatWith(DeveloperTeam $team): Chat {
    $candidate = new Candidate($this, $team);
    $chat = new Chat($candidate, $team);
    // 他にも初期化処理があればここで
    return $chat;    
}

処理の流れを単純化すれば「 Chat を生成しそれを返す」だけですが、いちおうこれまでと同様に3つの属性を書いておきます。

  • 処理の内容は「Chat を生成する」
  • 処理の手順は「JobSeeker から Candidate を生成する → Candidate と DeveloperTeam から Chat を生成する」
  • 処理の方法は「それぞれのオブジェクトは new で生成する」

となります。

では、さらにこの処理を呼ぶであろうクライアント、Controller はどうなるでしょう。

// CreateChatController
// POST /chats
public function __invoke(Request $request) {
    // session か token か分からないがログインしているユーザーを取得する
    $user = Auth::user();
    // User にひもづいている JobSeeker を探す
    $jobSeeker = JobSeeker::ofUser($user)->findOrFail();
    // DeveloperTeam.id はリクエストから渡ってくる
    $team = Team::findOrFail($request->team_id);
    // Chat を生成する
    $chat = $jobSeeker->startWithChat($team);
    // レスポンスを返す(JSON形式を想定)
    return new Response($chat);
}

(ちなみに loadUser() やリクエストやらレスポンスやらが絡んでいる部分はアプリケーションロジックです)

これを見ると、JobSeeker のインスタンスはユーザーから取得できるようになっているので、やろうと思えば開発チームのコンテキスト(開発チーム専用のアプリケーションまたは Controller クラスがあるはずです)からでも Chat の生成ができてしまうでしょう。

これを阻止するためにはどうすればいいでしょう?

ひとつ思いついたのは、完全に有効な手段ではないんですが、コンテキストを名前空間で分けて、求職者のコンテキストでは生成可能とし、開発チームのコンテキストでは生成不可とする、というやり方です。

namespace Domain\Models;

class Chat {
    protected function __construct(Candidate $candidate, DeveloperTeam $team) {}
}

namespace JobSeeker\Models;

class Chat extends BaseChat {
    public function __construct(Candidate $candidate, DeveloperTeam $team) {}
}

namespace Member\Models;

class Chat extends BaseChat {
}

PHP ではパッケージ(名前空間)でアクセスを制限したりはできないので、 Member コンテキストから JobSeeker コンテキストの Chat クラスを使われたらこの目論見は突破されてしまいます。

まぁ、下手するとせっかく protected で宣言していても、意図がちゃんと伝わってないと public に変更されてしまったりするので、何事も完璧というわけにはいかないですね、そのへんはチームでいいかんじにバランスを取ればいいのではないかと思います。

ここまでのまとめ

ドメインロジックは「処理の内容、手順、方法」を説明できるものであるということと、使用する言語によって取れる手段が限られるので、その中からより良い選択ができるかどうか、というのがキモ、という気がします。

つくろうとしている/つくっているアプリケーション固有の概念のうち、セッションやデータベース、HTTPの世界の概念などと関係ない部分がドメインのロジックである、という説明はもうちょっと洗練できるんじゃないかという予感はありつつ、現時点では私の精一杯のものなので、ひとまずこれでご容赦いただければと思います。

おわりに

いかがでしたでしょうか?

「ドメイン」てのがよく分からん、という方

「ドメイン」「ドメインモデル」「ドメインロジック」という言葉/概念の指すものがなんとなく分かったでしょうか?

まだ分からん、ということであれば直接補足しますので、Twitter にてメンションください、よろしくお願いします(その結果腹落ちしてもらえたら、この記事に反映できればいいですね)。

他にも「ドメイン」とはこういうものだ、という意見をお持ちの方

ぜひコメント欄にてご意見お聞かせください。

参考書籍

  • ドメイン駆動設計 エリック・エヴァンス
  • オブジェクト指向設計実践ガイド Sandi Metz
  • ユースケース駆動開発実践ガイド ダグ・ローゼンバーグ, マット・ステファン
  • 現場で役立つシステム設計の原則 増田 亨
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP初心者向け】カプセル化とアクセス権について解説!

はじめに

  • カプセル化とアクセス権について本やネットの情報から調べて理解したことをまとめました。
  • もし、書いていることに何か間違いがある場合はご指摘いただけると嬉しいです。

カプセル化とは

  • プロパティの値を変更されてしまわないように、他のクラスからのアクセスを制限すること。
  • プロパティをカプセルで覆うことで、触れられないようにするようなイメージ。

アクセス権とは

  • プロパティとメソッドへアクセスできる範囲を制限すること
  • public、protected、privateをアクセス修飾子という

アクセス修飾子の種類

アクセス修飾子 アクセスできる範囲
public どこからでもアクセス可能
protected そのクラス自身、継承したクラス、親クラスからのみアクセス可能
private そのクラスからのみアクセス可能

アクセス権のあるプロパティを操作するメソッド

  • getter(ゲッター)メソッドsetter(セッター)メソッドを使うことで、アクセス権があるプロパティを操作することができる。

getter(ゲッター)

  • アクセス権があるプロパティの値にクラスの外から取得するメソッド
  • 「getプロパティ名」のように書く。

コード例

<?php

class Products
{
    private $name = "パソコン";

    public function getName(){
        return $this->name;
    }
}

$product = new Products();

echo $product->getName();

?>
実行結果
パソコン

setter(セッター)

  • アクセス権があるプロパティの値にクラスの外からセット(設定)するメソッド
  • 「setプロパティ名」のように書く。

コード例

<?php

class Products{

    private $name;

    public function getName(){
        return $this->name;
    }

    public function setName($name){
        $this->name = $name;
    }
}

$product = new Products();

$product->setName("パソコン");

echo $product->getName();

?>
実行結果
パソコン
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者】PHPをブラウザで表示する方法

未来電子テクノロジー株式会社でインターンをしている大学一回生です。
PHPをブラウザで表示するのが難しかったのでアウトプットします。

問題点

ドッドインストールの、ローカル開発環境の構築(mac編)を参考にし、Virtualbox(6.0.6)とvagrant(2.2.4)とCyberduckをインストールし、PHPをブラウザで表示しようとしたが、Cyberduckの新規接続ができない。また、できたとしてもブラウザに表示できない。

解決策

1.Vagrantfileを編集する。

# -*- mode: ruby -*-
# vi: set ft=ruby :

# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
  # The most common configuration options are documented and commented below.
  # For a complete reference, please see the online documentation at
  # https://docs.vagrantup.com.

  # Every Vagrant development environment requires a box. You can search for
  # boxes at https://vagrantcloud.com/search.
  config.vm.box = "bento/centos-6.8"

  # Disable automatic box update checking. If you disable this, then
  # boxes will only be checked for updates when the user runs
  # `vagrant box outdated`. This is not recommended.
  # config.vm.box_check_update = false

  # Create a forwarded port mapping which allows access to a specific port
  # within the machine from a port on the host machine. In the example below,
  # accessing "localhost:8080" will access port 80 on the guest machine.
  # NOTE: This will enable public access to the opened port
  # config.vm.network "forwarded_port", guest: 80, host: 8080

  # Create a forwarded port mapping which allows access to a specific port
  # within the machine from a port on the host machine and only allow access
  # via 127.0.0.1 to disable public access
  # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"

  # Create a private network, which allows host-only access to the machine
  # using a specific IP.
   config.vm.network "private_network", ip: "192.168.33.11"

  # Create a public network, which generally matched to bridged network.
  # Bridged networks make the machine appear as another physical device on
  # your network.
  # config.vm.network "public_network"

  # Share an additional folder to the guest VM. The first argument is
  # the path on the host to the actual folder. The second argument is
  # the path on the guest to mount the folder. And the optional third
  # argument is a set of non-required options.
  # config.vm.synced_folder "../data", "/vagrant_data"

  # Provider-specific configuration so you can fine-tune various
  # backing providers for Vagrant. These expose provider-specific options.
  # Example for VirtualBox:
  #
  # config.vm.provider "virtualbox" do |vb|
  #   # Display the VirtualBox GUI when booting the machine
  #   vb.gui = true
  #
  #   # Customize the amount of memory on the VM:
  #   vb.memory = "1024"
  # end
  #
  # View the documentation for the provider you are using for more
  # information on available options.

  # Enable provisioning with a shell script. Additional provisioners such as
  # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
  # documentation for more information about their specific syntax and use.
  # config.vm.provision "shell", inline: <<-SHELL
  #   apt-get update
  #   apt-get install -y apache2
  # SHELL
end

上のコードの、「#」がはずれている、

config.vm.network "private_network", ip: "192.168.33.11"

の部分を編集しました。
ちなみにドットインストールでは、

config.vm.network "private_network", ip: "192.168.33.10"

でした。
参照(https://sazaijiten.work/docker_virtualbox_vagrant/)

2.Cyberduckで新規接続

スクリーンショット 2019-05-02 18.00.49.png

サーバは、先ほど変更した数字、ポートは22(ポートがなぜ22なのかは分からないです)、ユーザ名とパスワードはvagrantです。

3.ターミナルで操作

ターミナルでCyberduckで作ったファイルに入り、「ip a」を入力。
スクリーンショット 2019-05-02 21.03.25.png
最後から3行目に「192.168.33.11」と表示されている。
次に、「php -S 192.168.33.11:8000」を入力し、(なぜ:8000なのかは分からないです)、ブラウザで「http://192.168.33.11:8000」と検索。
スクリーンショット 2019-05-02 21.11.25.png
これでphpをブラウザで表示できます。

まとめ

最後にブックマークをすると、phpで作った複数のファイルがブラウザで表示できるようになりました。
スクリーンショット 2019-05-02 21.15.35.png

プログラミングを始めて一ヶ月も経っておらず、不備があるかもしれないので、どうぞご指摘ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[忘備録]laravel5.5 データベースjoin 結合

laravelを用いて初めてデータベースの結合をしてみると、想像以上に簡単だったので、忘備録として残しておこうと思う。

前提

データベースにlaravelデフォルトの users に加え、  id, uesr_id, text カラムを含む posts テーブルを用意する。

postsテーブルを作成する際はマイグレーションファイルで user_id カラムを次のように設定する。

$table->integer('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('users');

流れ

①モデル設定
②コントローラーから使う

①モデル設定

今回はユーザーが複数のpostを持つというで、以下のような関係になります。

user has many posts
posts belongs to user

この点を踏まえて、 User.php, Post.php に以下のコードを追記。

User.php
public function posts() {
        return $this->hasMany('App\Post');
    }
Post.php
public function user() {
        return $this->belongsTo('App\User');
    }

これで準備は整いました。

②コントローラーから使う

今回はコントローラーで確認用 unction join() という関数を作ります。

Post.php
function join() {
        #postsテーブルのuser_idが1のレコードを全て取得
        $user = User::find(1);
        $posts = $user->posts; 

        #postテーブルのデータを全て取得
        $posts = Post::all();

        #上記で取得した「postsテーブルのuser_idが1のレコード」をひとつずつ表示
        foreach ($posts as $post) {
            echo $post . "<br/>";
        }

        #上記で取得した「postテーブルのデータ」から結合されたユーザーの名前をひとつずつ表示する。
        foreach ($posts as $post) {
            echo $post->user->name . "<br/>";
        }

    }

これで、適当にルート設定をし、うまく表示されていることを確認することができました。

おかしな部分がある場合、指摘していただけるとありがたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nginx php ubuntu備忘録

php7.2インストール

ninja@localhost:~$ sudo apt-get update
ninja@localhost:~$ sudo apt-get upgrade
ninja@localhost:~$ sudo apt-get install software-properties-common
ninja@localhost:~$ sudo add-apt-repository ppa:ondrej/php
ninja@localhost:~$ sudo apt-get update
ninja@localhost:~$ sudo apt-get install php7.2
ninja@localhost:~$ sudo apt-get install php7.2-mbstring php7.2-mysql php7.2-xml php7.2-gd php7.2-zip php7.2-fpm

apache削除

ninja@localhost:~$ sudo apt-get purge apache2 apache2-utils apache2-bin apache2-data
ninja@localhost:~$ sudo apt-get autoremove
/* apache2のフォルダの場所を探す。 */
ninja@localhost:~$ whereis apache2
ninja@localhost:~$ sudo rm -rf /etc/apache2

nginxインストール

ninja@localhost:~$ sudo apt-get install nginx

php7.2(開発中にエラーがでたから入れたやつ)

ninja@localhost:~$ sudo apt-get install php-gmp

php7.2設定

ninja@localhost:~$ sudo vi /etc/php/7.2/cli/php.ini
post_max_size = 10M
cgi.fix_pathinfo=0
upload_max_filesize = 10M


ninja@localhost:~$ sudo vi /etc/php/7.2/fpm/php.ini
post_max_size = 10M
cgi.fix_pathinfo=0
upload_max_filesize = 10M

nginx設定

/etc/nginx/sites-available/default.
ninja@localhost:~$ sudo vi /etc/nginx/sites-available/default

/* 全て消して書き換える*/
server {
        listen 80 default_server;
        listen [::]:80 default_server ipv6only=on;
        root /var/www/html;
        index index.php index.html index.htm;
        #server_name XXX.net;

        location / {
                try_files $uri $uri/ /index.php?$query_string;
        }
        location ~ \.php$ {
                try_files $uri =404;
                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                include fastcgi_params;
        }
}
/etc/nginx/nginx.conf
ninja@localhost:~$ sudo vi /etc/nginx/nginx.conf
# httpの中に追加以下を追加
client_max_body_size 10m; # defaule 1m

nginxの動作確認

ninja@localhost:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

ninja@localhost:~$ sudo service php7.2-fpm restart
ninja@localhost:~$ sudo service nginx restart

ninja@localhost:~$ sudo ufw app list
Available applications:
  Nginx Full
  Nginx HTTP
  Nginx HTTPS
ninja@localhost:~$ sudo ufw allow 'Nginx Full'

ブラウザから動作確認
XXX.XXX.XXX.XXX/info.phpにアクセスして表示されればOK

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC-CUBE 3でコードを編集してページを作成する

はじめに

EC-CUBEで固定ページを作成するときに、管理画面経由で作成すると、user_dataというURLが付いちゃうのと、データベースの値に左右されてしまう点が気になったので、そもそもコントローラとか追加したらページ作成できるのではないかと思って試してみました。
(あと地味にURLの最後をスラッシュで終われない点も気になりました)

こちらでのメリットとしては、プログラム側に記述されているので、データベースの値を併せなくてもページ自体は表示できるので、Gitなどでファイルを共有すればページも閲覧できるようになる点かと思います。

注意点

下記の流れはEC-CUBE3のコアファイルを編集しています。「とりあえずページ追加したい」という方以外は下記の流れで作らない方が良いと思います。

ルーティング

ルーティングは、src/Eccube/ControllerProvider/FrontControllerProvicer.phpで操作しているようです。
中身はSilexのルーティングの記述ですね。

例えば、

  • /foo/ でアクセスできるページを
  • src/Eccube/Controller/StaticPageController
  • foo メソッドから呼び出して
  • foo という名前でurl関数から呼び出せる

様にする場合下記のような記述になります。

$c->match('/foo/', '\Eccube\Controller\StaticPageController::foo')->bind('foo');

特にこの記述がコアファイルに直接記入してしまっているのが問題です。他にいい方法がアレば…。

プラグインでも良いんですけど、結局インストールと有効化することでDBに依存してしまうのが気になる。

コントローラ

次にStaticPageControllerを作ってみます。

今回は単純に静的なページを追加するイメージで考えていますので、単純にビューを呼び出すだけの記述になりますが、特定カテゴリーの商品の一覧とかを表示させたい場合は、ここから必要なRepositoryを呼べばよいはずです。

<?php
namespace Eccube\Controller;

use Eccube\Application;

class StaticPageController extends AbstractController
{
    public function foo(Application $app)
    {
        return $app->render('Static/foo.twig');
    }
}

これで、Static/foo.twigが読み込まれるようになりました。

ちなみに、上記はコアファイル配下に設置していますが、composer.jsonにautoloadされるディレクトリを追加して、別の場所にコントローラを追加してしまえば、コアファイルとの切り離しも可能だと思います。

ビュー

ビューファイルはコアファイルに含める必要はありません。
app/template/default/Static/foo.twigを作成すれば、src配下のファイルよりも優先されて読み込まれます。

なお、管理画面上から追加していないので、titleやmeta情報などは少し強引に設定する必要があります。

{# titleに値を入れれば<title>追加される #}
{% set title = 'ページタイトル' %}

{# レイアウトテンプレートを指定 #}
{% extends 'default_frame.twig' %}

{# メタ情報を挿入する #}
{% block meta_tags %}
<meta name="description" content="ページの説明ほげほげ">
{# OGP情報とか、canonicalとか #}
{% endblock %}

{# CSSを読み込む #}
{% block stylesheet %}
<link rel="stylesheet" href="{{ app.config.front_urlpath }}/path/to/foo.css" type="text/css" media="all">
{% endblock %}

{# JavaScriptを読み込む #}
{% block javascript %}
<script src="{{ app.config.front_urlpath }}/path/to/foo.js"></script>
{% endblock %}

{% block main %}
{# メインコンテンツエリア、ここに色々記述する #}
{% endblock %}

これで/foo/にアクセスるとページが表示されるようになっているはずです。

終わりに

とりあえずは思ったとおりにページが作れて満足です。(他にもっといい方法がありそうですけども)
EC-CUBE4ではこういった要望も満たされてると良いんですが調べていません、どうなんでしょうね?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

5/2 PHP CookieとSessionの使い方

クッキーの作成と呼び出し

・setcookieでcoolieを作成
・var_dump($_COOKIE)で呼び出し
・time()+3600で有効期限の指定

 <?php
setcookie("name", "Tanaka"time()+3600);
var_dump($_COOKIE);
?>

Session

サーバーに保存される一時データ

sessionの開始>session_start();

<?php
session_start();
$_SESSION["name"] = "Suzuki";
var_dump($_SESSION);
?>

その他必要な関数

unset関数
引数に渡した変数を開放する。配列+キーを指定することでそのキーのみ開放されます。
session_unset関数
$SESSION内の全ての変数を開放します。
session_destroy関数
$
SESSION自体を破棄してしまいます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

5/2 PHP webサイトでのデータ送信メソッド

画面遷移のやり方

前提知識

GET ※よく使う URLで指定した内容をサーバーからもらう アドレスバーに内容が出る
POST ※よく使う URLで指定した内容をサーバーからもらう アドレスバーに内容が出ない
PUT
DELETE
HEAD
OPTIONS
TRACE
CONNECT

GETメソッドの例

ファイル1

<!DOCTYPE html>
<html lang="utf-8">
<head>
    <meta charset="utf-8">
    <title>Send username</title>
</head>
<body>
    <form action="test.php" method="GET">
        <input type="text" name="last_name" placeholder="苗字を入力してください">
        <input type="text" name="first_name" placeholder="名前を入力してください">
        <input type="submit" value="送信">
    </form>
</body>
</html>

ファイル2

<?php
$last_name = "";
$first_name = "";
if ($_SERVER['REQUEST_METHOD'] === "GET") {
    $last_name = $_GET["last_name"];
    $first_name = $_GET["first_name"];
}
?>
<!DOCTYPE html>
<html lang="utf-8">
<head>
    <meta charset="utf-8">
    <title>Send username</title>
</head>
<body>
<?php echo "苗字 : ".htmlspecialchars($last_name).PHP_EOL; ?>
<?php echo "名前 : ".htmlspecialchars($first_name).PHP_EOL; ?>
</body>
</html>


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

5/2 PHPの例外処理

例外処理

ポイント

・if で例外を定義 throw new Exceptionで処理内容を記述

<?php
 function say_hello($name)
  {
   try{if (empty($name))
     {
       throw new Exception("no Name!");
       }
       echo "Hello!".$name>PHP_EOL;
    }
  catch (Exception $e) {echo $e ->getMessage();
  }
 }
say_hello("")
 ?>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

5/2 PHP オブジェクト操作における定義ファイルの読込

オブジェクト操作における定義ファイルの読込

…オブジェクト指向で一回一回定義をファイルに打ち込むのは面倒
 そこで、定義ファイルを作成し、それを読み込ませれば良いのだわ

<?php
require "user.php";
// require_once "user.php";
$user_obj = new User("Tanaka");
$user_obj->say_hello();
?>

・ここでは別途用意したuser.phpファイルを読み込んでいる。

読み込の操作の一覧

require
require_once
include
include_once
autoloader(クラスにのみ使える)

・require_onceは読込をスキップしてくれる。とりあえずはonceを使うべし
・requireとincludeは

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

5/2 PHP オブジェクト操作

オブジェクト操作における定義ファイルの読込

…オブジェクト指向で一回一回定義をファイルに打ち込むのは面倒
 そこで、定義ファイルを作成し、それを読み込ませれば良いのだわ

<?php
require "user.php";
// require_once "user.php";
$user_obj = new User("Tanaka");
$user_obj->say_hello();
?>

・ここでは別途用意したuser.phpファイルを読み込んでいる。

読み込の操作の一覧

require
require_once
include
include_once
autoloader(クラスにのみ使える)

・require_onceは読込をスキップしてくれる。とりあえずはonceを使うべし
・requireとincludeは

名前空間の定義

作成したクラスやメソッドをひとまとめにする

<?php
namespace Courage\User;
コードの先頭に記述
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelのtrait Macroableを使って動的にメソッドを追加する

Macroableとは

Laravelをインストールしたときに一緒にインストールされるtraitです。

vendor\laravel\framework\src\Illuminate\Support\Traits\Macroable.phpにあります。

Macroableをuseするとそのクラスは定義されたクラス以外でも動的にメソッドを追加できるようになります。

ソースそのものは108行しかないのでシンプルです。

以下メソッド

public static function macro($name, $macro)
public static function mixin($mixin)
public static function hasMacro($name)
public static function __callStatic($method, $parameters)
public function __call($method, $parameters)

仕組みとしてはmacro()でメソッド名と実行する内容を登録して、マジックメソッドの __call() か __callStatic() で使えるようにするといったものです。

hasMacro()は名前の通りmacro()で登録してあるかチェックするためのメソッドです。

mixin()は5.5から追加されました。名前で推測できるように引数にインスタンスを渡して、そのインスタンスのメソッドを使えるようにするためのものと判断出来ます。
ですが、癖が強いので使い勝手が良いかどうかは疑問です。後述します。

使い方

class Human{
    use \Illuminate\Support\Traits\Macroable;
    public function punch(){
        echo "パンチ";
    }
}
Human::macro('kick', function(){
    echo "キック";
});
$human = new Human();
$human->punch();
$human->kick();

//結果 パンチキック

Laravelで使うときはProviderで登録するのがまとまりがあって良いでしょう。

mixin()の使い方

まずはmixinメソッドがどのように定義されているか見てみましょう。

public static function mixin($mixin)
{
    $methods = (new ReflectionClass($mixin))->getMethods(
        ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
    );

    foreach ($methods as $method) {
        $method->setAccessible(true);
        static::macro($method->name, $method->invoke($mixin));
    }
}

リフレクションを使い、public,protectedのメソッドの一覧を取得してforeachでmacro()で登録する流れになっています。
ですが見てください。macroメソッドの第二引数の渡し方。

$method->invoke($mixin)

普通にメソッドの呼び出しをしています。
もしも直感的にメソッドを使うなら次のように間違って書いてしまうでしょう。

class Cat{
    public function say(){
        echo "にゃー";
    }
}

Human::macro('kick', function(){
    echo "キック";
});
Human::mixin(new Cat());
$human = new Human();

$human->punch();
$human->kick();
//$human->say(); BadMethodCallExceptionが発生する 

//結果 にゃーパンチキック

say()を呼び出していないのに にゃー と鳴いてしまってます。
say()を呼び出すとBadMethodCallExceptionになります。

どう解決しましょうか。
答えはシンプルです。

say()メソッドの返り値をクロージャーにします。

class Cat{
    public function say(){
        return function(){
            echo "にゃー";
        };
    }
}

このように定義すればsay()を呼び出してもBadMethodCallExceptionが発生しません。
結果はパンチキックにゃーの順で呼び出されます。

mixin()は無名クラスを使って登録するのがベターな気がします。

Human::mixin(new class{
    public function say(){
        return function(){
            echo "にゃー";
        };
    }
});

Macroableの使い所は?

第三者に拡張性のあるクラスを提供したい!といった場合くらいですかね。
自分で率先して使う場面というのはあんまり無いでしょう。
バグの原因になりそうです。

実際の用途としてはLaravelで定義してあるクラスを拡張するのに使用するといったところでしょうか。

余談

日付操作ライブラリのCarbonにも同じようなMacroableのようなコードがあります。

個人的にマジックメソッドの __call() や __callStatic() はソースが追いにくくなるため苦手です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelチュートリアル - 汎用業務Webアプリを作る(1/4) Laradockで環境構築

はじめに

これまで私はDjangoでいくつかのWebアプリを作ってきましたが、とある事情から今後Laravelに触れる必要が出てきました。

そこで、既存のDjangoアプリをLaravelで再現するチュートリアル記事を作成し、その過程を通してLaravelについて学ぶことにしました。

題材には、CRUD機能、検索機能、認証機能を持ち、Djangoを勉強する際にも大変お世話になった、下記Webアプリとさせていただきました。

Djangoチュートリアル - 汎用業務Webアプリを最速で作る - Qiita

チュートリアル全体の構成

  1. Laradockで環境を構築する(本記事)
  2. テーブルとCRUD画面を作る(作成中)
  3. 検索機能を作る(作成予定)
  4. 認証機能を作る(作成予定)

環境

  • macOS High Sierra 10.13.6
  • php 7.2.16
  • Laravel 5.5.45
  • PostgreSQL 9.6.2

1. Laradockで環境構築する

Laradockのコピー

プロジェクトのルートとなるディレクトリに、Laradockをコピーします。

$ git clone https://github.com/Laradock/laradock.git

laradockディレクトリ配下のenv-exampleファイルをコピーし、.envファイルを作成します。

$ cd laradock
laradock $ cp env-example .env

PostgreSQLのバージョンの指定

PostgreSQLは9.6.12を使用することにします。
PostgreSQLのDockerファイルを以下の通り編集します。

laradock/postgres/Dockerfile
FROM postgres:9.6.12

LABEL maintainer="Ben M <git@bmagg.com>"

CMD ["postgres"]

EXPOSE 5432

コンテナの起動

コンテナを起動します。

laradock $ docker-compose up -d --build workspace postgres php-fpm nginx

Laravelのインストール

Laravelをインストールします。

まず、起動したコンテナの中に入ります。

laradock $ docker-compose exec workspace bash

続いて、Laravelをインストールします。
バージョンは5.5.*にします。
また、Laravelとしてのプロジェクト名は、srcにします。

/var/www# composer create-project --prefer-dist laravel/laravel src "5.5.*"

データベースを作成する

コンテナの中に入ったまま、今度はPostgreSQLに接続し、データベースを作成します。

まず、PostgreSQLに接続します。

/var/www# psql -U default -h postgres

パスワードを入力します。

Password for user default: 

続いて、データベースを作成します。
データベース名は、sampleにします。

default=# create database sample;

PostgreSQLとの接続を終了します。

default=# \q

Laravelのタイムゾーンを日本時間にする

Laravelのタイムゾーンを日本時間にします。

src/config/app.php
<?php
return [
//略
  'timezone' => 'Asia/Tokyo',
//略
];

.envファイルの編集

Laravelの.envファイルを編集し、LaravelからPostgreSQLに接続できるようにします。

src/.env
DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=sample
DB_USERNAME=default
DB_PASSWORD=secret

最後に

以上で環境構築は完了です。

次の記事では、テーブルとCRUD画面の作成を行います。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel 簡単なアプリケーション作成(1)

Laravel 簡単なアプリケーション作成(1)

Laravelを利用して簡単なアプリケーションを作成する手順

環境

前回作成したsampleappを流用する。

作成する画面、機能の概要

ID URL 機能
1 /home トップ画面表示
2 /auth/register ユーザ登録およびメール送信
3 /auth/login Login
4 /auth/logout Logout

トップ画面の作成

http://homestead.test/home にアクセスしたらTop画面が表示されるようにする。

ディレクトリ構成

(base) mbp:sampleapp username$ tree -d -L 1
.
├── app
├── bootstrap
├── config
├── database
├── public
├── resources
├── routes
├── storage
├── tests
└── vendor

ルーティング定義追加

routes/web.php
// add
Route::get('/home', function(){
    return view('home');
});

TOP画面のHTML作成(新規作成)

resources/home.blade.php
<html>
<head>
<meta charset='utf-8'>
</head>
<body>
Hello! World!
</body>
</html>

http://homestead.test/home にアクセスしたらTop画面が表示される.

テストフレームワーク PHPUnit を試してみる

Laravelには、PHPを代表するテストフレームワークである「PHPUnit」が同梱される。
以下の2つのテストを行う。

  • トップ画面のHTTPステータスコードが200
  • トップ画面のレスポンスに"Hell! World!"の文字列が含まれていること

テストコードファイルの作成

(base) mbp:sampleapp username$ pwd
/Users/username/code/sampleapp
(base) mbp:sampleapp username$ php artisan make:test HomeTest
Test created successfully.
(base) mbp:sampleapp username$

生成されたテストファイル

tests/Feature/HomeTest.php
<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class HomeTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function testExample()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

以下に変更、追加

tests/Feature/HomeTest.php
<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class HomeTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function testExample()
    {
        $response = $this->get('/home');

        $response->assertStatus(200);
    }

    public function testBody(){
        $response = $this->get('/home');

        $response-> assertSeeText("Hell! World!");
    }
}

トップ画面のテストを実行

(base) mbp:sampleapp username$ pwd
/Users/username/code/sampleapp
(base) mbp:sampleapp username$ vendor/bin/phpunit tests/Feature/HomeTest.php
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 100 ms, Memory: 14.00 MB

OK (2 tests, 2 assertions)
(base) mbp:sampleapp username$

ユーザ登録の実装

ユーザ登録機能の実装を通じてリクエストの受信とバリデーション機能(検証、認可)に関して学ぶ。

手順概要

  1. データベース準備
  2. 認証/登録機能のコードを確認、ルーティングに追加
  3. 登録画面を作成

データベース準備

ユーザ情報を登録するテーブルを作成する。テーブル作成にはマイグレーション機能を利用する。

マイグレーション機能とはデータベースのスキーマ作成やデータ投入などをプログラムコードを使って処理する機能。

マイグレーションを行うためのファイルはdatabase/migrationsにある。

databaseディレクトリの内容

(base) mbp:sampleapp username$ tree database/
database/
├── factories
│   └── UserFactory.php
├── migrations
│   ├── 2014_10_12_000000_create_users_table.php
│   └── 2014_10_12_100000_create_password_resets_table.php
└── seeds
    └── DatabaseSeeder.php

3 directories, 4 files

マイグレーションファイル

2014_10_12_000000_create_users_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

マイグレーション実行

vagrantで仮想環境にログイン

# cd ~/Homestead
# vagrant ssh

# cd ~/code/sampleapp/

vagrant@homestead:~/code/sampleapp$ php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table
vagrant@homestead:~/code/sampleapp$

MySQLに接続して作成したテーブルを確認する

vagrant@homestead:~/code/sampleapp$ mysql --host=localhost --user=homestead --password=secret homestead
mysql: [Warning] Using a password on the command line interface can be insecure.
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 11
Server version: 5.7.25-0ubuntu0.18.04.2 (Ubuntu)

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show tables from homestead ;
+---------------------+
| Tables_in_homestead |
+---------------------+
| migrations          |
| password_resets     |
| users               |
+---------------------+
3 rows in set (0.00 sec)

mysql>

認証/登録機能をルーティングに追加

登録処理はコントローラーで処理する。コントローラーはMVCアーキテクチャを構成する要素の1つ。

サービス利用者からの入力、受信結果を返却するためのビューの選択、生成などを担う。

登録処理はRegisterControllerクラスの showRegistorationForm メソッドと registor メソッドで行う。

ルーティング定義の追加

routes/web.php
Route::get('auth/register', 'Auth\RegisterController@showRegistrationForm');
Route::post('auth/register', 'Auth\RegisterController@register');

登録画面の作成

resources/views/auth にregister.blade.php として保存

resources/views/auth/register.blade.php
<html>
<head>
<meta charset='utf-8'>
</head>

<body>
<h1>ユーザ登録フォーム</h1>
<form name="registform" action="/auth/register" method="post">
    {{csrf_field()}}
    名前<input type="text" name="name" size="30"><span>{{ $errors->first('name') }} </span><br />
    メールアドレス<input type="text" name="email" size="30"><span>{{ $errors->first('email') }} </span><br />
    パスワード<input type="password" name="password" size="30"><span>{{ $errors->first('password') }} </span><br />
    パスワード(確認)<input type="password" name="password_confirmation" size="30"><span>{{ $errors->first('password_confirmation') }} </span><br />
    <button type='submit' name='action' value='send'>送信</button>
</form>
</body>

</html>

TOP画面を変更する

resources/views/home.blade.php
<html>
<head>
<meta charset='utf-8'>
</head>
<body>
Hello!

@if (Auth::check())
    {{\Auth::user()->name}}さん
@else
    ゲストさん<br />
    <a href="/auth/register">会員登録</a>
@endif
</body>
</html>

TOP画面にアクセスし、ユーザ登録フォームが表示されることを確認する。
試しに適当な値を入力し、送信ボタンを押し、 /home のページにリダイレクトされ、ログイン後の画面が表示される事を確認する。

最後に、DBに登録されたデータを確認してみる。

vagrant@homestead:~$ mysql --host=localhost --user=homestead --password=secret homestead
mysql: [Warning] Using a password on the command line interface can be insecure.
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 16
Server version: 5.7.25-0ubuntu0.18.04.2 (Ubuntu)

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> select * from users;
+----+-----------------+----------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
| id | name            | email                | email_verified_at | password                                                     | remember_token | created_at          | updated_at          |
+----+-----------------+----------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
|  1 | xxx | xxxx.com | NULL              | xxxxx| NULL           | 2019-05-01 08:44:24 | 2019-05-01 08:44:24 |
+----+-----------------+----------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
1 row in set (0.00 sec)

mysql>

ユーザ認証

ログイン機能とログアウト機能を実装する

ルーティング追加

routes/web.php
// Login Form
Route::get('/auth/login', 'Auth\LoginController@showLoginForm');
Route::post('/auth/login', 'Auth\LoginController@login');

// Logout
Route::get('/auth/login', 'Auth\LoginController@logout');

ログインフォーム実装

resources/views/auth/login.blade.php
<html>
<head>
<meta charset='utf-8'>
</head>

<body>
<h1>ログインフォーム</h1>

@isset($mesage)
    <p style="color:red">{{$message}}</p>
@endisset

<form name="loginform" action="/auth/login" method="post">
    {{csrf_field()}}
    mailaddress: <input type="text" name="email" size="30" value="{{old('email')}}"><br />
    password: <input type="password" name="password" size="30"><br />
    <button type='submit' name='action' value='send'>Login</button>
</form>
</body>

</html>

TOP画面にログアウト機能を追加

views/home.blade.php
<html>
<head>
<meta charset='utf-8'>
</head>
<body>
Hello!

@if (Auth::check())
    {{\Auth::user()->name}}さん<br />
    <a href="/auth/logout">Logout</a>
@else
    ゲストさん<br />
    <a href="/auth/login">Login</a><br />
    <a href="/auth/register">会員登録</a>
@endif
</body>
</html>

ログアウト後の遷移先を変更する

/app/Http/Controllers/Auth/LoginController.php に以下のメソッドをオーバライドする

/app/Http/Controllers/Auth/LoginController.php
    // Override
    public function logout(\Illuminate\Http\Request $request)
    {
        $this->guard()->logout();

        $request->session()->invalidate();

        // return $this->loggedOut($request) ?: redirect('/');
        return $this->loggedOut($request) ?: redirect('/home');
    }

元は sampleapp/vendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticatesUsers.php
に定義されるメソッドだが、vendor/ 以下に直接変更を加えず、オーバライドしておこう

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む