CakePHP3 ComponentとHelperにもControllerと同様のPrefix機能を作成するPlugin

CakePHP3 ComponentとHelperにもControllerと同様のPrefix機能を作成するPluginを作成したのでメモ

CakePHP3ではPrefixをつけると、Controllerをディレクトリごとに分けて設置することができます。

自分はよく、管理画面・公開画面をPrefixで分けて作成します。

Controllerはディレクトリに分けることでスッキリするので、 Model、Component、Helperも同様に分けたい思いPluginを作成しました。

GitHub - Junkins/PrefixRegistry

機能 Componentの階層化

機能 Helperの階層化

実装してない機能 Modelは階層化すると色々と不都合がでたので、ペンディング

Setup・使い方

Components

<?php
namespace App\Controller\Admin;

use Cake\Controller\Controller;
use PrefixRegistry\Controller\PrefixComponentRegistry;

class AppController extends Controller
{

    /**
     * initialize
     */
    public function initialize()
    {
        parent::initialize();
        // you can use Components definded on
        // '/src/Controller/Component/Admin/*' OR '/src/Controller/Component/*'.
        $this->_components = new PrefixComponentRegistry($this);
    }
}

Helpers

<?php
namespace App\View\Admin;

use Cake\View\View;
use PrefixRegistry\View\PrefixHelperRegistry;

class AdminView extends View
{
     public function initialize()
     {
         parent::initialize();
         // you can use Helpers definded on
         // '/src/View/Helper/Admin/*' OR '/src/View/Helper/*'.
         $this->_helpers = new PrefixHelperRegistry($this);
     }
}

便利なところ

Prefixのディレクトリに該当のクラスが存在しなければ、通常のディレクトリ配下のクラスを探しに行きます。

CakePHP3 認証機能拡張Plugin 承認メール、パスワードリマインダ

CakePHP3 認証機能拡張Pluginを作成したのでメモ

よく要件に出る認証関係の機能をPluginにまとめました。

GitHub - Junkins/StandardAuth

機能1 承認メール

サービスのアカウント登録をした際に、登録したメールアドレスに承認メールを送信し、 承認リンクを踏んでもらって初めてアカウントを有効化する機能

機能2 パスワードリマインダ

パスワードを忘れてしまった場合に、該当のメールアドレスを入力し、リセットされたランダムパスワードを送信する機能

Setup

事前の準備はStandardAuthComponentと、StandardAuthBehaviorを対象のコントローラーとTableにセットするだけ。

<?php
namespace App\Controller

use Cake\Controller\Controller;

/**
 * AdminAppController
 */
class AdminAppController extends Controller
{
  /**
   * initialize
   */
  public function initialize()
  {
      parent::initialize();
      $this->loadComponent('StandardAuth.StandardAuth', [
        'authenticate' => [
            'Form' => [
                'userModel' => 'Admins',
                'fields' => [
                    'username' => 'username',
                    'password' => 'password'
                ],
            ]
        ],
      ]);
  }
}
<?php
namespace App\Model\Table;

use Cake\ORM\Table;

/**
 * AdminsTable
 */
class AdminsTable extends Table
{
  /**
   * initialize
   */
  public function initialize(array $config)
  {
      $this->addBehavior('StandardAuth.StandardAuth');
  }
}

使い方

<?php

    /**
     * approval
     */
    public function approval($id, $hash)
    {
        $this->Admins->get($id);
        $result = $this->StandardAuth->approval($id, $hash);
        if ($result) {
            $this->Flash->success(__('The user has been saved.'));
        } else {
            $this->Flash->error(__('Invalid credentials, try again'));
        }
        return $this->redirect(['action' => 'login']);
    }

    /**
    * reset
    * @author ito
    */
    public function reset()
    {
        $this->viewBuilder()->layout('login');
        $this->StandardAuth->resetPassword();
    }
}

Junkins今年のCakePHP3活動の一部

皆さんこんばんは。 この記事はの CakePHP3 Advent Calendar 2016 の21日目として投稿したものです。 毎週水曜日Junkinsの日3日目でございます。

私がCakePHP3を触って1年半になります。 今年はCakePHPに慣れてきたのでいくつかPluginを作成しました。 この記事ではPluginの紹介となぜ作った理由について書きたいと思います。

紹介するPluginは2つです。 まだまだ未完成なところもありますが、今年のまとめとして書きたいと思います。 よろしくお願い致します。

1. GitHub - Junkins/MultiStepForm

1つ目はMultiStepFormです。 用途は「複数ページの入力画面」です。 複数のタブで入力画面を開いた場合でも、タブを超えてデータが共有されないように作成しています。

作ったキッカケ

システム管理者を登録する複数ページの入力画面を作る際にこんな問題が発生しました。

戻るボタンで戻ってきたら、パスワードが長くなっている。。。。

理由は簡単でリクエストデータをEntityに変換してデータを保持していたのですが、(ついでにバリデーションもかけたいので) Entityに変換する際にsetterが走ってしまい、パスワードがハッシュ化されていました。

CakePHP3のチュートリアルにもあるようにパスワードのハッシュ化はEntityのsetterで行います。 http://book.cakephp.org/3.0/ja/tutorials-and-examples/bookmarks/intro.html#id5

この問題が発生した時に考えた対応策は次の3つになります。

  1. setterでハッシュ化を行わない。CakePHP2のようにbeforeSave()でハッシュ化する
  2. Entity化せず、リクエストデータをそのまま使う
  3. Entity化する際にsetterが走らないようにする。

ただこの対応策はどれも問題がありました。

  1. CakePHP3の書き方を制限してしまう。setterが使えなくなってしまう。
  2. Entity化しないとバリデーションをかけられない。バリデーションメッセージを表示するためにはEntity化した方が都合がいい。
  3. setterを走らせずにEntity化する方法はあるが、TableのFunction経由ではsetterを走らせずEntity化することができない。

そこで無いなら作ってしまおう精神で作ったのがMultiStepFormです。

MultiStepFormの特徴

MultiStepFormの特徴は下記になります。

  1. Marshallerを拡張して、setterを使わずEntity化できるクラスを作成
  2. ModellessFormにも対応

目下の悩みはテストコードです。 明日記事を書かれるhiromiさんのTransitionComponentを参考に書きたいなと思っています。

GitHub - hiromi2424/TransitionComponent: Transition component for CakePHP.

2. GitHub - Junkins/JpChronos

次に紹介するのは、JpChronosです。 用途は「和暦対応のChronos」です。

使い方はこんな感じです。

<?php
$jpChronos = new JpChronos('1912-07-30');
$wareki = jpChronos->format({元号}{}年m月d日);

echo $wareki; // 大正元年07月30日
作ったキッカケ

今までは和暦変換のライブラリを別途作っていたのですが、上記の例のようにdate()のformat パラメータ文字列も同時に使いたいケースが多くいちいち文字列連結するのが面倒で作りました。

少し話は逸れますが、FrozenDate、FrozenTimeを拡張して2つクラスを作成した方がいいかもしれませんね。 このあたりのクラス設計が目下の悩みです。

まとめ

CakePHP3のPluginも増えてはいますが、まだまだCakePHP2に比べると少ないなと感じます。 これからPluginを増やして便利にしていければいいなと思います。

CakePHP3の焼き方

この記事はの CakePHP3 Advent Calendar 2016 の14日目として投稿したものです。

毎週水曜日Junkinsの日2日目でございます。 本日のテーマはCakePHP3 Advent Calendar 2016 初日や9日目の記事にも取り上げられた、Bakeです。

Bakeは便利

CakePHP3のBakeはカスタマイズが楽で重宝しています。

Bakeで生成したコードはPluginに比べ気軽に柔軟に修正することでき、 Pluginとはまた違った形で開発効率の向上に寄与してくれます。

Bakeのカスタマイズ

bakeをカスタマイズする場合は、TaskとTemplateをカスタマイズします。

例として、ModelTaskを拡張してみます。

Task

src/Shell/Task/JunkinsModelTask.php

<?php
namespace App\Shell\Task;

use Bake\Shell\Task\ModelTask;

class JunkinsModelTask extends ModelTask
{}

この時点でbakeコマンドを実行してみます。

bin/cake bake
Welcome to CakePHP v3.3.10 Console
---------------------------------------------------------------
App : src
Path: /var/www/html/htdocs/bake/src/
PHP : 5.6.22
---------------------------------------------------------------
The following commands can be used to generate skeleton code for your application.

Available bake commands:

- all
- behavior
- cell
- component
- controller
- fixture
- form
- helper
- junkins_model
- mailer
- migration
- migration_diff
- migration_snapshot
- model
- plugin
- seed
- shell
- shell_helper
- task
- template
- test

JunkinsModelのタスクが実行できるようになっています。

- junkins_model

下記のコマンドで通常のModelTaskと同じ処理が可能です。

bin/cake bake junkins_model
Welcome to CakePHP v3.3.10 Console
---------------------------------------------------------------
App : src
Path: /var/www/html/htdocs/bake/src/
PHP : 5.6.22
---------------------------------------------------------------
Choose a model to bake from the following:
- Accounts
- Admins
- Attachments

※ 同じ名前のクラス名で定義するとBakePluginのタスクが優先されてしまいます。

<?php
namespace App\Shell\Task;

use Bake\Shell\Task\ModelTask as BakeModelTask;

class ModelTask extends BakeModelTask
{}

Taskの拡張例

<?php
namespace App\Shell\Task;

use Bake\Shell\Task\ModelTask;

class JunkinsModelTask extends ModelTask
{
    public $skipTables = ['i18n', 'cake_sessions', 'phinxlog', 'users_phinxlog', 'admins'];
}
bin/cake bake junkins_model
Welcome to CakePHP v3.3.10 Console
---------------------------------------------------------------
App : src
Path: /var/www/html/htdocs/bake/src/
PHP : 5.6.22
---------------------------------------------------------------
Choose a model to bake from the following:
- Accounts
- Attachments

admins tableを無視してくれます。allオプションを指定する際などに便利ですね。

bin/cake bake junkins_model all --force
Template

src/Template/Bake/Model/table.ctp

継承元のクラスを変える変更をしています。

<?php
namespace <%= $namespace %>\Model\Table;

<%
$uses = [
    'use App\Model\Table\AppTable;',
];
sort($uses);
echo implode("\n", $uses);
%>

<%= $this->DocBlock->classDescription($name, 'Model', $annotations) %>
class <%= $name %>Table extends AppTable
{
}
bin/cake bake junkins_model Accounts

src/Model/Table/AccountsTable.php

継承元のクラスが変更されました。

<?php
namespace App\Model\Table;

use App\Model\Table\AppTable;
class AccountsTable extends AppTable
{
}
別のディレクトリへのファイル作成

src/Shell/Task/JunkinsModelTask.php

<?php
namespace App\Shell\Task;

use Bake\Shell\Task\ModelTask;

class JunkinsModelTask extends ModelTask
{
    public $pathFragment = 'JunkinsModel/';
}

このように拡張するとBakeで作成するファイルを設置する場所を変更することができます。

bin/cake bake junkins_model Clips

ClipsTable.phpとClip.phpがJunkinsModel以下に作成されていることがわかります。

Welcome to CakePHP v3.3.10 Console
---------------------------------------------------------------
App : src
Path: /var/www/html/htdocs/bake/src/
PHP : 5.6.22
---------------------------------------------------------------
One moment while associations are detected.

Baking table class for Clips...

Creating file /var/www/html/htdocs/bake/src/JunkinsModel/Table/ClipsTable.php
Wrote `/var/www/html/htdocs/bake/src/JunkinsModel/Table/ClipsTable.php`

Baking entity class for Clip...

Creating file /var/www/html/htdocs/bake/src/JunkinsModel/Entity/Clip.php
Wrote `/var/www/html/htdocs/bake/src/JunkinsModel/Entity/Clip.php`
テンプレートを変更する場合

先ほどの例では、下記のファイルパスにテンプレートをファイルを設置してテンプレートを作成しました。

src/Template/Bake/Model/table.ctp

ただ、このままだと複数のテンプレートを使い分けることができません。 その際に用いられるのが「テーマ」機能です。

下記のパスでテンプレートを作成すると、下記のコマンドで作成することができます。

// テンプレートパス
plugins/Simple/src/Template/Bake/Model/table.ctp
// コマンド
bin/cake bake junkins_model Clips --theme Simple

注意:SimplePluginはPluginとして使用出来る状態に設定する必要がある。

Bakeのイベント

Bake イベントを設定することで、bake対象のtableごとに処理を分けることが可能です。

config/bootstrap_cli.php
use Cake\Event\Event;
use Cake\Event\EventManager;
use Cake\Utility\Hash;

EventManager::instance()->on(
    'Bake.beforeRender.Model.table',
    function (Event $event) {
        $view = $event->subject();
    }
);
おまけ 1

もっとも自由にカスタマイズできるクラスは「SimpleBakeTask」です。

デフォルトで下記のクラスが準備されていますが、それ以外をBakeしたい場合はSimpleBakeTaskを使うと良いと思います。

BehaviorTask
CellTask
ComponentTask
ControllerTask
FixtureTask
FormTask
HelperTask
MailerTask
PluginTask
ShellHelperTask
ShellTask
TaskTask
TemplateTask
TestTask
おまけ 2

bakeの便利コマンド

// Model、Controller、Templateなど全てbakeするコマンド
cake bake all everything --force
参考URL

http://book.cakephp.org/3.0/ja/bake/usage.html

http://book.cakephp.org/3.0/ja/bake/development.html

http://book.cakephp.org/3.0/ja/bake/development.html#creating-a-bake-theme

http://book.cakephp.org/3.0/ja/views/themes.html

俺のプロジェクトマインドマップ

こんばんは 弊社のAdvent Calender 13日目です。

はじめに

CakePHP3 Advent Calender 2016の方でCakePHP3に関しては書こうと思っているので、

この記事ではCakePHP3以外の内容を書きたいと思います。

この頃、見積り業務やプロジェクトマネージメントの業務を多く担当しています。

大切なポイント

これらの業務を何度か担当するうちに下記の2つの業務をしっかりする事が大切だと感じます。

  1. 観点の洗い出し
  2. タスクの洗い出し

個人的な意見ですが、プロジェクト管理を行う上で「観点・タスクの漏れ」は非常に恐ろしいです。

見積り段階で漏れていると赤字に、タスクの漏れがあるとお客様に迷惑をかけてしまいます。

漏れを防ぐためには必要な観点・タスクだけでなく、やらない観点・タスクも洗い出す事が必要です。

また、提案までの期間が短い場合もあるので、この作業をスピーディーに行う必要があります。

観点・タスクをまとめる方法

観点やタスクをメモするために表形式でまとめる方法を試しましたが、

個人的にマインドマップがしっくりきています。

個人的に感じているマインドマップの利点は、各観点の関連性が明確になる点です。

私が作成しているマインドマップを公開したいと思います。

まだまだ育て中ですが、このように観点や気をつける点をメモしています。

マイ・マインドマップを作ろう!

人のマインドマップを見て観点の気づきを得ることも良いですが、

自分なりのマインドマップを作成する方が良いと思います。

自分でマインドマップを作成する過程で経験とひも付、ノウハウが整理されると思うからです。

私が公開するのはWebシステム開発マインドマップですが、

他の業務であっても一度、マインドマップを作成することをお勧めします。

ちなみに私は、「XMind」というアプリを使っています。

f:id:wanwansiba3:20161212225732j:plain

CakePHP3 配列からEntityを生成する!

どうもこんばんは

はじまりました。CakePHP3 Advent Calendar 2016

生意気にも毎週水曜日を私、Junkinsの日として3本記事を書かせていただきたいと思います。 読んでいただけると幸いです。

これを機にブログも始めました。 Junkinsのブログです。 よろしくお願いいたします。

1本目の記事のテーマは「配列 -> Entity編」です。 配列をEntityに変換する処理についてまとめたいと思います。

このテーマを選んだ理由は、CakePHP2経験者が最も戸惑う部分がEntityの扱いだと思うからです。

1. 配列からEntityの第一歩

リクエストデータをEntityに変換する処理はEntityを生成する処理の中でもよく見る処理です。

こんな感じですかね。

// newEntity()
$data = $this->request->data;
$topic = $this->Topics->newEntity($data);

// patchEntity()
$topic = $this->Topics->newEntity();
$data = $this->request->data;
$topic = $this->Topics->patchEntity($topic, $data);

2. 実際にEntityを生成する処理はどこに記述されているか?

newEntity()とpatchEntity()はCake\ORM\Tableの関数です。 しかし、実際に配列からEntityの生成を行っているクラスはCake\ORM\Marshallerになります。

コードに記述する関数 実際にEntityの生成を行う関数
newEntity() Marshaller::one()
patchEntity() Marshaller::merge()

3. Marshallerの流れを見てみよう

Marshaller::one()、Marshaller::merge()の処理の流れ

この流れを掴んでデバックがやりやすくなったなと感じます。

  1. 事前のコールバック処理(beforeMarshal)
  2. EntityClassの生成 ( newEntity()の場合のみ )
  3. アソシエーション先のModelのEntity生成と結合
  4. バリデーションの実行
  5. カラムの型に合わせたデータの変換
  6. Entityと配列のマージ ( patchEntity()のみ )

※ 直接、Entityをインスタンス化する場合では、Entityのaliasは自動で設定されませんが、 Marshallerを通すとaliasが自動で付与されます。

4. それぞれの処理を詳しく見てみよう。

1. 事前のコールバック処理(beforeMarshal)

このコールバックはCakePHP2でいう、beforeValidate()になります。 バリデーションの前に対象データを触ることができます。

使いかたはこのリンクを参照してください。 http://book.cakephp.org/3.0/ja/orm/saving-data.html#before-marshal

※ CakePHP2にはafterValidate()がありましたが、afterMarshal()はありません。

2. EntityClassの生成

newEntity()の場合のみEntityのインスタンス化をおこないます。 第一引数を特に指定しない場合はTableクラス内で処理が完結します。

// Tableクラスで処理が完結する
$this->Topics->newEntity();

// Marshaller::one()が実行される
$data = $this->request->data;
$this->Topics->newEntity($data);
3. アソシエーション先のModelのEntity生成と結合

アソシエーション先のModelのEntityの生成はMarshallerで行なわれます。

直接Entityクラスをインスタンス化する場合ではアソシエーション先のModelのEntityは結合されません。

アソシエーションの設定を行うのはTableクラスなので、考えれば当然ですね。

4. バリデーションの実行

バリデーションの実行場所はMarshaller::_validate()です。 下記にソースコードを書きます。

バリデーションの実行場所はTableクラスとばかり思ってましたが、Marshaller::_validate()を経由して実行されます。

Table->Marshaller->Tableと処理が移動するので、ちょっと複雑で読みにくい部分ですね。

protected function _validate($data, $options, $isNew)
    {
        if (!$options['validate']) {
            return [];
        }
        if ($options['validate'] === true) {
            $options['validate'] = $this->_table->validator('default');
        }
        if (is_string($options['validate'])) {
            $options['validate'] = $this->_table->validator($options['validate']);
        }
        if (!is_object($options['validate'])) {
            throw new RuntimeException(
                sprintf('validate must be a boolean, a string or an object. Got %s.', gettype($options['validate']))
            );
        }

        return $options['validate']->errors($data, $isNew);
    }
5. カラムの型に合わせたデータの変換

CakePHP3では配列からEntityを生成する際に、そのカラムのデータ型に合わせた値の変換をかけます。

デフォルトで基本的なTypeクラス (Cake\Database\Type) は定義されていますが、独自のカラムタイプを定義することも可能です。

http://book.cakephp.org/3.0/en/orm/database-basics.html#adding-custom-types

バリデーションを無事通過したデータはこのTypeクラスを経由して変換がかけられます。

CakePHP2のときは配列を持ち回していたので、最初は日時の文字列が Cake\I18n\FrozenTimeオブジェクトにいきなり変わってびっくりしました。

6. Entityと配列のマージ ( patchEntity()のみ )

そして、最終的に既存Entityとのマージを行います。 既存のEntityデータはoriginalに、変更があったデータはdirtyに格納されます。

5. Junkinsの悩み

直接Entityをインスタンス化する場合は下記のオプションが指定できるのですが、 Marshallerを通してインスタンス化する場合はオプションの指定ができません。。。。。

        $options += [
            'useSetters' => true,
            'markClean' => false,
            'markNew' => null,
            'guard' => false,
            'source' => null
        ];

現状は、設定を書いたEntityクラスをTableに設定してインスタンス化を行っていますが。。。

もっと楽な方法ないかなー。