CakePHP3 から CakePHP4 に移行したら Middleware が動かなくなった #cakephp

現象

CakePHP3 から CakePHP4 に移行した際に、CakePHP がデフォルトで提供しているミドルウェアを拡張して利用していた Middleware が動かなくなった。(正確には意図しない挙動になった)

原因

CakePHP3 ではミドルウェア__invoke($request, $response, $next) を実装すればよかった。

しかし PSR 15 対応が行われたことにより、 __invoke メソッドは呼ばれなくなってしまった。

github.com

// Cake の提供している Middleware を拡張
class MyMiddleware extends ErrorHandlerMiddleware
{
    // 呼ばれなくなってしまった。。
    public function __invoke($request, $response, $next)
    {
        // do something
    }
}

対応

CakePHP4 では、 ミドルウェアに MiddlewareInterface を実装する必要がある。

ただし今回の場合は拡張なので、 ( __invoke ではなく) process($request, $handler) をオーバーライドすればよい。

// Cake の提供している Middleware を拡張
class MyMiddleware extends ErrorHandlerMiddleware
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // do something
    }
}

github.com

感想

拡張していることにより拡張元のメソッドが呼ばれることによって、パッと見動いているようになっていた。

それゆえに少し原因特定に時間がかかってしまった。

移行ガイドでも互換性があるように読み取れるし、拡張であったがゆえに見逃しやすい気がする。

参考

「リモートワークの達人」を読んだ

「リモートワークの達人」を読んだ。

以前「小さなチーム、大きな仕事」を読んでいいなぁ〜と思っていたんだけど、同じくベースキャンプ社 (元37シグナルズ) の本ということで長く気になってた本。

元々「強いチームはオフィスを捨てる」というタイトルだったが、改題したらしい。

タイトルからしてちょっとポジショントークに偏りそうだなぁとの印象があったので、なかなか手をつけていなかった。

ただコロナ禍もあって自分の職場もリモートワークが中心になり、なにか参考になることがあればなぁとようやく読んでみた。

本のタイトル変わったのもきっと時勢の影響だよね。

自分もリモートしたから共感できるところが多い

書籍の内容としては当然リモートワーク推しなんだけれども、実際に自分もやってみたからこそ共感できるような内容も多かった。

リモートでも一定同じ時間は一緒に働いたほうがいいとか、お互いの様子が分かるような工夫をした方がいいとか。

バーチャルな雑談の話とか、孤独に気をつけようといった、ちょっとウェットなことにも言及しているのは意外だったかも。

本質的にはリモートワークかどうかに関わらないような問題も多くて、リモートワークにすることで顕在化するような話もあるよなぁと思った。

過度な管理とか承認制とか。

ただちょっとずるいなぁと思うのは、リモートワークにおいて仕事の話とプライベートの話を区別せずに言及していること。

福利厚生的な側面と仕事の成果的な側面は、コンテキストが違いすぎるので、同列にされると違和感があるんだよな。

どうするのがいいのかなぁ

ボク個人としては、通勤がなくなったり、(今となっては)オフィスより快適な作業環境を整備したりしたことで、出社へのモチベーションは以前よりもだいぶ低くなった。

ただそれでも、この働き方が必ずしも「より良いものだ」とは言い切れない部分もある。

本の中でも「リモートワークは0か1ではない」と書かれていたけれども、試行錯誤していくしかないんだろうな。

個人としても、組織としても、社会としても。

「プロダクトマネジメント」を読んだ

プロダクトマネジメントを読んだ。*1

あんまり優先的に読む気もなかったんだけども、同僚たちの間で流行っていたおり、この本を前提に話されることが多くなりそうな雰囲気を感じたので目を通しておくか、という消極的なモチベーションで読んだ笑

言葉が先行しがちなプロダクトマネージャーやプロダクトマネジメントに網羅的に触れられている。

プロダクトマネージャーのキャリアパスであったり、プロダクトマネジメントについても組織戦略の話から現場でのプロセスやノウハウなど幅広い。

プログラマや開発者視点の本ではないので、共通言語を作りやすい面もありそう。「ビルドトラップ」なんかはそのものズバリだ。

この本を正解にする必要はないけど、取っ掛かりにして考えるのはよさそうだなと思った。

プロダクトマネジメント 読書メモ · GitHub

*1:実は読んだのは結構前なのだが、アウトプットをサボっていた

Slack API を使って、特定のチャンネルのメッセージと発言ユーザーを取得する #slack

概要

Slack の発言を分析したいみたいな話が職場であって、サクッと取れないかな〜と雑にシェルスクリプトで試してみた記録。

便利なツールとか既にありそうだけどね。。

  • Slack API を使って
  • 特定のチャンネルから一定期間の発言を取得し
  • 「投稿日時」
  • 「その発言をしたユーザー」
  • 「メッセージへのリンクURL」
  • を出力するまでの流れ

APIを叩く準備

Slack API を叩くには Token を発行する必要がある。

Token 単体では発行できなくて、アプリケーションを作成してインストールする流れになるようだ。

主な流れは下記の通り。

  1. Slack Application を作成
  2. Application に対して API を叩くのに必要な Scope を設定
  3. Token を発行
  4. Application を使いたい Slack Team にインストール

下記の記事がとても参考になった。

tearoom6.hateblo.jp

メッセージの取得

conversations.history を使う。

conversations.history method | Slack

Tester タブから実際にリクエストを試せるので便利。

slack_message_list.bash みたいなのを作ってみる。

  • jq を使って必要な情報だけ取り出す
    • ユーザーIDと投稿日時
  • 取得上限はデフォルト100件だが最大の1000にしている
    • LIMIT パラメータ
    • 1000件以上は考慮していない
  • 取得範囲を 2021-01-01 以降にしてる
    • OLDEST パラメータ
#!/usr/bin/env bash

set -eu

readonly API_TOKEN="xxxx-xxxxxxxxx-xxxx"
readonly CHANNEL="C1234567890"
readonly LIMIT=1000

# Unixtime に変換し、小数点配下を埋め。 ※Mac でやったので BSD の date コマンド
readonly OLDEST="$(date -j -f "%Y-%m-%d %H:%M:%S" "2021-01-01 00:00:00" +%s).000000"

## API を叩き、 jq で user と ts だけ抽出してカンマ区切りにする
curl -s "https://slack.com/api/conversations.history?channel=${CHANNEL}&oldest=${OLDEST}&limit=${LIMIT}" \
     -H "Authorization: Bearer ${API_TOKEN}" \
     -H 'Content-Type: application/json; charset=utf-8' \
  | jq -r '.messages[] | [.user, .ts] | @csv'

exit 0

ユーザー情報とメッセージへのリンクURLを取得

ユーザーIDを元にユーザー名、投稿日時を元にメッセージへのリンクURLを取得する。

ユーザーの取得には users.info を使う。

users.info method | Slack

メッセージへのリンクURLの取得には chat.getPermalink を使う。

chat.getPermalink method | Slack

conversations.history で取得した値を連携するような slack_user_link.bash を作成する。

#!/usr/bin/env bash

set -eu

readonly API_TOKEN="xxxx-xxxxxxxxx-xxxx"
readonly CHANNEL="C1234567890"

arg=${1}

# 受け取った値を分解して、不要なクォーテーションを除去
user_id=$(echo ${arg} | cut -d',' -f 1 | sed 's/"//g')
ts=$(echo ${arg} | cut -d',' -f 2 | sed 's/"//g')

## users.info
user_name=$(curl -s "https://slack.com/api/users.info?user=${user_id}" \
     -H "Authorization: Bearer ${API_TOKEN}" \
     -H 'Content-Type: application/json; charset=utf-8' \
     | jq -r .user.name)

## chat.getPermalink
url=$(curl -s "https://slack.com/api/chat.getPermalink?channel=${CHANNEL}&message_ts=${ts}" \
     -H "Authorization: Bearer ${API_TOKEN}" \
     -H 'Content-Type: application/json; charset=utf-8' \
      | jq -r .permalink)

# 投稿日時を表示用に変換
date=$(echo ${ts} | cut -d'.' -f 1 | xargs -I@ date -r @ +"%Y-%m-%d %H:%M:%S")

# カンマ区切りで表示
echo "${date},${user_name},${url}"

exit 0

組み合わせて実行してみる

↓な感じで組み合わせて実行できる。

./slack_message_list.bash | xargs -I@ ./slack_user_link.bash @

最初のスクリプト"U012AB3CDE","1512085950.000216" みたいなのを出力するので、それを元に次のスクリプトAPI を順繰り叩く感じ。

最終的な出力は↓みたいになる。

2021-01-19 10:47:21,user_a,https://ghostbusters.slack.com/archives/C1H9RESGA/p135854651500008
2021-02-18 10:43:24,user_b,https://ghostbusters.slack.com/archives/C1H9RESGA/p135854651500009
2021-03-17 12:55:30,user_c,https://ghostbusters.slack.com/archives/C1H9RESGA/p135854651500000

その他参考

unixtimeとdatetimeを変換する(Mac/BSD編) - Qiita

Next.js で Material-UI と styled-components を使う #nextjs

概要

Next.js で Material-UI を利用しつつ、 styled-components でカスタマイズできる環境を構築した記録。

nextjs.org

material-ui.com

styled-components.com

Next.js でプロジェクトを作る

$ npx create-next-app

Material-UI を install する

$ npm install @material-ui/core

styled-components を install する

$ npm install styled-components

babel-plugin-styled-components を install する

これを入れないと Warning: Prop className did not match. とか言われる。

$ npm install --save-dev babel-plugin-styled-components

.babelrc を(なければ)作成して、設定する。

{
  "presets": ["next/babel"],
  "plugins": [["styled-components", { "ssr": true }]]
}

github.com

_app.js で優先順位を調整する

styled-components が最後に当たるようにするには、CSS injection order を設定しておくらしい。

Style Library Interoperability - Material-UI

<StylesProvider injectFirst> で Component ツリーを囲んでおく。

CssBaseline は reset.css 的なやつらしい。

import '../styles/globals.css'
import {StylesProvider} from "@material-ui/core";

function MyApp({Component, pageProps}) {
    return (
        <StylesProvider injectFirst>
            <CssBaseline/>
            <Component {...pageProps} />
        </StylesProvider>
    )
}

export default MyApp

_document.js を作成して修正する

material-ui や styled-components を Next.js で使うには、 pages/_document.js をカスタマイズする必要がある。

(なければ)ファイルを新規に作成し、どちらも公式の example があるので、参考にしながらいい感じにマージする。

material-ui/_document.js at master · mui-org/material-ui · GitHub

next.js/_document.js at master · vercel/next.js · GitHub

import React from 'react';
import Document, {Html, Head, Main, NextScript} from 'next/document';
import {ServerStyleSheets as MaterialUIStyleSheets} from '@material-ui/core/styles';
import {ServerStyleSheet as StyledComponentsStyleSheets} from "styled-components";

export default class MyDocument extends Document {
    render() {
        return (
            <Html lang="ja">
                <Head/>
                <body>
                <Main/>
                <NextScript/>
                </body>
            </Html>
        );
    }
}

MyDocument.getInitialProps = async (ctx) => {
    const materialUISheets = new MaterialUIStyleSheets()
    const styledComponentsSheets = new StyledComponentsStyleSheets()
    const originalRenderPage = ctx.renderPage

    try {
        ctx.renderPage = () =>
            originalRenderPage({
                enhanceApp: (App) => (props) => styledComponentsSheets.collectStyles(
                    materialUISheets.collect(<App {...props} />)
                ),
            })

        const initialProps = await Document.getInitialProps(ctx);

        return {
            ...initialProps,
            styles: (
                <>
                    {initialProps.styles}
                    {styledComponentsSheets.getStyleElement()}
                </>
            ),
        }
    } finally {
        styledComponentsSheets.seal()
    }
};

material-ui の Button を styled-component でカスタマイズしてみる

pages/sample.js みたいなのを作って、カスタマイズしてみる。

styled() に material-ui の Component を渡してあげれば style を上書きできる。

import React from 'react';
import styled from 'styled-components';
import Button from '@material-ui/core/Button';

const StyledButton = styled(Button)`
  background-color: red;
`;

const Sample = () => {
    return (
        <div>
            <StyledButton>Customized</StyledButton>
        </div>
    )
}

export default Sample

参考

CakePHP でカスタム Validation を追加しやすくする #cakephp

概要

CakePHP には独自に定義したカスタム Validation を使う方法がいくつかある。

Default のバリデーションプロバイダーを差し替えることで、アプリケーション全体で使うようなルールを追加しやすくする。

例えば「会員IDは 0 埋めを含む8桁の数字」のようなカスタムバリデーションを使う場合、下記のような形で呼び出せるようにする。

$validator = new AppValidator();
$validator->memberId('member_id', 'member_id は8桁の数字で指定してください');

Validation を拡張したクラスを作る

Validation を拡張して、カスタム Validation を追加するための独自クラスを作る。

Cake\Validation\Validation を拡張することで、既存のチェック処理を直接的にも間接的にも使える。

class AppValidation extends Validation
{
    /**
     * 会員IDをチェックする
     *
     * @param string $check Value to check
     * @return bool Success
     */
    public static function isMemberId($check)
    {
        // 数字8桁のチェック
        return self::custom($check, '/^\d{8}$/');
    }
}

Validator を拡張したクラスを作る

Validator を拡張して、プロキシメソッドを追加するための独自クラスを作る。

このクラスはなくてもよいのだが、カスタムバリデーションを使う際にいちいち配列を作ったりせずに済むようになるため、よく使うものには用意しておくと便利だと思う。

class AppValidator extends Validator
{
    /**
     * 会員IDのルールを追加する
     *
     * @param string $field The field you want to apply the rule to.
     * @param string|null $message The error message when the rule fails.
     * @return $this
     * @see AppValidation::isMemberId()
     */
    public function memberId($field, $message = null)
    {
        // この辺の書き方は、本家 Validator の類似メソッドを参考にするといいと思う
        $extra = array_filter(['message' => $message]);

        return $this->add(
            $field,
            'member_id',
            $extra + [
                'rule' => 'isMemberId',
            ]
        );
    }
}

デフォルトのバリデーションプロバイダーを差し替える

config/bootstrap.php でバリデーションプロバイダーを差し替えることで、アプリケーション全体に拡張した Validation が適用される。

addDefaultProvider で name を default にすることで、カスタムバリデーションでも provider の明示的な指定が不要になる。

ただし default を差し替える場合、 RulesProvider インスタンスで渡してやる必要がある。

ちなみに name を固有のものにすれば、 プロバイダーを使い分けることが可能。

※その場合、プロバイダーの名前を rule で渡してやる必要がある

// bootstrap.php でデフォルトのプロバイダーを差し替える
Validator::addDefaultProvider(
    'default', 
    new RulesProvider(AppValidation::class)
);

まとめ

  • 独自Validationを作る
  • 独自Validatorを作る
  • 独自Validationをデフォルトのバリデーションプロバイダーにする

上記を行うことで、カスタムバリデーションの追加がしやすくなる。

アプリケーション内横断で使いたいようなカスタムルールはそれなりにあると思うので、同じルールが分散するよりかは、定義する場所を確保しておくとメンテしやすいと思う。

参考

CakePHP で uploadedFile の Validation をテストする #cakephp #php

概要

CakePHP にはアップロードファイル用の validator が用意されていて便利。

ただテストをするときにちょっとめんどくさい点が2つある。

1つ目はファイルをアップロードするリクエストを作ること。

リクエストするデータとは別に $_FILES にも情報を突っ込んでおかないと、Controller で \Cake\Http\ServerRequest::getUploadedFile あたりを呼び出した時に値が取れなくなる。

もう1つは is_uploaded_file を stub にする必要があること。

uploadedFile の Validation はアップロードファイル判定に is_uploaded_file を使っており、テスト時にこの判定をくぐり抜けるのが難しい。

uploadedFile の Validation

Controller 内で↓のように書ける。便利。

$validator = new Validator();
$validator->uploadedFile(
  'my_file',
  ['types' => ['text/plain']], 
  'ファイル形式が正しくありません'
);
$errors = $validator->errors($this->request->getData());

オプションは mime type 以外にも、サイズ判定などがある。

テストで uploadedFile のリクエストを作る

uploadedFile を Controller のテスト (IntegrationTestTrait) で使う場合、 \Psr\Http\Message\UploadedFileInterface の実装インスタンスを生成してリクエストに乗せる。

\Zend\Diactoros\UploadedFile を使えばいいと思う。

このとき、リクエストデータとは別に $_FILES にもセットしておく必要がある。

直接代入してもいいのだが、 \Cake\TestSuite\IntegrationTestTrait::configRequestfiles いうキーで渡してあげると、結果的に同じことができるようだ。

$testFile = TESTS . 'Fixture' . DS . 'files' . DS . 'my_file.txt';
$uploadedFile = new UploadedFile(
    $testFile,
    10,
    UPLOAD_ERR_OK,
    'my_file.txt',
    'application/octet-stream'
);

// 別途設定する
$this->configRequest(
    [
        'files' => [
            'my_file'=> [
                'error' => $uploadedFile->getError(),
                'name' => $uploadedFile->getClientFilename(),
                'size' => $uploadedFile->getSize(),
                'tmp_name' => $testFile,
                'type' => $uploadedFile->getClientMediaType(),
            ]
        ]
    ]
);

// リクエスト
$this->post("your/api/", ['my_file' => $uploadedFile]);

ちなみにファイルの中身の確認は ext/finfo で 'tmp_name' に対して行われる。

実ファイルに対してチェックが行われるので、配置しておく必要がある。

is_uploaded_file を stub 化する

\Cake\Validation\Validation::uploadedFile が is_uploaded_file を使用しており、どうにも validation をテストで通過できなかったが、 stub 化することで対応できた。

このやり方は CakePHP 本体が同じことをやってる。

stub 用のファイルを用意して、テスト側で読み込めばOK。

// stub 側はこんな感じ
namespace Cake\Validation;

function is_uploaded_file($filename)
{
    return file_exists($filename);
}
// テスト側で stub を読み込む
require_once __DIR__ . DS . 'stubs.php';

参考