Proyek Akhir: Aplikasi Daftar Tugas

Menyatukan semuanya: enum, readonly DTO, repository berbasis interface, service, validasi, exception, dan respons JSON; lalu langkah belajar selanjutnya.

Part 22 dari 22 ± 60 menit Gratis

Tujuan proyek

Kita membangun inti aplikasi Daftar Tugas (Todo) dengan arsitektur bersih yang memakai hampir semua yang sudah kamu pelajari: strict_types, enum, readonly class, constructor promotion, interface, dependency injection, exception kustom, match, dan JSON.

Struktur proyek

todo/
├── public/index.php            ← titik masuk web (router sederhana)
├── src/
│   ├── Status.php              ← enum
│   ├── Tugas.php               ← readonly class (entitas)
│   ├── TugasRepository.php     ← interface
│   ├── MemoriTugasRepository.php ← implementasi di memori (bisa diganti PDO)
│   ├── TugasService.php        ← logika bisnis
│   └── TugasTidakDitemukan.php ← exception kustom
├── tests/
└── composer.json               ← autoload PSR-4 "App\\": "src/"

1. Enum & entitas

enum Status: string
{
    case Baru    = 'baru';
    case Proses  = 'proses';
    case Selesai = 'selesai';

    public function bolehKe(self $tujuan): bool
    {
        return match ($this) {
            self::Baru    => $tujuan === self::Proses,
            self::Proses  => $tujuan === self::Selesai,
            self::Selesai => false,
        };
    }
}

readonly class Tugas
{
    public function __construct(
        public int $id,
        public string $judul,
        public Status $status = Status::Baru,
    ) {}

    public function denganStatus(Status $status): static
    {
        return new static($this->id, $this->judul, $status);   // immutable: buat salinan baru
    }
}

2. Kontrak penyimpanan

interface TugasRepository
{
    public function simpan(Tugas $tugas): void;
    public function cari(int $id): ?Tugas;
    /** @return Tugas[] */
    public function semua(): array;
    public function idBerikut(): int;
}

Service hanya tahu TugasRepository. Hari ini implementasinya array di memori; besok PDO/MySQL — tanpa mengubah service. Inilah kekuatan interface dan dependency injection.

3. Service dengan aturan bisnis

final class TugasService
{
    public function __construct(private TugasRepository $repo) {}

    public function buat(string $judul): Tugas
    {
        $judul = trim($judul);
        if ($judul === '' || mb_strlen($judul) > 100) {
            throw new InvalidArgumentException('Judul wajib diisi, maksimal 100 karakter');
        }

        $tugas = new Tugas($this->repo->idBerikut(), $judul);
        $this->repo->simpan($tugas);

        return $tugas;
    }

    public function ubahStatus(int $id, Status $tujuan): Tugas
    {
        $tugas = $this->repo->cari($id) ?? throw new TugasTidakDitemukan($id);

        if (!$tugas->status->bolehKe($tujuan)) {
            throw new DomainException("Tidak bisa dari {$tugas->status->value} ke {$tujuan->value}");
        }

        $baru = $tugas->denganStatus($tujuan);
        $this->repo->simpan($baru);

        return $baru;
    }
}

4. Titik masuk web (JSON API mini)

<?php
declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use App\{TugasService, MemoriTugasRepository, TugasTidakDitemukan, Status};

header('Content-Type: application/json; charset=utf-8');

$service = new TugasService(new MemoriTugasRepository());
$metode  = $_SERVER['REQUEST_METHOD'];
$path    = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

try {
    $hasil = match (true) {
        $metode === 'POST' && $path === '/tugas'
            => $service->buat((string) (json_decode(file_get_contents('php://input'), true)['judul'] ?? '')),
        default => throw new RuntimeException('Rute tidak ditemukan', 404),
    };
    echo json_encode($hasil, JSON_THROW_ON_ERROR);
} catch (InvalidArgumentException $e) {
    http_response_code(422);
    echo json_encode(['error' => $e->getMessage()]);
} catch (TugasTidakDitemukan) {
    http_response_code(404);
    echo json_encode(['error' => 'Tugas tidak ditemukan']);
} catch (Throwable $e) {
    error_log((string) $e);              // detail untuk developer
    http_response_code(500);
    echo json_encode(['error' => 'Terjadi kesalahan server']);   // pesan ramah untuk pengguna
}

5. Pengujian

// tests/TugasServiceTest.php  (PHPUnit)
public function test_judul_kosong_ditolak(): void
{
    $this->expectException(InvalidArgumentException::class);
    (new TugasService(new MemoriTugasRepository()))->buat('   ');
}

public function test_selesai_tidak_bisa_diubah(): void
{
    $svc = new TugasService(new MemoriTugasRepository());
    $t = $svc->buat('Belajar PHP');
    $svc->ubahStatus($t->id, Status::Proses);
    $svc->ubahStatus($t->id, Status::Selesai);

    $this->expectException(DomainException::class);
    $svc->ubahStatus($t->id, Status::Baru);
}

Karena service bergantung pada interface, tes cukup memakai repository di memori — tanpa database, cepat dan deterministik.

Tantangan lanjutan (kerjakan di komputermu)

  1. Buat PdoTugasRepository (SQLite) dan tukar tanpa menyentuh TugasService.
  2. Tambah PUT /tugas/{id}/status dan GET /tugas.
  3. Tambah autentikasi: tabel pengguna, password_hash, session/JWT.
  4. Jalankan phpstan level 8 dan perbaiki temuannya.
  5. Pasang di server dengan Nginx + PHP-FPM, HTTPS, dan display_errors=Off.

Setelah kursus ini, ke mana?

  • Laravel — framework yang membungkus semua konsep ini. Lanjutkan ke kursus Laravel 13 dari Nol.
  • Pengujian: Pest/PHPUnit, TDD. Kualitas: PHPStan, Pint, Rector.
  • Arsitektur: domain-driven design, event, CQRS; performa: OPcache, JIT, profiling dengan Xdebug/Blackfire.
  • Basis data lanjut: indeks, EXPLAIN, migrasi, replikasi.

Selamat! Setelah lulus bab ini dan quiz akhir, kamu berhak mendapatkan sertifikat. Kunci menjadi mahir: buat proyekmu sendiri dan terus membaca kode orang lain.

Praktikkan langsung, gratis

Tulis dan jalankan kode langsung di browser, dapatkan penilaian otomatis. Daftar gratis kapan saja untuk menyimpan progres dan meraih sertifikat.

Coba latihan sekarang, tanpa daftar
Bagikan: WhatsApp Facebook LinkedIn X