2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

チェックボックスを外すとキーごと送信されないので、同じ設定を2つの画面から保存すると、どちらかで勝手に切れます【PHP の $_POST で実測】

2
Posted at

チェックを外したチェックボックスは、フォームから何も送られません。値が 0 で送られるのではなく、キーそのものがありません。そのため、同じ設定を2つの画面のフォームから保存していると、スイッチの無いほうの画面で保存しただけで、その設定が切れることがあります。Chromium(Playwright)でフォームを送信し、PHP の $_POST に何が入るかを測りました。

書き方 チェック 送信された本文 $_POST['s']
A. チェックボックスだけ する s[other]=x&s[enabled]=1 {"other":"x","enabled":"1"}
〃 外す s[other]=x {"other":"x"}(キーが無い)
B. 同じ名前の hidden(値0)を前に置く する …&s[enabled]=0&s[enabled]=1 enabled: "1"
〃 外す …&s[enabled]=0 enabled: "0"
C. 同じ名前の hidden(値0)を後ろに置く する …&s[enabled]=1&s[enabled]=0 enabled: "0"(チェックしたのに0)
〃 外す …&s[enabled]=0 enabled: "0"
D. 目印 s[_toggle]=1 を一緒に送る する …&s[_toggle]=1&s[enabled]=1 _toggle: "1", enabled: "1"
〃 外す …&s[_toggle]=1 _toggle: "1"(enabled は無い)

同じ名前が2回送られると、PHP は後ろの値で上書きします。B は、それを使って「外したら0」を作る書き方です。hidden を後ろに置いた C では、チェックしても0になりました。

先に断っておくと、チェックされていないチェックボックスが送信されないのは、HTML の仕様どおりです。フォームの送信内容を組み立てる手順(constructing the entry list)で、チェックされていないチェックボックスは飛ばされます。ここで書くのは、それを前提にせずに同じ設定を2つの画面に置いた自作の WordPress プラグインで何が起きたかと、書き方ごとに実際に送って測った結果です。

何が起きたか

自作の WordPress プラグイン Rapls AI Chatbot には、サイトの中身を読み込む「サイト学習」のオン・オフのスイッチがあります。読み込む仕組みは無料版に入っているのに、スイッチは Pro 版が出す画面にしかありませんでした。無料版で一度オフにすると、戻す場所がありません。

1.21.1 で、無料版の設定画面にもこのスイッチを置きました。すると、同じ crawler_enabled を、2つの画面のフォームが保存することになります。しかも、どちらのフォームも、ほかの設定と一緒に1つの配列(raplsaich_settings)として保存します。

保存する側の3つの書き方

保存のときに「キーが無い」をどう読むかで、結果が変わります。3通りを並べました。

$policies = [
  '1 無ければ off'
      => fn($in, $saved) => !empty($in['enabled']),
  '2 無ければ保存値のまま'
      => fn($in, $saved) => array_key_exists('enabled', $in) ? !empty($in['enabled']) : $saved,
  '3 目印があれば、無い=off'
      => fn($in, $saved) => !empty($in['_toggle'])
             ? !empty($in['enabled'])
             : (array_key_exists('enabled', $in) ? !empty($in['enabled']) : $saved),
];

保存値がオンの状態から、3つの操作をしたときの結果です。

操作 1 無ければ off 2 無ければ保存値のまま 3 目印で判断
スイッチの画面でチェックを外して保存 off on のまま(切れない) off
スイッチの画面でチェックして保存 on on on
スイッチの無い別の画面で保存 off(勝手に切れる) on on

1 は、スイッチの無い画面で別の設定を保存しただけで、オフになります。2 は、その事故は起きませんが、スイッチの画面でチェックを外してもオフにできません。外したチェックボックスは何も送らないので、「外した」と「この画面にはスイッチが無い」が区別できないからです。

3 だけが、どちらも正しく扱えました。

1.21.1 の直し方

スイッチを置いた画面では、チェックボックスと一緒に、送られても保存はしない目印を送ります。

<input type="hidden" name="raplsaich_settings[_site_learning_toggle]" value="1">
<input type="checkbox" name="raplsaich_settings[crawler_enabled]" value="1"
    <?php checked(!empty($settings['crawler_enabled'])); ?>>

保存の処理では、目印があるとき(Pro 版の画面では、その画面にしかない crawler_interval があるとき)だけ、「キーが無い=オフ」と読みます。どちらも無いフォームからの保存では、保存値を変えません。

$crawler_form_submitted = array_key_exists('crawler_interval', $input)
    || !empty($input['_site_learning_toggle']);

目印そのものは、保存する配列には入れません。

B(hidden を前に置く)では足りないのか

B でも、「外したら0」は送れます。1つの画面にしかスイッチが無いなら、B で十分です。

ただ、B が送るのは「この画面にはスイッチがあり、その値は0」という情報です。2つの画面のうち片方にしか B が無ければ、もう片方からの保存では、やはりキーが無いまま届きます。保存する側は結局、「キーが無いときにどうするか」を決めなければなりません。1.21.1 が選んだのは、目印で「この画面はスイッチを持っている」と明示する D でした。

もう1つ、B は hidden の位置を間違えると C になり、チェックしても0が保存されます。位置に意味を持たせる書き方は、あとから画面を直したときに壊れやすい。

チェックボックスを保存するとき

  • チェックを外したチェックボックスは、キーごと送られない。0 は来ない
  • 同じ設定を複数の画面から保存するなら、「キーが無い」を、外したのか、その画面に無いのかで分ける
  • 分けるには、スイッチを持つ画面だけが送る目印を足す
  • 同じ名前の hidden で0を送る書き方は、hidden を前に置く。後ろに置くと、チェックしても0になる

あなたのプラグインに、同じ設定を2つのフォームから保存している画面はありませんか。

手元で試すコマンド

PHP のビルトインサーバーと Playwright で動きます。

<?php
// post.php:受け取った本文と $_POST をそのまま返す
header('Content-Type: application/json');
echo json_encode(['body' => file_get_contents('php://input'), 'post' => $_POST], JSON_UNESCAPED_UNICODE);
// run.js
const { chromium } = require('playwright');
const forms = {
  'A そのまま': `<input type=checkbox name="s[enabled]" value=1 id=c>`,
  'B hidden を前に': `<input type=hidden name="s[enabled]" value=0><input type=checkbox name="s[enabled]" value=1 id=c>`,
  'C hidden を後ろに': `<input type=checkbox name="s[enabled]" value=1 id=c><input type=hidden name="s[enabled]" value=0>`,
  'D 目印': `<input type=hidden name="s[_toggle]" value=1><input type=checkbox name="s[enabled]" value=1 id=c>`,
};
(async () => {
  const b = await chromium.launch(); const p = await b.newPage();
  for (const [name, inner] of Object.entries(forms)) for (const checked of [true, false]) {
    await p.setContent(`<form method=post action="http://127.0.0.1:8765/post.php"><input type=hidden name="s[other]" value=x>${inner}<button id=go>保存</button></form>`);
    if (checked) await p.check('#c');
    const [res] = await Promise.all([p.waitForResponse(r => r.url().includes('post.php')), p.click('#go')]);
    const j = await res.json();
    console.log(name, checked ? 'チェック' : '外す', decodeURIComponent(j.body), JSON.stringify(j.post.s));
  }
  await b.close();
})();
php -S 127.0.0.1:8765 &
node run.js

ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。
https://raplsworks.com/

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?