Files
Gnuboard7/app/Models/Concerns/UserOverridesAwareBuilder.php
2026-04-20 20:37:49 +09:00

104 lines
3.7 KiB
PHP

<?php
namespace App\Models\Concerns;
use Illuminate\Database\Eloquent\Builder;
/**
* user_overrides 인지 Eloquent Builder.
*
* `HasUserOverrides` trait 이 적용된 모델의 mass update 를 가로채 user_overrides
* 컬럼에 수정 필드를 자동 누적합니다.
*
* 동작 흐름:
* - `Model::where(...)->update([...])` 호출 시
* - 대상 행을 preload (1 SELECT)
* - 각 행별로 `calculateUserOverridesFor($values)` 호출하여 user_overrides 계산
* - per-row UPDATE 를 parent::update() 로 수행 (재귀 없이)
* - 시더 컨텍스트(`user_overrides.seeding`) 에서는 기본 경로 유지
* - 입력 $values 에 trackable 필드가 하나도 없으면 계산 건너뛰고 기본 경로 (성능 최적화)
*
* 성능 트레이드오프:
* - 기본: 1 UPDATE
* - 추적 활성: 1 SELECT + N UPDATE (trackable 필드 포함 시에만)
* - 관리자 UI 일괄 저장(수십 행 이하) 대상에는 차이 무시 가능.
* - 대량 처리는 `DB::table()->update()` (Eloquent 우회) 권장.
*
* @since 7.0.0-beta.2
*/
class UserOverridesAwareBuilder extends Builder
{
/**
* 쿼리 조건에 부합하는 모델을 업데이트합니다.
*
* @param array<string, mixed> $values
* @return int 영향받은 행 수
*/
public function update(array $values)
{
// 시더 컨텍스트에서는 trait 이벤트 handler 가 어차피 early return 하므로
// 성능을 위해 기본 mass update 경로 사용.
if (app()->bound('user_overrides.seeding') && app('user_overrides.seeding') === true) {
return parent::update($values);
}
$model = $this->getModel();
$trackable = method_exists($model, 'getTrackableFields')
? $model->getTrackableFields()
: [];
// 입력에 trackable 필드가 하나도 없으면 user_overrides 갱신 불필요 — 기본 경로.
if (empty($trackable) || empty(array_intersect(array_keys($values), $trackable))) {
return parent::update($values);
}
$models = $this->get();
if ($models->isEmpty()) {
return 0;
}
$affected = 0;
$keyName = $model->getKeyName();
foreach ($models as $row) {
$newOverrides = $row->calculateUserOverridesFor($values);
$rowValues = $values;
// user_overrides 에 변경이 있으면 UPDATE payload 에 포함 (json cast 는 Eloquent 가 처리)
if ($newOverrides !== ($row->user_overrides ?? [])) {
$rowValues['user_overrides'] = $newOverrides;
}
// 재귀 없이 기본 UPDATE — PK 단일 행 UPDATE
$affected += $row->newQueryWithoutScopes()
->where($keyName, $row->getKey())
->toBase()
->update($this->prepareValuesForRow($rowValues, $row));
}
return $affected;
}
/**
* array-cast 필드를 DB 저장 형식(JSON 문자열)으로 변환합니다.
*
* Eloquent Builder::update 는 toBase() 로 진입 시 array 값을 자동 JSON 인코딩하지
* 않으므로, 본 메서드에서 모델의 cast 설정을 참조하여 사전 변환합니다.
*
* @param array<string, mixed> $values
* @param \Illuminate\Database\Eloquent\Model $model
* @return array<string, mixed>
*/
private function prepareValuesForRow(array $values, $model): array
{
foreach ($values as $key => $value) {
if (is_array($value) || is_object($value)) {
// array/json cast 필드는 JSON 인코딩 후 저장
$values[$key] = json_encode($value);
}
}
return $values;
}
}