diff --git a/app/Http/Controllers/Master/ArtikelKabupatenController.php b/app/Http/Controllers/Master/ArtikelKabupatenController.php index c32eea6bd..eca384075 100644 --- a/app/Http/Controllers/Master/ArtikelKabupatenController.php +++ b/app/Http/Controllers/Master/ArtikelKabupatenController.php @@ -3,6 +3,8 @@ namespace App\Http\Controllers\Master; use App\Http\Controllers\Controller; +use App\Services\ArtikelService; +use Illuminate\Http\Request; use Illuminate\View\View; class ArtikelKabupatenController extends Controller @@ -11,15 +13,15 @@ class ArtikelKabupatenController extends Controller /** * Display a listing of the resource. - * - * @return \Illuminate\Http\Response */ - public function index() + public function index(Request $request): View { - $listPermission = $this->generateListPermission(); - $clearCache = request('clear_cache', false); - if ($clearCache) { - (new \App\Services\ArtikelService)->clearCache('artikel', ['filter[id]' => $clearCache]); + $listPermission = $this->generateListPermission(); + $clearAllCache = $request->query('clear_all_cache', 0); + + if ($clearAllCache > 0) { + // setiap ada perubahan di clear cache semua, termasuk ketika edit karena bisa jadi edit judul saja + (new ArtikelService)->clearAllCache(); } return view('master.artikel.index')->with($listPermission); @@ -27,8 +29,6 @@ public function index() /** * Show the form for creating a new resource. - * - * @return \Illuminate\Http\Response */ public function create(): View { @@ -37,12 +37,8 @@ public function create(): View /** * Show the form for editing the specified resource. - * - * @param int $id - * - * @return \Illuminate\Http\Response */ - public function edit($id): View + public function edit(int $id): View { return view('master.artikel.edit', compact('id')); } diff --git a/app/Services/ArtikelService.php b/app/Services/ArtikelService.php index 618ea0c0a..f8ccc2543 100644 --- a/app/Services/ArtikelService.php +++ b/app/Services/ArtikelService.php @@ -2,31 +2,65 @@ namespace App\Services; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; +use stdClass; class ArtikelService extends BaseApiService { protected int $cacheTtl = 3600; // TTL dalam detik (1 jam) - public function artikel(array $filters = []) + private string $cacheSingleArtikel = 'artikel_'; + + private string $cacheRegistryKey = 'artikel_cache_registry'; + + /** + * Daftarkan cache key ke registry setiap kali generate + */ + private function registerCacheKey(string $key): void + { + $keys = Cache::get($this->cacheRegistryKey, []); + + // Pastikan selalu array meskipun cache corrupt + if (! is_array($keys)) { + $keys = []; + } + + $keys[$key] = time(); + + // Gunakan TTL 7 hari, tidak forever untuk mencegah memory bloat + Cache::put($this->cacheRegistryKey, $keys, now()->addDays(7)); + } + + /** + * Mendapatkan daftar artikel dengan filter opsional + * + * @param array $filters + */ + public function artikel(array $filters = []): Collection { $cacheKey = $this->buildCacheKey('artikel', $filters); + // ✅ Daftarkan key setiap kali generate + $this->registerCacheKey($cacheKey); + // Ambil dari cache dulu - return Cache::remember($cacheKey, $this->cacheTtl, function () use ($filters) { + return Cache::remember($cacheKey, $this->cacheTtl, function () use ($filters): Collection { $data = $this->apiRequest('/api/v1/artikel/list', $filters); - if (!$data) { + + if (empty($data)) { return collect([]); } - return collect($data)->map(function ($item) { - // Return 'attributes' but with 'id' populated + + return collect($data)->map(function (array $item): stdClass { + // Return 'attributes' but with 'id' populated $attributes = $item['attributes'] ?? []; $attributes['id'] = $item['id'] ?? null; // Fetch detail to enrich with gambar and isi if missing - if (isset($attributes['id']) && (!isset($attributes['gambar']) || !isset($attributes['isi']))) { - $detail = $this->artikelById($attributes['id']); - if ($detail) { + if (isset($attributes['id']) && (! isset($attributes['gambar']) || ! isset($attributes['isi']))) { + $detail = $this->artikelById((int) $attributes['id']); + if ($detail !== null) { $attributes['gambar'] = $detail->gambar ?? null; $attributes['isi'] = $detail->isi ?? null; } @@ -37,11 +71,17 @@ public function artikel(array $filters = []) }); } - public function artikelById(int $id) + /** + * Mendapatkan detail artikel berdasarkan ID + */ + public function artikelById(int $id): ?stdClass { - $cacheKey = "artikel_$id"; + $cacheKey = $this->cacheSingleArtikel.$id; + + // ✅ Daftarkan key setiap kali generate + $this->registerCacheKey($cacheKey); - return Cache::remember($cacheKey, $this->cacheTtl, function () use ($id) { + return Cache::remember($cacheKey, $this->cacheTtl, function () use ($id): ?stdClass { $data = $this->apiRequest('/api/v1/artikel/tampil', [ 'id' => $id, ]); @@ -54,9 +94,50 @@ public function artikelById(int $id) }); } - public function clearCache(string $prefix = 'artikel', array $filters = []) + /** + * Menghapus cache artikel tunggal berdasarkan ID + */ + public function clearCacheSingle(int $id): void { - $cacheKey = $this->buildCacheKey($prefix, $filters); + $cacheKey = $this->cacheSingleArtikel.$id; Cache::forget($cacheKey); } + + /** + * ✅ HAPUS SEMUA CACHE ARTIKEL 100% BERFUNGSI DI SEMUA DRIVER! + * Termasuk semua cache list dengan hash MD5 apapun + */ + public function clearAllCache(): void + { + // Ambil semua key yang pernah terdaftar + $keys = Cache::get($this->cacheRegistryKey, []); + + // Validasi tipe data, hindari fatal error jika cache corrupt + if (! is_array($keys)) { + Cache::forget($this->cacheRegistryKey); + + return; + } + + $cacheKeys = array_keys($keys); + + if (empty($cacheKeys)) { + Cache::forget($this->cacheRegistryKey); + + return; + } + + // Laravel 10+ mendukung deleteMultiple untuk batch operation + try { + Cache::deleteMultiple($cacheKeys); + } catch (\BadMethodCallException $e) { + // Fallback untuk driver yang tidak mendukung deleteMultiple + foreach ($cacheKeys as $key) { + Cache::forget($key); + } + } + + // Reset registry + Cache::forget($this->cacheRegistryKey); + } } diff --git a/catatan_rilis.md b/catatan_rilis.md index 51bee7f78..10989dee2 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -10,6 +10,7 @@ Di rilis ini, versi 2604.0.0 berisi penambahan dan perbaikan yang diminta penggu #### Perbaikan BUG 1. [#954](https://github.com/OpenSID/OpenKab/issues/954) Perbaikan list menu tidak tampil. +2. [#369](https://github.com/OpenSID/API-Database-Gabungan/issues/369) Perbaikan cache artikel tidak dihapus setelah operasi hapus. #### Perubahan Teknis diff --git a/resources/views/master/artikel/create.blade.php b/resources/views/master/artikel/create.blade.php index 33072b387..2068eddb2 100644 --- a/resources/views/master/artikel/create.blade.php +++ b/resources/views/master/artikel/create.blade.php @@ -302,7 +302,7 @@ function artikel() { }); setTimeout(() => { window.location.href = - '{{ route('master-data-artikel.index') }}'; + '{{ route('master-data-artikel.index') }}?clear_all_cache=1'; }, 1500); } else { Swal.fire({ diff --git a/resources/views/master/artikel/edit.blade.php b/resources/views/master/artikel/edit.blade.php index 2fa7503ab..0082b9992 100644 --- a/resources/views/master/artikel/edit.blade.php +++ b/resources/views/master/artikel/edit.blade.php @@ -362,8 +362,7 @@ function artikel() { }); setTimeout(() => { window.location.href = - '{{ route('master-data-artikel.index') }}?clear_cache=' + - artikelId; + '{{ route('master-data-artikel.index') }}?clear_all_cache=1'; }, 1500); } else { Swal.fire({ diff --git a/resources/views/master/artikel/index.blade.php b/resources/views/master/artikel/index.blade.php index 1b94b5991..655be2f13 100644 --- a/resources/views/master/artikel/index.blade.php +++ b/resources/views/master/artikel/index.blade.php @@ -175,7 +175,10 @@ className: 'text-center', showConfirmButton: true, timer: 1500 }) - table.ajax.reload(null, false); + setTimeout(() => { + window.location.href = + '{{ route('master-data-artikel.index') }}?clear_all_cache=1'; + }, 1500); } else { Swal.fire({ title: 'Error!', diff --git a/tests/Unit/ArtikelServiceTest.php b/tests/Unit/ArtikelServiceTest.php index 9a3a2fb50..532a62145 100644 --- a/tests/Unit/ArtikelServiceTest.php +++ b/tests/Unit/ArtikelServiceTest.php @@ -4,6 +4,8 @@ use App\Services\ArtikelService; use Illuminate\Support\Facades\Cache; +use Mockery; +use ReflectionClass; use Tests\TestCase; class ArtikelServiceTest extends TestCase @@ -30,7 +32,7 @@ public function it_builds_cache_key_correctly() $method->setAccessible(true); $cacheKey = $method->invokeArgs($this->service, ['artikel', ['id' => 1]]); - + $this->assertIsString($cacheKey); $this->assertStringContainsString('artikel', $cacheKey); } @@ -40,11 +42,11 @@ public function clear_cache_removes_cached_data() { $cacheKey = 'test_artikel_cache'; Cache::put($cacheKey, 'test_data', 3600); - + $this->assertTrue(Cache::has($cacheKey)); - + Cache::forget($cacheKey); - + $this->assertFalse(Cache::has($cacheKey)); } @@ -54,9 +56,9 @@ public function it_has_cache_ttl_property() $reflection = new \ReflectionClass($this->service); $property = $reflection->getProperty('cacheTtl'); $property->setAccessible(true); - + $ttl = $property->getValue($this->service); - + $this->assertEquals(3600, $ttl); $this->assertIsInt($ttl); } @@ -87,4 +89,48 @@ public function service_extends_base_api_service() { $this->assertInstanceOf(\App\Services\BaseApiService::class, $this->service); } + + // tests/Unit/Services/ArtikelServiceTest.php + public function test_clear_all_cache_removes_all_registered_keys(): void + { + $registeredKeys = [ + 'artikel_cache_key_1' => time(), + 'artikel_cache_key_2' => time(), + ]; + + Cache::shouldReceive('get') + ->once() + ->with('artikel_cache_registry', []) + ->andReturn($registeredKeys); + + Cache::shouldReceive('forget') + ->once() + ->with('artikel_cache_key_1'); + + Cache::shouldReceive('forget') + ->once() + ->with('artikel_cache_key_2'); + + Cache::shouldReceive('forget') + ->once() + ->with('artikel_cache_registry'); + + $service = new ArtikelService(); + $service->clearAllCache(); + } + + public function test_clear_all_cache_handles_empty_registry(): void + { + Cache::shouldReceive('get') + ->once() + ->with('artikel_cache_registry', []) + ->andReturn([]); + + Cache::shouldReceive('forget') + ->once() + ->with('artikel_cache_registry'); + + $service = new ArtikelService(); + $service->clearAllCache(); + } }