https://github.com/gnuboard/g7/issues/125 — 상품 상세설명을 HTML 로 저장할 때 HTMLPurifier 가 모듈 vendor 폴더 안에 정의 캐시를 만들려다 실패해 저장이 매번 500 으로 끝나던 문제를 고친다. vendor 를 읽기 전용으로 두는 표준 배포에서 그 쓰기는 예외가 아니라 PHP 경고로 나오고 Laravel 이 이를 ErrorException 으로 승격시킨다. 캐시는 설정 해시당 1회만 기록되므로 캐시가 영영 생기지 않아 재시도해도 같은 결과였다. 캐시 경로를 storage 아래로 옮기고, 그 경로마저 확보하지 못하면 캐시만 끄고 정화는 그대로 수행한다 — 캐시는 성능 장치이고 정화는 보안 장치라, 전자의 실패가 후자를 건너뛰게 만들면 안 된다. 저장은 성공하므로 운영자에게 도달하는 흔적이 로그 하나뿐이라 error 수준으로 남긴다 (출하 기본 로그 수준이 error 라 warning 은 기본 설치 상태에서 파일에 남지 않는다). 그 과정에서 갈라져 있던 두 축을 코어 한 곳으로 모은다. - 쓰기 디렉토리 확보: 억제 생성·chmod·setgid·소유권 상속·쓰기 판정 절차가 정적 게시와 정의 캐시 두 곳에 서로 다른 하드닝으로 복제돼 있었다(억제 mkdir·setgid·clearstatcache 가 사본마다 한쪽씩 빠져 있었다). FilePermissionHelper 의 ensureWritableDirectory 와 hardenDirectory 로 통합하고, 실패 사유는 out 파라미터로 올려 정책(조용한 성능 저하 대 시끄러운 실패)은 호출부가 정하게 둔다. - 확장 저장 경로: storage_path('app/modules/…') 손조립이 30곳에 흩어져 있어 테스트 격리 분기를 넣으려면 사본마다 복제해야 했고, 한 곳만 빠뜨려도 그 확장의 테스트가 운영 설정 파일을 덮어쓴다. 디스크 root 를 단일 출처로 읽는 ExtensionStoragePath 로 전환하고 테스트 분기는 config/filesystems.php 한 줄에서 끝낸다. 함께 고친 것 - 테스트가 운영 라우트 캐시로 부팅해 확장 allowlist 가 라우트 축에서 통째로 무력화되던 문제. 삭제가 아니라 경로를 돌린다 — 라우트 캐시는 확장 작업 전까지 재생성되지 않아, 삭제하면 운영 사이트가 그때까지 라우트 파일 스캔 경로로 떨어진다. - PHPUnit 프로세스가 확장 vendor 의 제3자 composer 패키지를 오토로드하지 않아 그 패키지를 쓰는 코드 경로가 통째로 테스트 불가였던 문제. 확장 자신의 오토로더를 그대로 쓰면 활성 디렉토리가 _bundled 를 이기고 base path 유추까지 깨지므로, 생성된 맵에서 제3자 항목만 골라 별도 로더로 등록한다. - 게시 폴더가 setgid 를 갖지 않아, 명령줄과 웹이 번갈아 만든 하위 폴더를 다른 쪽이 쓰지 못하던 문제. - 관리자 템플릿이 HTML 정화 라이브러리를 직접 지정하지 않아 전이 의존으로 딸려온 구버전이 쓰이던 문제. 동반 산출물 - 규정 표(·AGENTS.md) 6행 + storage-driver/service-repository/testing-guide 문서 - audit 룰 2종 + coverage 6항목. 저장소가 이미 전량 전환돼 전수 실행이 공허 통과하므로 판정식은 픽스처 36건이 잠근다 - INSTALL.md 에 설치 후 파일 권한 절 추가 (vendor 쓰기 권한 불요를 명시)
129 lines
4.7 KiB
PHP
129 lines
4.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Upgrades\Data\Ext\Plugins\SirsoftTosspayments\V1_0_1\Migrations;
|
|
|
|
use App\Extension\Helpers\FilePermissionHelper;
|
|
use App\Extension\Upgrade\DataMigration;
|
|
use App\Extension\UpgradeContext;
|
|
use App\Support\ExtensionStoragePath;
|
|
use Illuminate\Support\Facades\File;
|
|
|
|
/**
|
|
* 저장된 이커머스 주문설정의 토스 주문서형 결제수단 항목에 PG 고정 선언을 백필한다.
|
|
*
|
|
* 배경:
|
|
*
|
|
* 주문서형 결제수단(toss_*)은 과거 `pg_provider: null` 로 등록되어 저장 파일에 그대로
|
|
* 영속화됐다. 그 결과 관리자 주문설정 화면이 다른 간편결제(KG 이니시스 등)와 달리
|
|
* "PG 고정" 배지 대신 빈 PG 선택 셀렉트(---)를 그렸다.
|
|
*
|
|
* 현재 코드는 결제수단 카탈로그를 병합할 때 능력 선언(pg_provider / pg_locked / needs_pg /
|
|
* refund_method)을 플러그인 정의에서 가져오므로 런타임 표시는 이미 정상이다(자가 치유).
|
|
* 본 마이그레이션은 저장 파일 자체를 현재 선언과 일치시켜 설정 화면과 파일 내용이
|
|
* 어긋나 보이지 않게 한다.
|
|
*
|
|
* V-1 안전 격리 (docs/extension/upgrade-step-guide.md §13):
|
|
* - 파일 시스템 + FilePermissionHelper 만 사용 (이전 버전에도 존재하던 표면)
|
|
* - Service / Manager / Repository 컨테이너 해석 없음
|
|
*/
|
|
final class BackfillTossPgProvider implements DataMigration
|
|
{
|
|
/**
|
|
* 이 플러그인이 등록하는 주문서형 결제수단의 ID 접두사.
|
|
*/
|
|
private const METHOD_PREFIX = 'toss_';
|
|
|
|
/**
|
|
* 이 플러그인이 제공하는 PG 식별자.
|
|
*/
|
|
private const PG_PROVIDER_ID = 'tosspayments';
|
|
|
|
/**
|
|
* 마이그레이션 식별자 (로그용).
|
|
*
|
|
* @return string 사람이 읽을 수 있는 짧은 식별자
|
|
*/
|
|
public function name(): string
|
|
{
|
|
return 'BackfillTossPgProvider';
|
|
}
|
|
|
|
/**
|
|
* 저장된 주문설정의 토스 결제수단 항목에 PG 고정 선언을 백필한다. idempotent.
|
|
*
|
|
* @param UpgradeContext $context 업그레이드 컨텍스트 (로거 등)
|
|
*/
|
|
public function run(UpgradeContext $context): void
|
|
{
|
|
// 이커머스 모듈의 주문설정 저장 경로. 절대 경로는 코어 해석기가 디스크 root 를 기준으로 조립한다 — 확장마다
|
|
// 경로를 직접 조립하면 테스트 환경에서 운영 설정 파일을 그대로 건드리게 된다.
|
|
$path = ExtensionStoragePath::module('sirsoft-ecommerce', 'settings').'/order_settings.json';
|
|
|
|
if (! File::exists($path)) {
|
|
$context->logger->info('[tosspayments] 이커머스 주문설정 파일 없음 — 기본값으로 동작하므로 skip');
|
|
|
|
return;
|
|
}
|
|
|
|
$settings = json_decode(File::get($path), true);
|
|
if (! is_array($settings) || ! is_array($settings['payment_methods'] ?? null)) {
|
|
$context->logger->warning('[tosspayments] 주문설정 JSON 형식 비정상 — 결제수단 PG 백필 skip');
|
|
|
|
return;
|
|
}
|
|
|
|
$updated = 0;
|
|
foreach ($settings['payment_methods'] as $index => $method) {
|
|
if (! is_array($method)) {
|
|
continue;
|
|
}
|
|
|
|
$id = (string) ($method['id'] ?? '');
|
|
if (! str_starts_with($id, self::METHOD_PREFIX)) {
|
|
continue;
|
|
}
|
|
|
|
$before = [
|
|
$method['pg_provider'] ?? null,
|
|
$method['pg_locked'] ?? null,
|
|
$method['needs_pg'] ?? null,
|
|
$method['refund_method'] ?? null,
|
|
];
|
|
|
|
$method['pg_provider'] = self::PG_PROVIDER_ID;
|
|
$method['pg_locked'] = true;
|
|
$method['needs_pg'] = true;
|
|
$method['refund_method'] = 'pg';
|
|
|
|
$after = [
|
|
$method['pg_provider'],
|
|
$method['pg_locked'],
|
|
$method['needs_pg'],
|
|
$method['refund_method'],
|
|
];
|
|
|
|
if ($before !== $after) {
|
|
$updated++;
|
|
}
|
|
|
|
$settings['payment_methods'][$index] = $method;
|
|
}
|
|
|
|
if ($updated === 0) {
|
|
$context->logger->info('[tosspayments] 결제수단 PG 선언이 이미 최신 — 변경 없음');
|
|
|
|
return;
|
|
}
|
|
|
|
File::put($path, json_encode($settings, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
|
FilePermissionHelper::inheritOwnershipFromParent($path);
|
|
|
|
$context->logger->info('[tosspayments] 주문서형 결제수단 PG 고정 선언 백필 완료', [
|
|
'pg_provider' => self::PG_PROVIDER_ID,
|
|
'updated_methods' => $updated,
|
|
]);
|
|
}
|
|
}
|