From 9e9476223a1425aed5ff9f6eaa77adb92803c8e1 Mon Sep 17 00:00:00 2001 From: gakigaki Date: Thu, 11 Sep 2025 15:53:51 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(databases):=20=E7=B7=A8=E9=9B=86?= =?UTF-8?q?=E6=99=82=E3=81=AE=E5=BF=85=E9=A0=88=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E5=86=8D=E3=82=A2=E3=83=83=E3=83=97=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=83=89=E4=B8=8D=E8=A6=81=E5=8C=96=EF=BC=88ImplicitR?= =?UTF-8?q?ule=E5=B0=8E=E5=85=A5=EF=BC=89+=20=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../User/Databases/DatabasesPlugin.php | 16 ++- app/Rules/CustomValiRequiredFileKeep.php | 132 ++++++++++++++++++ .../Rules/CustomValiRequiredFileKeepTest.php | 132 ++++++++++++++++++ 3 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 app/Rules/CustomValiRequiredFileKeep.php create mode 100644 tests/Unit/Rules/CustomValiRequiredFileKeepTest.php diff --git a/app/Plugins/User/Databases/DatabasesPlugin.php b/app/Plugins/User/Databases/DatabasesPlugin.php index 5a641c0f0..8f548154d 100644 --- a/app/Plugins/User/Databases/DatabasesPlugin.php +++ b/app/Plugins/User/Databases/DatabasesPlugin.php @@ -35,6 +35,7 @@ use App\Rules\CustomValiCsvImage; use App\Rules\CustomValiCsvExtensions; use App\Rules\CustomValiWysiwygMax; +use App\Rules\CustomValiRequiredFileKeep; use App\Plugins\User\UserPluginBase; @@ -1202,7 +1203,12 @@ private function getValidatorRule($validator_array, $databases_column) $validator_rule = null; // 必須チェック if ($databases_column->required) { - $validator_rule[] = 'required'; + if (DatabasesColumns::isFileColumnType($databases_column->column_type)) { + // ファイル系の必須は「既存ファイルが残るなら再アップ不要」の独自ルール + $validator_rule[] = new CustomValiRequiredFileKeep($databases_column->id); + } else { + $validator_rule[] = 'required'; + } } // メールアドレスチェック if ($databases_column->column_type == DatabaseColumnType::mail) { @@ -1263,12 +1269,16 @@ private function getValidatorRule($validator_array, $databases_column) } // 画像チェック if ($databases_column->column_type == DatabaseColumnType::image) { - $validator_rule[] = 'nullable'; + if (!$databases_column->required) { + $validator_rule[] = 'nullable'; + } $validator_rule[] = 'image'; } // 動画チェック if ($databases_column->column_type == DatabaseColumnType::video) { - $validator_rule[] = 'nullable'; + if (!$databases_column->required) { + $validator_rule[] = 'nullable'; + } $validator_rule[] = 'mimes:mp4'; } // wysiwygチェック diff --git a/app/Rules/CustomValiRequiredFileKeep.php b/app/Rules/CustomValiRequiredFileKeep.php new file mode 100644 index 000000000..bfa5c5287 --- /dev/null +++ b/app/Rules/CustomValiRequiredFileKeep.php @@ -0,0 +1,132 @@ +column_id = $column_id; + } + + /** + * バリデーション本体 + * + * @param string $attribute 対象属性名(例: databases_columns_value.) + * @param mixed $value 入力値 + * @return bool 検証結果 + */ + public function passes($attribute, $value) + { + if ($this->hasNewUpload($attribute)) { + return true; + } + + $row_id = $this->getRowIdFromRoute(); + if (empty($row_id)) { + // 新規登録はアップロード必須 + return false; + } + + // 編集時:既存ファイルがあり、削除チェックが無ければOK + if ($this->hasExistingFile($row_id) && !$this->isDeleteChecked()) { + return true; + } + + // 既存が無い、または削除チェックありで新規アップ無しはNG + return false; + } + + /** + * 新規アップロード有無の判定 + * + * @param string $attribute 属性名 + * @return bool アップロードがあれば true + */ + private function hasNewUpload(string $attribute): bool + { + return request()->hasFile($attribute); + } + + /** + * ルートパラメータから行ID(databases_inputs.id)を取得 + * + * @return int|null 行ID。未指定の場合は null + */ + private function getRowIdFromRoute(): ?int + { + $route = request()->route(); + if (!$route) { + return null; + } + $id = $route->parameter('id'); + if ($id === null) { + return null; + } + return is_numeric($id) ? (int) $id : null; + } + + /** + * 既存ファイルの有無をDBで確認 + * + * @param int $row_id databases_inputs.id + * @return bool 既存ファイルがあれば true + */ + private function hasExistingFile(int $row_id): bool + { + $existing = DatabasesInputCols::where('databases_inputs_id', $row_id) + ->where('databases_columns_id', $this->column_id) + ->first(); + return $existing && !empty($existing->value); + } + + /** + * 削除チェックが付いているか判定 + * + * @return bool 削除チェック済みなら true + */ + private function isDeleteChecked(): bool + { + $delete_column_ids = request()->input('delete_upload_column_ids', []); + return is_array($delete_column_ids) && array_key_exists($this->column_id, $delete_column_ids); + } + + /** + * バリデーションメッセージ + * + * @return string + */ + public function message() + { + return ':attribute は必須です。'; + } +} diff --git a/tests/Unit/Rules/CustomValiRequiredFileKeepTest.php b/tests/Unit/Rules/CustomValiRequiredFileKeepTest.php new file mode 100644 index 000000000..27b72663e --- /dev/null +++ b/tests/Unit/Rules/CustomValiRequiredFileKeepTest.php @@ -0,0 +1,132 @@ +row_id = $row_id; } + public function parameter($key) { return $key === 'id' ? $this->row_id : null; } + }; + $request->setRouteResolver(fn () => $route); + + // 現在のrequest() を差し替え + $this->app->instance('request', $request); + } + + /** + * 新規: 未アップロードはエラー + */ + public function testNewRecordWithoutUploadFails() + { + $column_id = 55; + $rule = new CustomValiRequiredFileKeep($column_id); + + $request = Request::create('/dummy', 'POST'); + // id パラメータ未設定(新規) + $this->bindRequest($request, null); + + $this->assertFalse($rule->passes("databases_columns_value.$column_id", null)); + } + + /** + * 編集: 既存ファイルあり、削除チェックなし、アップロードなし → 成功 + */ + public function testEditWithExistingNoDeletePasses() + { + $column_id = 55; + $inputs_id = 123; + // 既存ファイルあり + DatabasesInputCols::withoutEvents(function () use ($inputs_id, $column_id) { + DatabasesInputCols::create([ + 'databases_inputs_id' => $inputs_id, + 'databases_columns_id' => $column_id, + 'value' => 999, // uploads.id を想定した非NULL値 + ]); + }); + + $rule = new CustomValiRequiredFileKeep($column_id); + + $request = Request::create('/dummy', 'POST'); + $this->bindRequest($request, $inputs_id); + + $this->assertTrue($rule->passes("databases_columns_value.$column_id", null)); + } + + /** + * 編集: 既存ファイルあり、削除チェックあり、アップロードなし → エラー + */ + public function testEditWithExistingAndDeleteFails() + { + $column_id = 55; + $inputs_id = 123; + // 既存ファイルあり + DatabasesInputCols::withoutEvents(function () use ($inputs_id, $column_id) { + DatabasesInputCols::create([ + 'databases_inputs_id' => $inputs_id, + 'databases_columns_id' => $column_id, + 'value' => 999, + ]); + }); + + $rule = new CustomValiRequiredFileKeep($column_id); + + // 削除チェックあり(キー・値に同じIDが入る送信形) + $request = Request::create('/dummy', 'POST', [ + 'delete_upload_column_ids' => [ (string)$column_id => (string)$column_id ], + ]); + $this->bindRequest($request, $inputs_id); + + $this->assertFalse($rule->passes("databases_columns_value.$column_id", null)); + } + + /** + * 新規アップロードがあれば成功 + */ + public function testNewUploadPasses() + { + $column_id = 55; + $inputs_id = 123; + + $rule = new CustomValiRequiredFileKeep($column_id); + + // 新規アップロードを擬似 + $file = UploadedFile::fake()->create('sample.txt', 1, 'text/plain'); + $request = Request::create('/dummy', 'POST', [], [], [ + 'databases_columns_value' => [ (string)$column_id => $file ], + ]); + $this->bindRequest($request, $inputs_id); + + $this->assertTrue($rule->passes("databases_columns_value.$column_id", null)); + } +} From c547f9a8c6a7d45c3862f11e29a36b1c829a8d50 Mon Sep 17 00:00:00 2001 From: gakigaki Date: Thu, 11 Sep 2025 15:58:43 +0900 Subject: [PATCH 2/2] improve: phpcs --- tests/Unit/Rules/CustomValiRequiredFileKeepTest.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/Unit/Rules/CustomValiRequiredFileKeepTest.php b/tests/Unit/Rules/CustomValiRequiredFileKeepTest.php index 27b72663e..af1ddd566 100644 --- a/tests/Unit/Rules/CustomValiRequiredFileKeepTest.php +++ b/tests/Unit/Rules/CustomValiRequiredFileKeepTest.php @@ -35,8 +35,14 @@ private function bindRequest(Request $request, ?int $row_id = null): void // ルート風のスタブを用意し、parameter('id') を返す $route = new class($row_id) { private $row_id; - public function __construct($row_id) { $this->row_id = $row_id; } - public function parameter($key) { return $key === 'id' ? $this->row_id : null; } + public function __construct($row_id) + { + $this->row_id = $row_id; + } + public function parameter($key) + { + return $key === 'id' ? $this->row_id : null; + } }; $request->setRouteResolver(fn () => $route);