Skip to content

HARI 5: Testing dan Deployment ​

Bootcamp Laravel - Sistem Pengelolaan Dokumen (SiDoku) ​


📋 Informasi Sesi ​

ItemKeterangan
Hari5 dari 5
Durasi8 Jam (09:00 - 17:00 WIB)
Topik UtamaTesting, Optimization, Deployment
ProjectSiDoku - Finalisasi dan deployment

🎯 Learning Objectives ​

Setelah menyelesaikan sesi ini, peserta akan mampu:

  1. Menulis dan menjalankan unit test dan feature test
  2. Mengoptimasi performa aplikasi Laravel
  3. Mengkonfigurasi environment production
  4. Melakukan deployment ke server
  5. Menerapkan best practices keamanan
  6. Melakukan maintenance dan monitoring

SESI 1: Testing di Laravel ​


1.1 Pengenalan Testing ​

Laravel menyediakan dukungan testing yang komprehensif dengan PHPUnit.

Jenis Testing:

JenisLokasiFungsi
Unit Testtests/Unit/Test fungsi/method individual
Feature Testtests/Feature/Test fitur lengkap (HTTP, database)

1.2 Konfigurasi Testing ​

Edit phpunit.xml:

xml
<php>
    <env name="APP_ENV" value="testing"/>
    <env name="BCRYPT_ROUNDS" value="4"/>
    <env name="CACHE_DRIVER" value="array"/>
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/>
    <env name="MAIL_MAILER" value="array"/>
    <env name="QUEUE_CONNECTION" value="sync"/>
    <env name="SESSION_DRIVER" value="array"/>
</php>

⚠️ Catatan PHPUnit 11+: Syntax /** @test */ sudah deprecated dan akan dihapus di PHPUnit 12. Gunakan format public function test_nama_method() sebagai gantinya.


1.3 Unit Test ​

Buat unit test:

bash
php artisan make:test DokumenUnitTest --unit

Edit tests/Unit/DokumenUnitTest.php:

php
<?php

namespace Tests\Unit;

use App\Models\Dokumen;
use App\Models\Kategori;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class DokumenUnitTest extends TestCase
{
    use RefreshDatabase;
    
    public function test_dokumen_can_be_created(): void
    {
        $user = User::factory()->create();
        $kategori = Kategori::factory()->create();
        
        $dokumen = Dokumen::create([
            'user_id' => $user->id,
            'kategori_id' => $kategori->id,
            'judul' => 'Test Dokumen',
            'nomor' => 'TEST/001/2025',
            'tanggal_dokumen' => now(),
        ]);
        
        $this->assertDatabaseHas('dokumens', [
            'judul' => 'Test Dokumen',
            'nomor' => 'TEST/001/2025',
        ]);
    }
    
    public function test_dokumen_belongs_to_kategori(): void
    {
        $kategori = Kategori::factory()->create(['nama' => 'Surat Keputusan']);
        $dokumen = Dokumen::factory()->create(['kategori_id' => $kategori->id]);
        
        $this->assertEquals('Surat Keputusan', $dokumen->kategori->nama);
    }
    
    public function test_dokumen_belongs_to_user(): void
    {
        $user = User::factory()->create(['name' => 'John Doe']);
        $dokumen = Dokumen::factory()->create(['user_id' => $user->id]);
        
        $this->assertEquals('John Doe', $dokumen->user->name);
    }
    
    public function test_dokumen_status_default_is_draft(): void
    {
        $dokumen = Dokumen::factory()->create();
        
        // Jika factory tidak mengoverride status
        $this->assertContains($dokumen->status, ['draft', 'pending', 'approved', 'rejected']);
    }
    
    public function test_scope_status_filters_correctly(): void
    {
        Dokumen::factory()->count(3)->create(['status' => 'approved']);
        Dokumen::factory()->count(2)->create(['status' => 'pending']);
        
        $approved = Dokumen::status('approved')->count();
        $pending = Dokumen::status('pending')->count();
        
        $this->assertEquals(3, $approved);
        $this->assertEquals(2, $pending);
    }
}

1.4 Feature Test ​

Buat feature test:

bash
php artisan make:test DokumenFeatureTest

Edit tests/Feature/DokumenFeatureTest.php:

php
<?php

namespace Tests\Feature;

use App\Models\Dokumen;
use App\Models\Kategori;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class DokumenFeatureTest extends TestCase
{
    use RefreshDatabase;
    
    private User $user;
    private Kategori $kategori;
    
    protected function setUp(): void
    {
        parent::setUp();
        
        $this->user = User::factory()->create();
        $this->kategori = Kategori::factory()->create();
    }
    
    public function test_guest_cannot_access_dokumen_index(): void
    {
        $response = $this->get('/dokumen');
        
        $response->assertRedirect('/login');
    }
    
    public function test_authenticated_user_can_view_dokumen_index(): void
    {
        Dokumen::factory()->count(5)->create();
        
        $response = $this->actingAs($this->user)->get('/dokumen');
        
        $response->assertStatus(200);
        $response->assertViewIs('dokumen.index');
        $response->assertViewHas('dokumens');
    }
    
    public function test_user_can_create_dokumen(): void
    {
        $data = [
            'judul' => 'SK Pengangkatan Baru',
            'nomor' => 'SK/NEW/2025',
            'kategori_id' => $this->kategori->id,
            'tanggal_dokumen' => '2025-06-20',
            'deskripsi' => 'Deskripsi dokumen test',
        ];
        
        $response = $this->actingAs($this->user)
            ->post('/dokumen', $data);
        
        $response->assertRedirect(route('dokumen.index'));
        $response->assertSessionHas('success');
        
        $this->assertDatabaseHas('dokumens', [
            'judul' => 'SK Pengangkatan Baru',
            'nomor' => 'SK/NEW/2025',
        ]);
    }
    
    public function test_dokumen_validation_fails_without_required_fields(): void
    {
        $response = $this->actingAs($this->user)
            ->post('/dokumen', []);
        
        $response->assertSessionHasErrors(['judul', 'nomor', 'kategori_id', 'tanggal_dokumen']);
    }
    
    public function test_dokumen_nomor_must_be_unique(): void
    {
        Dokumen::factory()->create(['nomor' => 'SK/001/2025']);
        
        $response = $this->actingAs($this->user)
            ->post('/dokumen', [
                'judul' => 'Dokumen Baru',
                'nomor' => 'SK/001/2025', // duplicate
                'kategori_id' => $this->kategori->id,
                'tanggal_dokumen' => '2025-06-20',
            ]);
        
        $response->assertSessionHasErrors(['nomor']);
    }
    
    public function test_user_can_view_dokumen_detail(): void
    {
        $dokumen = Dokumen::factory()->create();
        
        $response = $this->actingAs($this->user)
            ->get("/dokumen/{$dokumen->id}");
        
        $response->assertStatus(200);
        $response->assertViewIs('dokumen.show');
        $response->assertSee($dokumen->judul);
    }
    
    public function test_user_can_update_dokumen(): void
    {
        $dokumen = Dokumen::factory()->create([
            'user_id' => $this->user->id,
        ]);
        
        $response = $this->actingAs($this->user)
            ->put("/dokumen/{$dokumen->id}", [
                'judul' => 'Judul Updated',
                'nomor' => $dokumen->nomor,
                'kategori_id' => $this->kategori->id,
                'tanggal_dokumen' => '2025-06-21',
            ]);
        
        $response->assertRedirect(route('dokumen.index'));
        
        $this->assertDatabaseHas('dokumens', [
            'id' => $dokumen->id,
            'judul' => 'Judul Updated',
        ]);
    }
    
    public function test_user_can_delete_dokumen(): void
    {
        $dokumen = Dokumen::factory()->create([
            'user_id' => $this->user->id,
        ]);
        
        $response = $this->actingAs($this->user)
            ->delete("/dokumen/{$dokumen->id}");
        
        $response->assertRedirect(route('dokumen.index'));
        
        $this->assertSoftDeleted('dokumens', ['id' => $dokumen->id]);
    }
}

1.5 API Test ​

bash
php artisan make:test Api/DokumenApiTest

Edit tests/Feature/Api/DokumenApiTest.php:

php
<?php

namespace Tests\Feature\Api;

use App\Models\Dokumen;
use App\Models\Kategori;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;

class DokumenApiTest extends TestCase
{
    use RefreshDatabase;
    
    private User $user;
    private Kategori $kategori;
    
    protected function setUp(): void
    {
        parent::setUp();
        
        $this->user = User::factory()->create();
        $this->kategori = Kategori::factory()->create();
    }
    
    public function test_unauthenticated_user_cannot_access_api(): void
    {
        $response = $this->getJson('/api/dokumen');
        
        $response->assertStatus(401);
    }
    
    public function test_authenticated_user_can_list_dokumens(): void
    {
        Sanctum::actingAs($this->user);
        Dokumen::factory()->count(5)->create();
        
        $response = $this->getJson('/api/dokumen');
        
        $response->assertStatus(200)
            ->assertJsonStructure([
                'success',
                'message',
                'data' => [
                    'data' => [
                        '*' => ['id', 'judul', 'nomor', 'status']
                    ]
                ],
            ]);
    }
    
    public function test_user_can_create_dokumen_via_api(): void
    {
        Sanctum::actingAs($this->user);
        
        $response = $this->postJson('/api/dokumen', [
            'judul' => 'API Created Dokumen',
            'nomor' => 'API/001/2025',
            'kategori_id' => $this->kategori->id,
            'tanggal_dokumen' => '2025-06-20',
        ]);
        
        $response->assertStatus(201)
            ->assertJson([
                'success' => true,
                'data' => [
                    'judul' => 'API Created Dokumen',
                ],
            ]);
    }
    
    public function test_api_validation_returns_422(): void
    {
        Sanctum::actingAs($this->user);
        
        $response = $this->postJson('/api/dokumen', []);
        
        $response->assertStatus(422)
            ->assertJsonValidationErrors(['judul', 'nomor', 'kategori_id', 'tanggal_dokumen']);
    }
    
    public function test_user_can_get_single_dokumen(): void
    {
        Sanctum::actingAs($this->user);
        $dokumen = Dokumen::factory()->create();
        
        $response = $this->getJson("/api/dokumen/{$dokumen->id}");
        
        $response->assertStatus(200)
            ->assertJson([
                'success' => true,
                'data' => [
                    'id' => $dokumen->id,
                ],
            ]);
    }
}

1.6 Menjalankan Test ​

bash
# Jalankan semua test
php artisan test

# Jalankan dengan verbose
php artisan test -v

# Jalankan test spesifik
php artisan test --filter=DokumenFeatureTest

# Jalankan method spesifik
php artisan test --filter=test_user_can_create_dokumen

# Dengan coverage (perlu Xdebug/PCOV)
php artisan test --coverage

SESI 2: Optimasi Performa ​


2.1 Route Caching ​

bash
# Cache routes
php artisan route:cache

# Clear cache
php artisan route:clear

2.2 Config Caching ​

bash
# Cache config
php artisan config:cache

# Clear cache
php artisan config:clear

2.3 View Caching ​

bash
# Cache compiled views
php artisan view:cache

# Clear cache
php artisan view:clear

2.4 Event & Optimization ​

bash
# Cache events
php artisan event:cache

# Optimize all
php artisan optimize

# Clear all cache
php artisan optimize:clear

2.5 Query Optimization ​

Eager Loading:

php
// [X] N+1 Problem
$dokumens = Dokumen::all();
foreach ($dokumens as $dok) {
    echo $dok->kategori->nama; // Query setiap iterasi
}

// [OK] Eager Loading
$dokumens = Dokumen::with('kategori')->get();
foreach ($dokumens as $dok) {
    echo $dok->kategori->nama; // Tidak ada query tambahan
}

Select Kolom Spesifik:

php
// [X] Select semua kolom
$users = User::all();

// [OK] Select yang diperlukan
$users = User::select('id', 'name', 'email')->get();

Chunk untuk Data Besar:

php
// [OK] Chunk processing
Dokumen::chunk(100, function ($dokumens) {
    foreach ($dokumens as $dokumen) {
        // Process
    }
});

2.6 Database Indexing ​

php
// Tambah index di migration
Schema::table('dokumens', function (Blueprint $table) {
    $table->index('status');
    $table->index('tanggal_dokumen');
    $table->index(['kategori_id', 'status']);
});

2.7 Caching Data ​

php
use Illuminate\Support\Facades\Cache;

// Cache query result
$kategoris = Cache::remember('kategoris_aktif', 3600, function () {
    return Kategori::aktif()->get();
});

// Cache forever
$settings = Cache::rememberForever('app_settings', function () {
    return Setting::all()->pluck('value', 'key');
});

// Clear cache
Cache::forget('kategoris_aktif');

// Tags (Redis/Memcached)
Cache::tags(['dokumen'])->put('list', $dokumens, 3600);
Cache::tags(['dokumen'])->flush();

SESI 3: Keamanan ​


3.1 Environment Variables ​

Jangan commit .env!

bash
# .gitignore
.env
.env.backup
.env.production

Gunakan variabel environment:

php
// Akses config, bukan env langsung
$appName = config('app.name');
$dbHost = config('database.connections.mysql.host');

3.2 HTTPS ​

Force HTTPS di production:

Edit app/Providers/AppServiceProvider.php:

php
use Illuminate\Support\Facades\URL;

public function boot(): void
{
    if ($this->app->environment('production')) {
        URL::forceScheme('https');
    }
}

3.3 SQL Injection Prevention ​

php
// [OK] Eloquent/Query Builder (otomatis escaped)
Dokumen::where('status', $request->status)->get();

// [OK] Parameter binding
DB::select('SELECT * FROM dokumens WHERE status = ?', [$status]);

// [X] Raw query tanpa binding
DB::select("SELECT * FROM dokumens WHERE status = '$status'"); // BAHAYA!

3.4 XSS Prevention ​

php
// [OK] Blade otomatis escape
{{ $dokumen->judul }}

// [X] Unescaped (hati-hati)
{!! $dokumen->deskripsi !!}

3.5 Mass Assignment Protection ​

php
// Di Model
protected $fillable = ['judul', 'nomor', 'deskripsi'];
// atau
protected $guarded = ['id', 'created_at', 'updated_at'];

3.6 Rate Limiting ​

Di routes/api.php:

php
Route::middleware('throttle:60,1')->group(function () {
    Route::apiResource('dokumen', DokumenController::class)
        ->parameters(['dokumen' => 'dokumen']);  // Pastikan parameter route benar
});

Custom rate limiter di app/Providers/AppServiceProvider.php:

php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });
    
    RateLimiter::for('login', function (Request $request) {
        return Limit::perMinute(5)->by($request->ip());
    });
}

SESI 4: Deployment ​


4.1 Checklist Pre-Deployment ​

  • [ ] Semua test passing
  • [ ] .env.example terupdate
  • [ ] Tidak ada dd() atau dump() di kode
  • [ ] Error handling sudah proper
  • [ ] Logging sudah dikonfigurasi
  • [ ] Assets sudah di-compile (npm run build)

4.2 Server Requirements ​

  • PHP >= 8.2
  • BCMath PHP Extension
  • Ctype PHP Extension
  • Fileinfo PHP Extension
  • JSON PHP Extension
  • Mbstring PHP Extension
  • OpenSSL PHP Extension
  • PDO PHP Extension
  • Tokenizer PHP Extension
  • XML PHP Extension

4.3 Deployment Manual ​

1. Clone repository:

bash
cd /var/www
git clone https://github.com/username/sidoku.git
cd sidoku

2. Install dependencies:

bash
composer install --optimize-autoloader --no-dev
npm install
npm run build

3. Setup environment:

bash
cp .env.example .env
php artisan key:generate

4. Edit .env untuk production:

env
APP_ENV=production
APP_DEBUG=false
APP_URL=https://sidoku.example.com

LOG_CHANNEL=daily
LOG_LEVEL=error

DB_CONNECTION=mysql
DB_HOST=localhost
DB_DATABASE=sidoku_prod
DB_USERNAME=sidoku_user
DB_PASSWORD=secure_password_here

CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

5. Jalankan migration dan optimasi:

bash
php artisan migrate --force
php artisan db:seed --force # jika perlu
php artisan storage:link
php artisan optimize

6. Set permissions:

bash
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache

4.4 Nginx Configuration ​

nginx
server {
    listen 80;
    server_name sidoku.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name sidoku.example.com;
    root /var/www/sidoku/public;

    ssl_certificate /etc/letsencrypt/live/sidoku.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/sidoku.example.com/privkey.pem;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

4.5 Supervisor untuk Queue Worker ​

Install supervisor:

bash
sudo apt-get install supervisor

Buat config /etc/supervisor/conf.d/sidoku-worker.conf:

ini
[program:sidoku-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/sidoku/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/sidoku/storage/logs/worker.log
stopwaitsecs=3600
bash
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start sidoku-worker:*

4.6 Deployment Script ​

Buat deploy.sh:

bash
#!/bin/bash

echo "Starting deployment..."

# Pull latest code
git pull origin main

# Install dependencies
composer install --optimize-autoloader --no-dev

# Build assets
npm ci
npm run build

# Clear and cache
php artisan optimize:clear
php artisan migrate --force
php artisan optimize

# Restart queue workers
php artisan queue:restart

echo "Deployment completed!"

4.7 CI/CD dengan GitHub Actions ​

Buat .github/workflows/deploy.yml:

yaml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_DATABASE: testing
          MYSQL_ROOT_PASSWORD: password
        ports:
          - 3306:3306
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 8.2
          extensions: mbstring, mysql, zip
          coverage: none
      
      - name: Install Composer dependencies
        run: composer install --prefer-dist --no-progress
      
      - name: Copy .env
        run: cp .env.example .env
      
      - name: Generate key
        run: php artisan key:generate
      
      - name: Run tests
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_DATABASE: testing
          DB_USERNAME: root
          DB_PASSWORD: password
        run: php artisan test
  
  deploy:
    needs: test
    runs-on: ubuntu-latest
    
    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/sidoku
            ./deploy.sh

SESI 5: Monitoring dan Maintenance ​


5.1 Logging ​

php
use Illuminate\Support\Facades\Log;

// Level logging
Log::emergency('Sistem down!');
Log::alert('Butuh tindakan segera');
Log::critical('Error kritis');
Log::error('Error terjadi');
Log::warning('Peringatan');
Log::notice('Perlu diperhatikan');
Log::info('Informasi');
Log::debug('Debug info');

// Dengan context
Log::info('Dokumen dibuat', [
    'dokumen_id' => $dokumen->id,
    'user_id' => auth()->id(),
]);

Konfigurasi di .env:

env
LOG_CHANNEL=daily
LOG_LEVEL=warning

5.2 Error Tracking ​

Untuk production, gunakan service seperti:

  • Sentry
  • Bugsnag
  • Laravel Telescope (development)

Setup Sentry:

bash
composer require sentry/sentry-laravel
env
SENTRY_LARAVEL_DSN=https://xxxxx@sentry.io/project

5.3 Health Check ​

Buat endpoint health check:

php
Route::get('/health', function () {
    try {
        DB::connection()->getPdo();
        Cache::get('health_check');
        
        return response()->json([
            'status' => 'healthy',
            'timestamp' => now()->toIso8601String(),
        ]);
    } catch (\Exception $e) {
        return response()->json([
            'status' => 'unhealthy',
            'error' => $e->getMessage(),
        ], 500);
    }
});

5.4 Backup Database ​

Menggunakan spatie/laravel-backup:

bash
composer require spatie/laravel-backup
php artisan vendor:publish --provider="Spatie\Backup\BackupServiceProvider"
bash
# Backup
php artisan backup:run

# Backup hanya database
php artisan backup:run --only-db

# Jadwalkan di Console/Kernel.php
$schedule->command('backup:run')->daily()->at('02:00');

5.5 Maintenance Mode ​

bash
# Aktifkan maintenance mode
php artisan down

# Dengan pesan custom
php artisan down --message="Sedang dalam perbaikan"

# Allow IP tertentu
php artisan down --allow=127.0.0.1 --allow=192.168.1.100

# Dengan secret bypass
php artisan down --secret="bypass-token"
# Akses: https://sidoku.example.com/bypass-token

# Nonaktifkan maintenance
php artisan up

PRAKTIKUM FINAL: Deploy SiDoku ​


Langkah 1: Finalisasi Kode ​

bash
# Jalankan semua test
php artisan test

# Build assets
npm run build

# Commit semua perubahan
git add .
git commit -m "Ready for production"
git push origin main

Langkah 2: Setup Production Environment ​

  1. Siapkan server dengan PHP 8.2, MySQL, Nginx
  2. Clone repository
  3. Setup .env production
  4. Jalankan migration
  5. Konfigurasi web server

Langkah 3: Monitoring ​

  1. Setup logging
  2. Konfigurasi error tracking
  3. Setup backup otomatis

✅ Checkpoint Hari 5 ​

Pastikan Anda sudah bisa:

  • Menulis dan menjalankan unit test dan feature test
  • Mengoptimasi aplikasi untuk production
  • Mengkonfigurasi keamanan aplikasi
  • Melakukan deployment ke server
  • Setup monitoring dan maintenance

🎉 Selamat! ​

Anda telah menyelesaikan 5-Day Laravel Bootcamp!

Skill yang Dikuasai: ​

  1. ✅ PHP Fundamentals dan OOP
  2. ✅ Laravel Installation dan Structure
  3. ✅ Routing, Controllers, dan Views
  4. ✅ Blade Templating
  5. ✅ Database dan Eloquent ORM
  6. ✅ Form Handling dan Validation
  7. ✅ Authentication dan Authorization
  8. ✅ File Upload
  9. ✅ REST API Development
  10. ✅ Testing
  11. ✅ Deployment

Project SiDoku: ​

Anda telah membangun Sistem Pengelolaan Dokumen dengan fitur:

  • CRUD Dokumen dan Kategori
  • Authentication
  • File Upload
  • REST API
  • Responsive UI dengan Tailwind CSS

Next Steps: ​

  1. Eksplorasi Laravel Livewire untuk interaktivitas
  2. Pelajari Laravel Queues untuk background jobs
  3. Implementasi Real-time dengan Laravel Echo
  4. Pelajari Docker untuk containerization
  5. Eksplorasi Laravel Cloud untuk deployment

Referensi ​

Dibuat dengan ❤️ untuk ASN Indonesia