Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions app/Plugins/User/Databases/DatabasesPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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チェック
Expand Down
132 changes: 132 additions & 0 deletions app/Rules/CustomValiRequiredFileKeep.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\ImplicitRule;
use App\Models\User\Databases\DatabasesInputCols;

/**
* データベース・プラグインのファイル必須バリデーション
*
* 概要:
* - 新規登録時: 必須(アップロードが必要)
* - 編集時 : 既存ファイルが残っており、削除チェックが入っていなければ再アップロードは不要
*
* 用途:
* - ファイル/画像/動画などのファイル系カラムで、`required` の代わりに利用する
*/
/**
* このルールは ImplicitRule として実行され、
* フィールドが未送信(存在しない)場合でも評価されます。
*/
class CustomValiRequiredFileKeep implements ImplicitRule
{
/**
* 対象のカラムID(databases_columns.id)
*
* @var int
*/
private $column_id;

/**
* コンストラクタ
*
* @param int $column_id データベースカラムID(databases_columns.id)
*/
public function __construct(int $column_id)
{
$this->column_id = $column_id;
}

/**
* バリデーション本体
*
* @param string $attribute 対象属性名(例: databases_columns_value.<column_id>)
* @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 は必須です。';
}
}
138 changes: 138 additions & 0 deletions tests/Unit/Rules/CustomValiRequiredFileKeepTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

namespace Tests\Unit\Rules;

use App\Models\User\Databases\DatabasesInputCols;
use App\Rules\CustomValiRequiredFileKeep;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Routing\Route;
use Tests\TestCase;

/**
* CustomValiRequiredFileKeep ルールのユニットテスト
*
* 検証観点:
* - 新規: 未アップロードはエラー
* - 編集: 既存あり+削除なしは成功
* - 編集: 既存あり+削除ありはエラー
* - 新規アップロードがあれば成功
*/
class CustomValiRequiredFileKeepTest extends TestCase
{
use RefreshDatabase;

/**
* Request に擬似ルートを紐づけ、グローバル request() を差し替える
*
* @param Request $request 擬似リクエスト
* @param int|null $row_id ルートパラメータ id(null で新規)
* @return void
*/
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;
}
};
$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));
}
}