tohokuaikiのチラシの裏

技術的ネタとか。

EntryDateでゼロパディングしないで月日を表示する方法

www.movabletype.jp

普通にこのEntryDateのformatモディファイアを使うと…

<mt:EntryDate format="%Y年%m月%d日">

ってやると、2025年02月05日みたいにゼロパディングされる。

一桁で表示するフォーマット無いみたいです。

正規表現で置換してやればいいじゃん

ってことで、正規表現のグローバルモディファイアを使うよ。

こんな感じです。

<mt:EntryDate format="%Y年%m月%d日" regex_replace="/0([1-9])(?=[^0-9]|$)/g","$1">
  • s/0([1-9])(?=[^0-9]|$)/$1/g を使用。
  • 0([1-9]):0 に続く 1-9 の数字をキャプチャ。
  • (?=[^0-9]|$):次の文字が数字でない(例: å¹´ ã‚„ 日)または文字列の終端なら置換。

こんなのは勿論ChatGPTに考えてもらったよ!もう正規表現は自分で考えることが無くなってきたよ。

MovableTypeのDataAPIを叩こうとしたら以前はXMLRPCだったのでRest対応はどうするか困ってしまった件をなんとかする。

経緯

昔、MovableTypeのデータAPIを叩いて大量のデータを投稿するとかよくやってたんだけど、そのころはXMLRPCを使ってた。

イマドキは、Rest APIでしょ!ってんで、MovableTypeのデータAPI V6ではRestが主流っぽかった。別にXMLRPCも使えるんだろうけど。

ということで、PHPのGuzzlehttpを使ってたんだけど、認証系とデータの取得系は行けるけど、データの更新系が全然うまくいかずに困ってしまってワンワンワワーン。

JavaScriptのDataAPIのサンプルがあるよ。

DataAPI使ったサンプルないかなーって公式のページを見つけたんだけど、読んでもよーわからん…というかこれNodeでやんの?えー。とか思ってたらいいページがあった。これ10年前かよ!

knowledge.sakura.ad.jp

で作ってみた

こんな感じ。JavaScriptSDKはmt-staticフォルダにあるので、それを使う。今回の mt-staticのパスは ↓のコードを参照。

https://mt.example.com/test_data_api.html を作って、https://mt.example.com/mt.cgi のMovableTypeのデータAPIを叩くイメージ。MovableTypeの管理画面からAPIの許可を出しておいてくれ。

あと、Entryを投稿するには認証が必要なので、認証通ってない場合は「Login」のリンクをクリックしてID/Webサービスパスワード(管理画面へのログインPASSWORDではない!同じログイン画面を使うけどね)でログインする。

<html>
<head>
  <meta charset="UTF-8"/>
  <title>MT Data API</title>
  <script src="/mt-static/data-api/v6/js/mt-data-api.min.js"></script>
</head>
<body>
    <div id="entries"></div>
    <p><a href="#" id="login">Login</a></p>
    <p>UserName:<span id="username"></span></p>
    <button id="createEntry">Create Entry</button>
<script>
const siteId = 1;
var api = new MT.DataAPI({
  baseUrl:  "/mt-data-api.cgi",
  clientId: "your-client-id"
});

function goToAuthorizationUrl() {
  // 認証が成功した場合、現在の URL に戻ってくる
  const href = location.href;
  document.getElementById('login').href = api.getAuthorizationUrl(href);
}

if (! api.getTokenData()) {
  // トークンのデータを持っていない
  goToAuthorizationUrl();
}

api.listEntries(siteId, function(response) {
    let html = '';
    if (response.error) {
      html += "<p>データ取得エラーです。</p>";
      return;
    }

      html += "<ul>";

      for (var i = 0; i < response.items.length; i++) {
        var entry = response.items[i];
        html += "<li>" + entry.title + "</li>";
      }

      html += "</ul>";

      var entries = document.getElementById("entries");
      entries.innerHTML = html;

});

api.getUser('me', function(response) {
  if (response.error) {
    if (response.error.code === 401) {
      // 以前取得したトークンが期限切れになっているなどの理由で再認証が必要
      goToAuthorizationUrl();
      return;
    }

    // エラー処理
    return;
  }

  var user = response;

  document.getElementById('login').style.display = 'none';
  document.getElementById('username').textContent = user.displayName;
});

const entryData = {
    title : "entry from api",
    body: 'body sample'
};
document.getElementById('createEntry').addEventListener('click', ()=> {
api.createEntry(siteId, entryData, function(response) {
    console.log(response); 
});
});
</script>
</body>
</html>

POSTデータについて

で、上記のスクリプトを動かすと、POSTデータが以下のようになっている。(HTTPリクエストのボディ部分)

------WebKitFormBoundaryfgVhBpmmR6viaXNh
Content-Disposition: form-data; name="entry"

{"title":"entry from api","body":"body sample"}
------WebKitFormBoundaryfgVhBpmmR6viaXNh--

Content-Type: multipart/form-data; で送るのは知ってたけど、Content-Disposition: form-data; name="entry"これはなんぞ?そういえば、MT側でPOSTデータをチェックしている時にEntryってデータがないよってエラーが出てたんだよな…ここで指定するのか…

今までのPOSTデータ

今までGuzzlehttpで

<?php
        $response = $client->post($url, [
            'headers' => [
                'X-MT-Authorization'    => 'MTAuth accessToken=' . $access_token,
            ],
            'json' => [
                'entry' => [
                    'title' => 'sample entry title',
                    'body' => 'sample entry BODY',
                    'status' => 'Publish',
                    'date' => '2025-01-17T12:00:00Z',
                ],
                'publish' => 1,
            ],
        ]);

とかやってたけど、こんなJSONが返ってきた。

{"error":{"code":400,"message":"A resource \"entry\" is required."}}

Content-Disposition: form-data; name="entry"が含まれてないからMT側で「Entryデータが無い」って言われてたのか…

正しいPOSTデータ

こんな感じ。Body部分はmultipartで指定するのだった。

<?php
        $response = $client->post($url, [
            'headers' => [
                'X-MT-Authorization'    => 'MTAuth accessToken=' . $access_token,
            ],
            'multipart' => [
                [
                    'name'     => 'entry',
                    'contents' => '{"title":"entry from api(PHP)","body":"body sample"}',
                    ],
                ],
        ]);

PHPでSSLのCSR/CER/秘密鍵のmd5ハッシュ値を出す方法

作成したCSRと、認証局から発行されたCERと、秘密鍵が同一の流れのものか確認したい。

以前にこちらで使ったコマンドでmd5を取れば確認できる。 tohokuaiki.hateblo.jp

  • 作成したCSR openssl req -noout -modulus -in server.csr | openssl md5

  • 認証局から発行されたCER openssl x509 -noout -modulus -in server.crt | openssl md5

  • 秘密鍵が同一の流れのものか確認したい。 openssl rsa -noout -modulus -in servername.key | openssl md5

これをPHPでやる方法

sample.keyディレクトリに、それぞれcsr, cer, keyでファイルがあるとする。

$ ls -l sample.key
total 12
-rw-r--r-- 1 t-ito t-ito 1760 Jan 14 16:47 cer
-rw-r--r-- 1 t-ito t-ito 1077 Jan 14 16:48 csr
-rw-r--r-- 1 t-ito t-ito 1703 Jan 14 16:47 key

で、こんな感じ。

<?php
chdir(__DIR__.'/sample.key/');

echo "CSR:\n". opensslmd5(csr2modulus('csr'));
echo "CER:\n". opensslmd5(crt2modulus('cer'));
echo "KEY:\n". opensslmd5(key2modulus('key'));

// openssl req -noout -modulus -in server.csr と同じ
function csr2modulus($csr_file)
{
    $csrContent = file_get_contents($csr_file);
    $csrDetails = openssl_csr_get_public_key($csrContent);
    $keyDetails = openssl_pkey_get_details($csrDetails);
    if ($keyDetails === false) {
        exit;
    }
    // 公開鍵のモジュラスを取得
    $modulus = $keyDetails['rsa']['n']; // RSA鍵のモジュラス部分
    // モジュラスを16進数文字列として返す
    return 'Modulus='.strtoupper(bin2hex($modulus))."\n";
}


// openssl x509 -noout -modulus -in server.crt と同じ
function crt2modulus($crt_file)
{
    $certContent = file_get_contents($crt_file);
    $certDetails = openssl_x509_read($certContent);
    if ($certDetails === false) {
        exit;
    }
    $pubkeyResource = openssl_pkey_get_public($certDetails);
    $keyDetails = openssl_pkey_get_details($pubkeyResource);
    $modulus = $keyDetails['rsa']['n'];
    return 'Modulus='.strtoupper(bin2hex($modulus))."\n";
}

// openssl rsa -noout -modulus -in servername.key と同じ
function key2modulus($key_file)
{
    $keyContent = file_get_contents($key_file);
    $keyResource = openssl_pkey_get_private($keyContent);
    $keyDetails = openssl_pkey_get_details($keyResource);
    $modulus = $keyDetails['rsa']['n'];
    return 'Modulus='.strtoupper(bin2hex($modulus))."\n";
}

// openssl md5 と同じ
function opensslmd5($string)
{
    return 'MD5(stdin)= '.hash('md5', $string)."\n";
}

ついでにPHPでCSRを作成する方法とか

tohokuaiki.hateblo.jp

UXリサーチの始め方

1~4章まで

  • 量的リサーチ
    • アンケート
    • 利用ログ
  • 質的リサーチ
    • ユーザーインタビュー
  • 分け方
    • 探索(問題点や新しい洞察を探す)⇔検証(問題解決や立てた仮説が正しいか確認する)
    • 質的(思考や行動面をとらえる/仮説のきっかけとして。インタビュー)⇔量的(数値出力として集計ができる。仮説検証。アンケート)
    • レイヤー(難←戦略・要件・構造・骨格・表層→易)⇔タイムスパン(利用前・中・後)と全体(使い続けた時)
  • 始め方
    • 探索…何が課題かをインタビューやユーザービリティテストを通して。
    • 検証…すでに何らかの構想があって、それを資料やプロトタイプで提示してフィードバックを得る。
    • 既存データの活用…過去の調査結果やログデータの再解析
  • リサーチのステップ…状況理解→問い立案(課題設定)→手順設計(ガイドライン作成)→調査準備→調査実施(メイン)→データ分析→結果活用
    • 状況理解…状況・リソース・権限 何をしようとしているか。どれくらいの調査をしようとしているか必要なモノ。どこまでのことができるのか?
    • 問い立案(課題設定)…何が問題なのか?
    • 手順設計(ガイドライン作成)…
    • 調査準備…以下の項目をまとめて調査企画書にしておく。
      • 調査対象を想定…ペルソナの作成
      • 調査方法を規定…インタビューなら設問ごとの時間配分など
      • スケジュール化
      • 実施手順の作成…質問事項や実施内容ごとに「時間配分」「使う資料」「結果記録」を1枚のWordにまとめるする。
    • 調査実施(メイン)…
    • データ分析…
    • 結果活用…調査した結果が得られた場合、どのように活用するのかをある程度見越しておく。これが設計できないなら調査が無駄になる。絵に描いた餅
      • 調査や分析の同席
      • レポート…案件名・協力者概要・サマリ・協力者詳細・詳細な目的/資料/結果/考察、カスタマージャーニーマップ
      • 結果議論のワークショップ…結果を踏まえたりレポートでは伝えきれない情報を共有したりして、施策アイディア出し、カスタマージャーニーマップの理解検討
  • リサーチ手法
    • ユーザーインタビュー…量的情報を得たい場合にはアンケートを併用、
      • 質問項目…ユーザー自身の属性も→グルーピング&ラベリング→優先順位、を考えて。バイアスにならない質問順序
    • ユーザービリティテスト…5つの「使いやすさ」…学習しやすい・効率的に使える・操作を記憶しやすい・エラー対処・主観的満足度
      • リリース前(大きな課題のチェック)にも後(今後の機能改善)にも
      • 探索か検証かを意識する
      • シナリオ(ユースケース)からのタスク(操作してもらう内容)を考える。
      • 実施時間の目安(1件20~30分)
      • 実施人数は5人で85%のカバレッジ
      • 手順…事前インタビュー(タスクの背景説明、ユーザーの使用歴経験)・タスク実行・事後インタビュー(感想)
      • 実施時ポイント…思考発話(操作しながらしゃべってもらう)観察(間違った操作もUXのうち)多くの目撃者で
      • タスクの順序には注意する…タスクの相互関係がUXに影響を及ぼすことがある→例:A/Bテストを同一人物で行うのは難しい
    • コンセプトテスト…実際に作成する前にユーザーニーズを把握する、スライド、プリントなどで説明して反応を聞く。戦略段階で行う。
      • 事前インタビュー→コンセプト提示→評価(5段階評価で「使いたいか?」「わかりやすいか?」など)
      • ユーザーによって求めるものは全く違うことに注意
    • アンケート
    • フィールド調査…実際に使われいてる現場に赴いて調査
  • 分析手法…質的データは分析者によって全く異なる結果になりやすいことに留意して分析する。
    • KA法…(出来事→心の声→価値の想定)を1つのカードにし、カードからモデリングする。
    • SCAT…インタビューのまとめ方。発言テキストから、注目語句→言い換え→説明できる概念→テーマ化→課題発見、小さな質的データ
    • mGTA…インタビューからキーワードを見つけて概念化→カテゴリ化→モデル像を抽出する。
    • KJ法…意見をラベルに書き出す→グループ化しグループ名を作る→グループ間の関係性を見出す→関係性をストーリーにし新しいアイディアを作る。
    • ペルソナ…本サービスを必要としている人物像の具象化。関係者間での意思疎通をスムーズにしやすい。ペルソナごとのボリュームも検討、サービスの時間推移につきペルソナのアップデートもする。
    • カスタマージャーニーマップ…ユーザーの時間軸・行動の変遷(認知・情報収集・来店・試着・購入)を横軸に、縦軸に横軸ごとの顧客の視点/企業の視点からの要素(具体的な行動・感情・ニーズ・顧客接点・課題・対応施策)を想定する。新たな課題や優先度をとらえる。

acmephp/sslを使ってPHPでSSLの秘密鍵ファイル・CSRファイルを発行する方法

このライブラリであっという間にできてしまった。

acmephp.github.io

acmephp/sslはバージョン2.1、PHPは8.3.6でopensslモジュールは必須。

やりたいこと

以下の2つ

使い方

使い方というか、コード見た方が早そう。

とりあえずcomposerでインストール

composer require acmephp/ssl

コード

<?php
require_once __DIR__."/vendor/autoload.php";

use AcmePhp\Ssl\CertificateRequest;
use AcmePhp\Ssl\DistinguishedName;
use AcmePhp\Ssl\Generator\KeyPairGenerator;
use AcmePhp\Ssl\Signer\CertificateRequestSigner;
use AcmePhp\Ssl\Generator\RsaKey\RsaKeyOption;

// キー長の設定
$keyOption = new RsaKeyOption(2048);
$keyPairGenerator = new KeyPairGenerator();

// ノーパスの秘密鍵ファイルを作成
$keyPair = $keyPairGenerator->generateKeyPair($keyOption);
$pem = $keyPair->getPrivateKey()->getPEM();
file_put_contents('servername_nopass.key', $pem);

// ノーパスの秘密鍵ファイルからパスフレーズ有の秘密鍵ファイルを作成
// パスフレーズの設定
$passphrase = 'my-pass-phrase';
openssl_pkey_export($pem, $output, $passphrase);
file_put_contents('servername.key', $output);

// CSRの発行に必要なパラメータ
$distinguishedName = new DistinguishedName(
    'example.com',  // Common Name (CN)
    'US',           // Country (C)
    'California',   // State (ST)
    'San Francisco',// Locality (L)
    'Example Inc.', // Organization (O)
    'IT Department' // Organizational Unit (OU)
);
// CSRファイルの作成
$certificateRequest = new CertificateRequest($distinguishedName, $keyPair);
$export = (new CertificateRequestSigner)->signCertificateRequest($certificateRequest);
file_put_contents('server.csr', $export);

作ったCSRと認証局からもらったCER証明書のmd5もPHPでチェックできる。

tohokuaiki.hateblo.jp

TypeScriptでデータを元に重複を見てループする方法

俺のコード

type Job = Record<'area' | 'act' | 'type' | 'num' | 'days' | 'limit', string>;
type Filter = Record<'area' | 'act' | 'type', string[]>;

const filter:Filter = { area: [], act: [], type: [] };
(async () => {
    const resp = await fetch('https://exmple.com/api/jobs');
    const jobs = await resp.json() as Job[];
     jobs.map((job) => {
         if (filter.area.indexOf(job.area) < 0) filter.area.push(job.area);
         if (filter.act.indexOf(job.act) < 0) filter.act.push(job.act);
         if (filter.type.indexOf(job.type) < 0) filter.type.push(job.type);
    });
})();

ChatGPTに質問

このTypeScriptをもっと行数を少なく書けますか?特にmapのループが短くなれるとよいです。

ChatGPTの回答

改善点:

  • filterオブジェクトに対するpush処理は重複チェックを省略してシンプルにできます。
  • mapを使用するのではなく、forEachを使用して直接filterに値を追加できます。
  • indexOfの代わりにSetを使うと、重複のチェックと追加が高速で簡潔になります。

以下は改善後のコードです:

type Job = Record<'area' | 'act' | 'type' | 'num' | 'days' | 'limit', string>;
type Filter = Record<'area' | 'act' | 'type', Set<string>>;

const filter: Filter = { area: new Set(), act: new Set(), type: new Set() };

(async () => {
    const resp = await fetch('https://exmple.com/api/jobs');
    const jobs = await resp.json() as Job[];

    jobs.forEach((job) => {
        filter.area.add(job.area);
        filter.act.add(job.act);
        filter.type.add(job.type);
    });
})();

改善のポイント:

この方法で、より効率的で簡潔なコードを実現できます。

Setを使用:

Setを使うことで、重複チェックが簡単になります。Setは自動的に重複を排除するため、indexOfを使って手動でチェックする必要がありません。 add()メソッドで要素を追加するだけで、重複が自動的に排除されます。

forEachを使用:

mapは配列を新しく返すため、返り値を使わない場合はforEachが適切です。mapの返り値を無視する場合、forEachで直接処理する方が簡潔です。

ReactでModalを消すときにフォーカスが消える要素に当たってますよってエラーが出たので何とかしてもらった。

こんなエラーが出た。

Blocked aria-hidden on an element because its descendant retained focus. The focus must not be hidden from assistive technology users. Avoid using aria-hidden on a focused element or its ancestor. Consider using the inert attribute instead, which will also prevent focus. For more details, see the aria-hidden section of the WAI-ARIA specification at https://w3c.github.io/aria/#aria-hidden.
Element with focus: button
Ancestor with aria-hidden: <div class=​"modal fade" tabindex=​"-1" style=​"display:​ block;​" aria-hidden=​"true" >​…​</div>​

Modalが消える要素なのに、フォーカスが当たっていたらダメですよと。

手っ取り早くcloseする時にフォーカスをモーダルの親コンポーネントのボタンとかにRefを当ててそこにしたら消えた。

import React, { useRef, useState } from 'react';

const MyModal = () => {
  const [isOpen, setIsOpen] = useState(false);
  const triggerRef = useRef<HTMLButtonElement>(null);

  const openModal = () => setIsOpen(true);
  const closeModal = () => {
    setIsOpen(false);
    triggerRef.current?.focus(); // フォーカスを元のトリガーボタンに戻す
  };

  return (
    <div>
      <button ref={triggerRef} onClick={openModal}>
        Open Modal
      </button>
      {isOpen && (
        <div
          role="dialog"
          aria-modal="true"
          style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0, 0, 0, 0.5)' }}
        >
          <div style={{ background: 'white', padding: '20px' }}>
            <p>Modal Content</p>
            <button onClick={closeModal}>Close</button>
          </div>
        </div>
      )}
    </div>
  );
};

export default MyModal;

ただ、データ一覧があって各データにボタンがあったりそのModalをトリガーしたボタンが消えてしまっていては意味がない。

そこで、こんな感じにする。ポイントは、

  • DIV要素にtabIndexを付けてフォーカス可能にする。
  • focus({ preventScroll: true })でフォーカス時にスクロールしないようにする。
import React, { useRef, useState } from 'react';

const MyModal = () => {
  const [isOpen, setIsOpen] = useState(false);
  const triggerRef = useRef<HTMLDivElement>(null); // ボタン以外の要素

  const openModal = () => setIsOpen(true);

  const closeModal = () => {
    setIsOpen(false);
    triggerRef.current?.focus({ preventScroll: true }); // tabindexが設定されていれば問題なく動作
  };

  return (
    <div>
      <div
        ref={triggerRef}
        tabIndex={0} // フォーカス可能にする
        onClick={openModal}
        style={{
          display: 'inline-block',
          padding: '10px',
          background: 'lightblue',
          cursor: 'pointer',
        }}
      >
        Open Modal (Non-button Element)
      </div>
      {isOpen && (
        <div
          role="dialog"
          aria-modal="true"
          style={{
            position: 'fixed',
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            background: 'rgba(0, 0, 0, 0.5)',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
          }}
        >
          <div style={{ background: 'white', padding: '20px', borderRadius: '8px' }}>
            <p>Modal Content</p>
            <button onClick={closeModal}>Close</button>
          </div>
        </div>
      )}
    </div>
  );
};

export default MyModal;

というのをChatGPTに教えてもらった。

React Google Mapsを使ってて、PINを打とうとするとなんか微妙にずれる件

visgl.github.io

Pinを使ってラップするとうまい具合に中のピンも寄ってくれるので、GoogleマップのPinの要素は全部透明化してやればOK

            <AdvancedMarker position={{ lat: point.lat, lng: point.lng }} ref={ref} onClick={handleClick}>
                <Pin borderColor="transparent" background="transparent" glyphColor="transparent">
                    <div className="landmark">
                        <div className="icon">
                            <img src="/img/store_logo.png" />
                        </div>
                    </div>
                </Pin>
            </AdvancedMarker >

あとは、CSSでちょいちょいと。自分が使ったのはこんな感じ。

.landmark {
  position: absolute;
  top: -24px;
  left: -7px;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.icon {
  position: relative;
  cursor: pointer;
  display:  inline-block;
  width: 40px;
  height: 40px;
  &::after {
    position: absolute;
    content: "";
    display: block;
    background: #000000;
    clip-path: polygon(0 0, 100% 0, 50% 100%);
    left: 3.5%;
    height: calc(tan(60deg)* 100% / 2);
    width: 95%;
    top: 66%;
  }
  img {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 5;
  }
}

Laravel Breeze(Inertia.jsは+React)でユーザーのパスワード変更をしたらTokenがマッチしなくなった件

環境

  • Laravel11 + Breeze + Inertia.js + React でユーザーログイン周りのScaffoldを作った。
  • 操作は全てReactからのAPIコール
  • AuthはStatefulAPIを使って、Sanctumで行う。Routeにミドルウェアを Route::middleware('auth:sanctum')って感じ。
  • ログイン後にTokenを取得してCOOKIEにセットしている。
    useEffect(() => {
        (async () => {
            await fetch(route('sanctum.csrf-cookie'))
        })();
    }, []);
  • axiosでAjaxする前に、COOKIEのTokenã‚’HTTPヘッダに忍ばせている。
    const [cookies, setCookie, removeCookie] = useCookies(['XSRF-TOKEN']);
    axios.interceptors.request.use(config => {
        config.headers!['X-XSRF-TOKEN'] = cookies['XSRF-TOKEN']
        return config;
    }, error => {
        return Promise.reject(error)
    })

再現方法

  1. ログインする。
  2. sanctumで保護されているAPIを叩くページに移動しても問題ない。
  3. プロフィールページ http://localhost:8000/profileからパスワードを変更
  4. sanctumで保護されているAPIを叩くページに移動すると、401 unauthorizedを返される。

理由

セッションTokenとブラウザから送られるTokenが一致していない。

tokensMatchメソッドでセッションToken$request->session()->token() とRequestからのToken $this->getTokenFromRequest($request); が違うのでfalseを返す。

<?php
    protected function tokensMatch($request)
    {
        $token = $this->getTokenFromRequest($request);

        return is_string($request->session()->token()) &&
               is_string($token) &&
               hash_equals($request->session()->token(), $token);
    }

セッション側がプロフィールページでパスワードを変更した際に更新されているのにRequestトークンがそれに追いついていない。ちなみに、Requestトークンは X-CSRF-TOKEN または X-XSRF-TOKEN ヘッダを見ている。

理由その2

なぜ一致しないかというと、PasswordControllerが成功時に back() をレスポンスしている。…からじゃないかな?

<?php
class PasswordController extends Controller
{
    /**
     * Update the user's password.
     */
    public function update(Request $request): RedirectResponse
    {
        $validated = $request->validate([
            'current_password' => ['required', 'current_password'],
            'password' => ['required', Password::defaults(), 'confirmed'],
        ]);

        $request->user()->update([
            'password' => Hash::make($validated['password']),
        ]);

        return back();
    }
}

解決方法(失敗)

  • AjaxでAPI的に使っているので、back()を使わないようにする。
  • Jsonを返して処理するだけにしたいけど、resources/js/Pages/Profile/Partials/UpdatePasswordForm.tsx は ControllerがInertia::render() を返してくるのを期待しているので使わないようにする。自前でaxiosで処理する。

…と思ってたら、やっぱり駄目だった。

どうも、passwordにUPDATEをかけると何かLaravelがセッションかTokenの処理をするっぽい。nameの更新だけだと問題なかった。

ということで、Passwordまわりで何か特別な処理をしていないかを探る。

src/vendor/laravel/framework/src/Illuminate/Session/Middleware/AuthenticateSession.phpの public function handle($request, Closure $next)なんかこのあたりでやってそうだなー。

もう面倒だから、「パスワード変更したら再ログイン必要」ってことにしちゃおう。

VSCodeでLaravelを使っているとintelephenseが"Undefined method 'xxxxx'.(P1013)"のWarningを出して困っている

たとえばこんなコードで

<?php
        $sanctum_user = Auth::guard('sanctum')->user();
        if (in_array(HasApiTokens::class, class_uses($sanctum_user))){
            // @phpcs:ignore
            $sanctum_user->tokens()->delete();
        }

Undefined method 'tokens'.intelephense(P1013) とか出る。ChatGPTに聞いて // @phpcs:ignore とか入れたが勿論こんなのは屁のツッパリにもならない。

ググっても、WarningをOFFにするんだよとか「それ違うだろ。力技はいやなんだよ。」って回答しか目につかない。

力技との誹りは免れないが call_user_funcを使う

call_user_funcとはこんなんな。もはや古のPHPerしか知らない関数かもしれないが。

<?php
class Foo {
    function foo($param = [1,2,3]){
        var_dump($param); 
        return $this;
    }
    
    function bar() {
        echo "bar\n";
    }
}

$f = new Foo;
call_user_func([$f, 'foo'])->bar();

ということで、さっきのコードは

<?php
        $sanctum_user = Auth::guard('sanctum')->user();
        if (in_array(HasApiTokens::class, class_uses($sanctum_user))){
            // @phpcs:ignore
            call_user_func([$sanctum_user, 'tokens'])->delete();
        }

となる。これでWarningが出ない。むしろ可読性下がっとるやないか!!!との意見はその通りである!!