project_directory/app/Http/Controllers/AkreditasiInstrumenController.php

317 lines
10 KiB
PHP

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class AkreditasiInstrumenController extends Controller
{
private function akreditasiJsonPath(): string
{
return public_path('json/akreditasi.jff');
}
private function akreditasiLockPath(): string
{
return public_path('json/akreditasi.jff.lock');
}
private function ensureJsonDirExists(): void
{
$dir = public_path('json');
if (!is_dir($dir)) {
@mkdir($dir, 0777, true);
}
}
private function atomicWrite(string $path, string $contents): bool
{
$dir = dirname($path);
if (!is_dir($dir)) {
@mkdir($dir, 0777, true);
}
$tmp = $path . '.tmp';
$bytes = @file_put_contents($tmp, $contents);
if ($bytes === false || $bytes < strlen($contents)) {
@unlink($tmp);
return false;
}
// best-effort backup
if (file_exists($path)) {
@copy($path, $path . '.bak');
}
// replace
@unlink($path);
if (@rename($tmp, $path)) {
return true;
}
// fallback copy
$copied = @copy($tmp, $path);
@unlink($tmp);
return (bool) $copied;
}
private function stripUtf8Bom(string $raw): string
{
// Remove UTF-8 BOM if present
if (strncmp($raw, "\xEF\xBB\xBF", 3) === 0) {
return substr($raw, 3);
}
return $raw;
}
private function decodeJsonArray(string $raw, bool $strict = false): ?array
{
$raw = $this->stripUtf8Bom($raw);
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return $strict ? null : [];
}
return is_array($data) ? $data : ($strict ? null : []);
}
private function tryRepairJsonArray(string $raw): ?array
{
// Common corruption: trailing commas before ']' or '}'
$raw = $this->stripUtf8Bom($raw);
$repaired = preg_replace('/,(\s*[}\]])/m', '$1', $raw);
if (!is_string($repaired) || $repaired === '') {
return null;
}
$data = json_decode($repaired, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return null;
}
return is_array($data) ? $data : null;
}
private function splitList(?string $value): array
{
$value = trim((string) $value);
if ($value === '') return [];
$parts = preg_split('/[,\r\n]+/', $value) ?: [];
$out = [];
foreach ($parts as $p) {
$p = trim($p);
if ($p !== '') $out[] = $p;
}
return array_values(array_unique($out));
}
private function ensureTypeExists(array &$data, string $typeName): int
{
foreach ($data as $i => $t) {
if (($t['name'] ?? null) === $typeName) {
if (!isset($data[$i]['segment']) || !is_array($data[$i]['segment'])) {
$data[$i]['segment'] = [];
}
return $i;
}
}
$data[] = [
'name' => $typeName,
'segment' => [],
];
return count($data) - 1;
}
private function ensureSegmentExists(array &$typeSegments, string $segmentName): int
{
foreach ($typeSegments as $j => $s) {
if (($s['name'] ?? null) === $segmentName) {
if (!isset($typeSegments[$j]['turunan']) || !is_array($typeSegments[$j]['turunan'])) {
$typeSegments[$j]['turunan'] = [];
}
return $j;
}
}
$typeSegments[] = [
'name' => $segmentName,
'turunan' => [],
];
return count($typeSegments) - 1;
}
private function ensureItemExists(array &$segmentChildren, string $itemName): bool
{
foreach ($segmentChildren as $c) {
if (($c['name'] ?? null) === $itemName) {
return false; // already exists
}
}
$segmentChildren[] = ['name' => $itemName];
return true;
}
/**
* Read akreditasi.jff (array)
*/
public function index()
{
$path = $this->akreditasiJsonPath();
if (!file_exists($path)) {
return response()->json([]);
}
$raw = @file_get_contents($path);
$data = $this->decodeJsonArray($raw === false ? '[]' : $raw, false) ?? [];
return response()->json($data);
}
/**
* Add "folder" structure to akreditasi.jff.
*
* Payload:
* - type (required)
* - segment (optional)
* - item (optional)
*
* Behavior:
* - type only: creates new top-level type {name, segment: []}
* - type + segment: creates segment {name, turunan: []} under type
* - type + segment + item: creates item {name} under segment.turunan
*/
public function store(Request $request)
{
$validated = $request->validate([
'type' => ['required', 'string', 'max:255'],
'segment' => ['nullable', 'string', 'max:255'],
'item' => ['nullable', 'string', 'max:2000'],
]);
$typeName = trim($validated['type'] ?? '');
$segmentRaw = isset($validated['segment']) ? (string) $validated['segment'] : '';
$itemRaw = isset($validated['item']) ? (string) $validated['item'] : '';
$segments = $this->splitList($segmentRaw);
$items = $this->splitList($itemRaw);
if ($typeName === '') {
return response()->json([
'status' => false,
'message' => 'Type wajib diisi.',
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
if (count($items) > 0 && count($segments) === 0) {
return response()->json([
'status' => false,
'message' => 'Segment wajib diisi jika ingin menambah item.',
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
$this->ensureJsonDirExists();
$path = $this->akreditasiJsonPath();
$lockPath = $this->akreditasiLockPath();
// Use separate lock file so Windows can replace the target JSON file safely.
$lockFp = @fopen($lockPath, 'c+');
if (!$lockFp) {
return response()->json([
'status' => false,
'message' => 'Gagal membuka file lock untuk akreditasi.jff.',
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
try {
if (!flock($lockFp, LOCK_EX)) {
return response()->json([
'status' => false,
'message' => 'Gagal mengunci file akreditasi.jff.',
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
$raw = @file_get_contents($path);
if ($raw === false) {
$data = [];
} else {
$data = $this->decodeJsonArray($raw, true);
if ($data === null) {
// Try safe repair (e.g., remove trailing commas). If still invalid, abort to avoid data loss.
$repaired = $this->tryRepairJsonArray($raw);
if ($repaired === null) {
return response()->json([
'status' => false,
'message' => 'File public/json/akreditasi.jff tidak valid (JSON rusak). Perubahan dibatalkan agar data tidak hilang. Silakan restore dari akreditasi.jff.bak lalu coba lagi.',
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
$data = $repaired;
}
}
$typeIndex = $this->ensureTypeExists($data, $typeName);
$added = 0;
$skipped = 0;
// Case: type only
if (count($segments) === 0) {
// type already ensured; treat as skipped if it existed, added if new.
// We can't easily detect "new" without extra flag; keep message generic.
} elseif (count($items) === 0) {
// Case: add one or more segments
$typeSegments = $data[$typeIndex]['segment'];
foreach ($segments as $segName) {
$beforeCount = count($typeSegments);
$this->ensureSegmentExists($typeSegments, $segName);
$afterCount = count($typeSegments);
if ($afterCount > $beforeCount) $added++;
else $skipped++;
}
$data[$typeIndex]['segment'] = $typeSegments;
} else {
// Case: add one or more items under the given segment(s)
// If user provides multiple segments + items, add all items to each segment.
$typeSegments = $data[$typeIndex]['segment'];
foreach ($segments as $segName) {
$segIndex = $this->ensureSegmentExists($typeSegments, $segName);
$children = $typeSegments[$segIndex]['turunan'] ?? [];
if (!is_array($children)) $children = [];
foreach ($items as $itemName) {
$didAdd = $this->ensureItemExists($children, $itemName);
if ($didAdd) $added++;
else $skipped++;
}
$typeSegments[$segIndex]['turunan'] = $children;
}
$data[$typeIndex]['segment'] = $typeSegments;
}
// write back
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json === false) {
return response()->json([
'status' => false,
'message' => 'Gagal mengubah data menjadi JSON.',
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
$json .= "\n";
if (!$this->atomicWrite($path, $json)) {
return response()->json([
'status' => false,
'message' => 'Gagal menulis file akreditasi.jff (atomic write).',
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
return response()->json([
'status' => true,
'message' => ($added > 0 || $skipped > 0)
? "Selesai. Ditambahkan: {$added}, dilewati (sudah ada): {$skipped}."
: 'Selesai.',
'data' => $data,
]);
} finally {
@flock($lockFp, LOCK_UN);
@fclose($lockFp);
}
}
}