ブロックチェーンに詳しいフリーのエンジニアのIT備忘録

ブロックチェーンに魅了されフリーのITエンジニアに転身した元暗号理論研究者のブログです。

cakephp3 Controllerを階層化する際の方法

ちょっとした決済システムを構築しています。


販売者(merchants)と顧客(customers)がいて、
決済(orders)を通じてデータが紐付いています。


それぞれにMVC(Model,View,Controller)があるわけですが、
販売者、顧客が自身の決済を参照するために、
管理画面を用意する必要があります。


そうなると、
販売者はmerchants/merchant:id/orders/index
顧客はcustomers/customer:id/orders/index
で決済を見るようにしたいわけです。


それを実現するために、
Controllerを階層化したいのですが、
Google先生で調べながらやると、
迷走してしまいました^^;


迷走した理由は、
2つの情報が錯綜しているからなのですが、
それは後述するとして、
先に階層化する方法を書きます。

Controllerを階層化する際の方法


(1) routes.phpに以下を追記する

>|php|

Router::prefix('merchants',function($routes){
$routes->fallbacks(DashedRoute::class);
});

|


(2) Controller/Merchants/OrdersController.phpを作成する

<?php
namespace App\Controller\Merchants;

use App\Controller\AppController;

/**
 * Merchants\Orders Controller
 *
 * @property \App\Model\Table\OrdersTable $Orders
 *
 * @method \App\Model\Entity\Order[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
 */
class OrdersController extends AppController
{ 
...

  /**
   * Index method
   *
   * @return \Cake\Http\Response|void
   */
 public function index(){
 }

...
}


(3) Template/Merchants/Orders/index.ctpを作成する


以外と簡単な設定ですみました。


ちなみにログイン画面は、
merchants/loginでアクセスしたいと考えた場合、


上記のroutes.phpの設定では、
merchantsController.phpを探しに行くのではなく、
src/Controller/Merchants/LoginController.php
探しにいってしまいます。


MerchantsController::loginの方に
アクセスしたい場合は、
routes.php

 $routes->connect('/merchants/login', ['controller' => 'Merchants','action' => 'login', 'prefix' => null]);

と記載すればOKです。

[補足] Google検索したら迷走した理由

階層化は前述の通り比較的簡単な設定ですみましたが、
実際ここにたどり着くためには、1,2時間要してしましました。

理由は2つの情報が錯綜していたからです。


2つの情報は、それぞれ以下です。
▼1つ目(Controllerを階層化する方法)
teratail.com

▼2つ目(階層化した時にController名がかぶった場合の設定)
norm-nois.com



1つ目の情報によると、
Controllerを階層化するには、
composer.jsonを修正して、
src/Controller/Merchants/ を自動読み込みすればよい
ということが書いてあります。


2つ目の情報によると、
src/Controller/UsersController.php
src/Controller/Admins/UsersController.php
のように別階層に同じ名前のControllerを置く場合は、
bootstarp.phpに数行その旨を記述しないといけない
ということが書いてあります。


最初に見つけた情報が
1つ目の記事だったので、
書いてあるとおりに実行をしたら、
Controllerを階層化することには成功しました。


しかし、階層構造の中に
同じ名前のコントローラが出てくると
片方のコントローラが読み込みエラーになります。


そのため、
2つ目の記事にある
src/Controller/UsersController.php
src/Controller/Admins/UsersController.php
という階層構造を作る方法を実践してみたのですが、
こちらはうまくいきませんでした。


最終的に、
1つ目の設定を解除して
routes.phpで公式ドキュメントにある
プレフィックスルーティングの設定をすることで
解決したというところになります。

cakephp3でフォームの画面遷移のロジックをうまく整理できた

cakephp3を使用してフォームを書く度に、
画面遷移のロジックを綺麗に書けずといいますか、
納得した形で書けず、ずっと悩んでおりました。


悩みながらネットで調べていたところ
POSTで画面遷移をうまく切り替えるプラグイン
書いている方がいらっしゃいまして、
その方のコードを拝借することにしました。


詳細は以下のQiitaのページをご覧ください。
qiita.com


ただ、composerでインストールして
そのままで利用すると、
なぜか私の環境ではうまく動作しなかったので、
エラーがあるごとにプラグインのコードを修正いたしました。


一部自分仕様に仕上げたところもあり、
オリジナルと動作が異なる部分があります。


修正版のコードを
備忘録として挙げておきます。


以下、PostTransitionControllerTrait.php の修正版

<?php

namespace PostTransition\Controller;

use Cake\Utility\Security;
use Cake\Utility\Hash;
use Cake\Routing\Router;
use Cake\Network\Exception\MethodNotAllowedException;
use Cake\ORM\TableRegistry;

trait PostTransitionControllerTrait
{
    private $__default_settings = [
        'nextPrefix' => 'next',
        'backPrefix' => 'back',
        'nowField' => 'now',
        'default' => [
            'value' => [],
        ],
        'post' => [],
        'param' => [],
    ];
    private $__settings;
    public $transitionModel;

    private function __postTransition($settings){
        //設定値の設定
        $this->__postSettingAdjustment($settings);

        if (empty($this->__settings['model'])){
            $this->__settings['model'] = $this->modelClass;
        }

        $this->transitionModel = TableRegistry::get($this->__settings['model']);

        //post_max_sizeオーバーのときのエラーチェック
        //postでかつデータが空の時=>post_max_file_sizeオーバー時の対応

        if ($this->request->is('post') && empty($this->request->data)) {
            $this->Flash->error(__d('post_transition', 'Post max size error.'));
            $this->__sessionTimeout();
        }


        //初期アクセス時の対応
        if (!$this->request->is('post') && !$this->request->is('put')){
            $this->__firstAction();
            return;
        }

        //セッション切れの処理
        if (!$this->request->session()->check($this->__settings['model'] . '.' . $this->request->data['hidden_key'])){
            //$this->Flash->error(__d('post_transition', 'Session Timeout.'));
            return $this->__sessionTimeout();
        }

        //request->dataで来たデータから必要なprefixのついているものを抽出
        $keys = array_keys($this->request->data);
        $action_button_check = preg_grep('/^(' . $this->__settings['nextPrefix'] . '|' . $this->__settings['backPrefix'] . ')_/',$keys);
        if (empty($action_button_check)){
            //エラー
            throw new MethodNotAllowedException();
        }

        //一番目のものを取得する(複数はない前提)
        $action_data = array_shift($action_button_check);
        //next_action
        if (!preg_match('/^(' . $this->__settings['nextPrefix'] . '|' . $this->__settings['backPrefix'] . ')_(.*)$/',$action_data, $action)){

            //上部マッチで取っているはずだがもし流れた場合はエラー
            throw new MethodNotAllowedException();
        }

        $entity_data = $this->request->session()->read($this->__settings['model'] . '.' . $this->request->data['hidden_key']);
        $entity = $this->transitionModel->newEntity($entity_data);

        //追記
        $entity->now = $entity_data['now'];

        if (!empty($this->request->data[$this->__settings['nowField']])) {
            $nowField = $this->request->data[$this->__settings['nowField']];
        } else {
            $nowField = $entity->{$this->__settings['nowField']};

        }
        //何も設定がないときはdefaultを読む
        $validate_option = [];
        if (array_key_exists('validate_option', $this->__settings['post'][$nowField])){
            $validate_option = $this->__settings['post'][$nowField]['validate_option'];
        }

        $entity = $this->transitionModel->patchEntity($entity, $this->request->data(), $validate_option);

        if (
            $action[1] == $this->__settings['nextPrefix'] &&
            $entity->errors()
        ){
            //$this->Flash->error(__d('post_transition', 'Validation could not pass.'));

            $mergedData = array_merge(
                $entity_data,
                $this->request->data()
            );
            $this->request->session()->write($this->__settings['model'] . '.' . $this->request->data['hidden_key'], $mergedData);
            $this->_viewRender($entity, $entity->{$this->__settings['nowField']}, $this->__settings['param']);

            return;
        }

        $mergedData = array_merge(
            $this->request->data,
            [$this->__settings['nowField'] => $action[2]]
        );

        $entity = $this->transitionModel->patchEntity($entity, $mergedData);

        $mergedData = array_merge(
            $entity_data,
            $mergedData
        );

        $this->request->session()->write($this->__settings['model'] . '.' . $this->request->data['hidden_key'], $mergedData);

        $this->_viewRender($entity, $action[2], $this->__settings['param']);

        return;
    }

    protected function _viewRender($entity, $action, $param){

        $private_method = $this->__settings['post'][$action]['private'];
        if (method_exists($this, $private_method)){
            $this->{$private_method}($entity, $param);
        }

        //viewのnowに現在の画面の値をセットする
        $entity->{$this->__settings['nowField']} = $action;
        $this->set(compact('entity'));
        if ($this->__settings['post'][$action]['render'] !== false){
            $this->render($this->__settings['post'][$action]['render']);
        }
        return;
    }

    private function __postSettingAdjustment($settings){
        $this->__settings = Hash::merge(
            $this->__default_settings,
            $settings
        );

        foreach ($this->__settings['post'] as $post_key => $post_val){
            if (is_string($post_val)){
                $this->__settings['post'][$post_val] = [
                    'render' => $post_val,
                    'private' => '__' . $post_val,
                    'validate_option' => [],
                ];
                //不要なものは削除
                unset($this->__settings['post'][$post_key]);
            } else {
                if (!array_key_exists('render', $post_val)){
                    $this->__settings['post'][$post_key]['render'] = $post_key;
                }
                if (!array_key_exists('private', $post_val)){
                    $this->__settings['post'][$post_key]['private'] = '__' . $post_key;
                }
                if (!array_key_exists('validate_option', $post_val)){
                    $this->__settings['post'][$post_key]['validate_option'] = [];
                }
            }
        }

    }

    private function __firstAction(){
        $hidden_key = Security::hash(time() . rand());

        if (is_object($this->__settings['default']['value'])){
            $entity = $this->__settings['default']['value'];
            $entity->hidden_key = $hidden_key;
            $value = $entity->toArray();
        } else {
            /*
            $value = array_merge(
                $this->__settings['default']['value'],
                ['hidden_key' => $hidden_key]
            );
            */
            $value = $this->__settings['default']['value'];
            $entity = $this->transitionModel->newEntity($value);
            $entity->hidden_key = $hidden_key;
        }

        //セッションに空のデータを作成しておく
        $entity->{$this->__settings['nowField']} = $this->__settings['default']['post_setting'];
        $value[$this->__settings['nowField']] = $this->__settings['default']['post_setting'];

        $this->request->session()->write($this->__settings['model'] . '.' . $hidden_key, $value);

        $this->_viewRender($entity, $this->__settings['default']['post_setting'], $this->__settings['param']);
    }

    private function __sessionTimeout(){
        //最初に戻る
        if (!empty($this->__settings['start_action'])){
            $start_action = $this->__settings['start_action'];
        } else {
            //設定がないときは自身のURLにリダイレクト(基本こっち
            //自身のURLがうまくRouter::urlで取れないので自身で作成しておく
            $start_action = Router::fullBaseUrl() . $this->request->here;
        }
        return $this->redirect($start_action);
    }
}

bitcoindに秘密鍵をインポートしたのに残高が反映されない件

表題の通りなのですが、
bitcoindに秘密鍵をインポートしたのに、
残高が0のままで焦りました。
その対応の記録です。

同じ問題で困っている方の一助になればと思います。

(1)秘密鍵をインポートする

念の為、秘密鍵のインポートの仕方を記載します。
bitcoindを立ち上げた後、以下のコマンドでインポートされます。

 $bitcoin-cli importprivkey "bitcoinprivkey" ( "label" rescan)

引数に関して以下に解説を加えます。

引数名 解説
bitcoinprivkey 秘密鍵のこと。ただしWIF形式で指定します。
label (任意) アカウント名を指定します。
rescan(任意) ブロックチェーンを再読込する場合はtrueを指定します。

というところです。
最初rescanのことをうまく認識できておらず、
これが今回の問題の一番の要因となりました。。。

(2)秘密鍵をインポートしても残高が反映されない。

上記のコマンドで、
秘密鍵をインポートしても
残高が0のままなのです。。。

(3)ウォレットは秘密鍵をインポートした後の取引しか見ない

焦りながら情報を調べていった結果、
以下の記事を見つけ、
bitcoin coreのウォレットは、
"秘密鍵をインポートした後の取引しか見ない"ということが分かりました。
bitcoin.stackexchange.com

(4)インポートの前の履歴や残高を取得する場合

bitcoindにブロックチェーンを再読込するように
教えてあげないといけません。

コマンドは簡単で、

$ bitcoind -rescan

です。



再読込のため、
私のメモリ8GBのPCでは、
2時間弱かかりましたが、無事残高を正しく表示できました。



なお、bitcoin-cli importprivkey の引数の一つであるrescanについてです。
ここをtrueで指定してやると、勝手にブロックチェーンを再読込しそうなのですが、
機能しないようです。



以下の記事でそのようなことが報告されています。
github.com




これで無事、秘密鍵と残高が反映されました^^

Ubuntu 16.04 LTSのキーボードを日本語に変更する

[背景]

Lispを勉強するために

MacUbuntu on VirtualBoxを入れた。

しかし、入力が英語キーボードのため日本語キーボードに修正したい。

 

[解決]

以下のページを参照することで解決した。

blog.amedama.jp

 

電子マネーは分裂しないのにビットコインは分裂する理由

何周も周回遅れの
超初心者向け記事です。
 
ビットコイン分裂騒動、
ある程度落ち着いていて、
 
 
興味が薄くなっておりますが、
いまだになぜ分裂するん?と
聞かれるので、
 
 
いつも説明している内容を
ここに備忘録かねて書きます。
 
 
ちなみに、すでに色々な方が、
多少の専門用語込で、
分裂騒動書いてるので、
超絶初心者向けに書きます。
 
 
物足りない方は、
他の方の記事参照で!
 
 
さて、
ビットコインが分裂することは、
初学者にとって意味不明です。
 
 
電子マネーは分裂しないのに、
ビットコインは分裂するのですよ。
やっかいなことに。
 
 
これは根本的に
構造が違うからです。
 
 
そしてその構造の違いのため、
「システムアップデートが
 極めて難しい」
という特性を持っています。
 
 
これが分裂騒動の
一番の根本といえるでしょう。
 
 
じゃあ構造の違いというのは
何か?
 
 
電子マネーというのは、
発行している会社が、
システムを管理していますが、
 
 
ビットコインというのは、
特定の管理者が存在せず、
 
 
参加者がインストールした
ソフトウェアによって、
参加者全員で管理しています。
 
 
ビットコインの新規発行も
送金のチェックも、
参加者全員で実施しています。
特定のリーダーがいません。
 
 
かめはめ波(悟空1人の力)なのか、
元気玉(全世界の力)なのか、
そんな違いです(?)
 
 
不特定多数の
世界中のコンピューターで
ネットワークを運営している
 
 
ある程度順調に進んでましたが、
人気になっていくにつれて、
1つの課題にぶち当たりました。
 
 
それは、ユーザの送金需要に対して、
ビットコインの処理能力が
追いつかなくなってきたのです。
 
 
もともとの仕様で
「約10分に1MB分の送金を処理できる」
というものがあります。
 
 
わかりづらいので言い換えると
「約10分に約2000送金処理できる」
ということになります。
 
 
この処理速度が追いつかず、
だんだんネットワークが
パンクしはじめたんです。
 
 
じゃあそんな仕様を捨てて、
パワーアップして、
処理速度あげようぜ!
 
 
という話になるのですが、、、
そう簡単にはいかないのです。
 
 
バージョンアップする時の事を
想像してみてください。
 
 
世界中の不特定多数の
コンピュータが専用ソフトウェアを
インストールして管理してます。
 
 
バージョンアップするよ!
ってなったら、
 
 
みんな一斉に
バージョンアップしなければ
いけません。
 
 
だってみんなで管理してるから、
みんな同じバージョンじゃないと
整合性とれないでしょ?
 
 
世界中の数万のコンピュータが
同時にアップデートしないと、
大変な事になるんです。
 
 
例えばWindows10の
アップデートの時のことを
思い出してみてください。
 
 
強制的にWindows10のアップデートを
行う形で進めてましたが、
 
 
やれまだWin8がいいとか、
 
 
アップデートすると不具合がでるとか、
 
 
様子見たいとか、
 
 
いろんな理由で、
アップデートなかなかしませんでした。
 
 
これってね、
Microsoft社は良かれてと思って
やってるはずですよね。
 
 
なのにみんな、
色々な憶測を含めて、
なかなかやらないんです。。。
 
 
世界の多数のコンピュータユーザーの
意思統一というのは、
極めて難しいことが、
この例を見ただけで分かると思います。
 
 
Windowsの場合は、
まだ皆がコンピュータを別々に使ってるから
なんとかなるのですが、
 
 
皆で一緒にネットワークを
管理しているんです。
 
 
方やWindows10で
方やWindows8でとか、
新、旧で別の仕様で動くと
とんでもないことが起こります。。。
 
 
どうなるか?
 
 
新バージョンと
旧バージョンで通信できなくなって、
 
 
新バージョンネットワークと
旧バージョンネットワークに
綺麗に分離してしまうのです。。。
 
 
これがビットコインの分裂
というものです。
 
 
アップデートにともなって、
旧バージョンと新バージョンに
別れてしまう。。。
 
 
こういうことです。
 
 
分裂した後どうなるの?
 
 
これは誰も結果が分からず、
未知数なのです。
 
 
このような事情から、
ビットコインの世界では、
2つの派が喧嘩してます。
 
 
革新派と保守派です。
 
 
革新派は、
ネットワークがパンクするから、
分裂のリスクを負ってでも、
無理やりバージョンアップすべきだ!!!
という考えをもつ人達です。
 
 
保守派は、
そんな強硬に分裂させちゃうと、
ビットコインに甚大な被害をもたらすから
もっとゆっくりやろうや!!
 
 
ということです。
 
 
革新派と保守派が度々喧嘩をして、
 
 
エンジニアや投資家は、
「あわわ、、ビットコイン分裂するかも、、」
ということで価格が乱高下したりします。
たまに。
 
 
私がよく覚えてるのは、
2015年の8月です。
 
 
ギリシャEU離脱が騒がれて、
その直後に上海発世界同時株安が
起こりました。
 
 
金のようにリスク回避資産と
言われるビットコインですが、
 
 
世界同時株安など、
世界経済全体があかん場合は、
円>>>>>>>ビットコインなので、
ビットコインは暴落します。
 
 
そんな最中にさらに、
革新派と保守派の喧嘩が
ヒートアップしまして、
 
 
革新派(BitcoinTX)vs 保守派で、
はじめてビットコインが大規模分裂する!!
となって、
 
 
上海同時株安からの
ビットコイン分裂危機で、
二段階暴落が起こりました。
 
 
2017年7月現在ビットコイン
2500$を越えてますが、
 
 
二段階暴落の時は、
180$を切ったんですね。
さすがにビビりました^^;
 
 
その分裂騒動は収まって、
徐々に価格伸びたのですけど、
 
 
度々喧嘩する中で、
今回UASFというまた
大きな喧嘩が勃発して、
分裂!分裂!と騒がれた、、、
というのが最近の騒動です。
 
 
細かいこと言うと、
今回は、Unlimited派という派閥が
ASIC BOOSTという裏技を使いたいがために、
Segwitというバージョンアップを
拒否していたなんて話もあって、
 
 
より入り組んでるのですが、
その辺の細かいことは、
多くの方々が既に書いているので、
そちらに譲ります。
 
 
昔から
仮想通貨業界にいるものとして、
 
 
ビットコインはバージョンアップむずい
・ネットワークがパンクしてる
・革新派と保守派の喧嘩
 
 
という背景を
知って頂ければと思います。
 
 
8月1日は落ち着きそうですが、
この問題はまだまだ、
本質的に解決できてないので、
ずるずる続きますね。
 
 
以上、
「がの」がお送りしました(^o^)/

cakephp3でフォームに確認画面を用意する(どのようにしてvalidationを適用するか?)

こんばんは。「がの」です。

 

最近cakephp3を触ってます。

 

今回の記事は

validationについてです。

 

確認画面を要するフォーム作成時に

validationで少し躓いたので、

その備忘録を書きます。

 

ーーーー【達成したかったこと】ーーーー

(1)フォームに入力した項目を

TableのvalidationDefaultでチェックしたい。

 

(2)入力後、確認画面を表示した上で、

データを保存する。

 

(3)bakeで生成されるaddアクションを

見ると $this->Table->save($entity)で

validationDefaultが発動しているが、

saveの手前の「確認画面表示」の時に、

validationを行いたい。

ーーーーーーーーーーーーーーーーーーー

 

cakephp2での記述の仕方は

ググれば色々書いてあるのですが、

cakephp3は記事が少なく、

最初よく分かりませんでした。

 

 

調べていて分かったことは、

”cakephp3では、INSERTを行う際に、

  2度validationをかけている"

ということです。

 

2回というのは、

それぞれ以下になります。

 

ーーーーーーーーーーーーーーーーーーー

[1回目]

$entity = $this->Table->newEntity($postParams)

で、newEntityを作る時。

 

[2回目] 前述の通り

$this->Table->save($entity) の時

ーーーーーーーーーーーーーーーーーーー

 

通常どおり、プログラムを実行すると、

newEntityの時にvalidationをかけていることは

気づきません。。。

 

エラーが発生しても、

何の反応やメッセージもなく、

そのままコードを実行するからです。

 

達成したい内容は、

newEntityの時のvalidationの結果を踏まえて、

 

エラーが無ければ確認画面へ、、、

エラーが有れば入力画面でエラー表示

 

とすれば良いでしょう。

 

肝心のnewEntityの時のvalidation結果は、

$entity->errors()で取得可能です。

 

errors()が空であれば

確認画面に飛ばせば良いでしょう。

 

また、validationで指定した

エラーメッセージがerrors()の中に

格納されているので、

 

errors()が空ではない場合は、

そのままTemplateに渡して、

メッセージを表示させればよいでしょう。

 

書いたコードは

気が向いたら載せます^^;

 

 

以上、

「がの」がお送りしました(^o^)/