Kendala versi seperti ^3.0 pada chapter sebelumnya menggambarkan rentang, bukan satu angka. Rentang inilah sumber fleksibilitas sekaligus sumber kekacauan: hari ini Composer mungkin memilih psr/log 3.0.2, lalu besok — setelah rilis baru keluar — mesin rekan tim kita memilih 3.0.3. Dua angka yang berbeda itu bisa membuat aplikasi berperilaku tidak sama persis, padahal composer.json keduanya identik. Berkas composer.lock hadir untuk memutus keambiguan tersebut: hasil resolusi dibekukan menjadi versi pasti, lalu dibagikan ke seluruh tim. Pada chapter ini kita membedah isi berkas lock, cara composer install memakainya, reaksi Composer ketika lock tidak lagi sinkron, dan aturan kapan berkas ini boleh di-commit.

Dari Rentang ke Versi yang Pasti #

Kita mulai dengan proyek kecil bernama toko-lock yang composer.json-nya masih kosong, lalu pasang satu dependensi:

bash
cd toko-lock
composer require psr/log

Output yang Dihasilkan:

text
./composer.json has been updated
Running composer update psr/log
Loading composer repositories with package information
Updating dependencies
Lock file operations: 1 install, 0 updates, 0 removals
  - Locking psr/log (3.0.2)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing psr/log (3.0.2): Extracting archive
Generating autoload files
No security vulnerability advisories found.
Using version ^3.0 for psr/log

Perhatikan dua fase yang berbeda dalam output di atas. Fase pertama, Running composer update psr/log, menyelesaikan kendala ^3.0 menjadi keputusan pasti — terlihat pada baris - Locking psr/log (3.0.2) — lalu menulis keputusan tersebut ke berkas lock (Writing lock file). Fase kedua, Installing dependencies from lock file, memasang persis versi yang barusan dikunci. Sejak detik ini, proyek memiliki dua berkas: composer.json yang menyatakan keinginan (^3.0), dan composer.lock yang menyatakan kenyataan (3.0.2).

Melihat Isi composer.lock #

Mari kita buka berkas composer.lock dan lihat apa saja yang dibekukan. Berikut isi berkas dari sesi di atas, disingkat pada bagian yang kurang relevan agar fokus:

json
{
    "_readme": [
        "This file locks the dependencies of your project to a known state",
        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
        "This file is @generated automatically"
    ],
    "content-hash": "7c9b33979202ac0eedc385f24048eb98",
    "packages": [
        {
            "name": "psr/log",
            "version": "3.0.2",
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
                "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
            },
            "require": {
                "php": ">=8.0.0"
            },
            "type": "library",
            "autoload": {
                "psr-4": {
                    "Psr\\Log\\": "src"
                }
            }
        }
    ],
    "packages-dev": [],
    "platform": {},
    "platform-dev": {},
    "plugin-api-version": "2.9.0"
}

Entri pada array packages jauh lebih tegas daripada composer.json. Field version berisi angka pasti 3.0.2 — bukan rentang — dan field dist.reference menyimpan hash commit git f16e1d5... yang menunjuk kode persis di repositori psr/log. Artinya, dua mesin yang memasang dari lock file ini tidak hanya mendapatkan versi yang sama, tetapi juga byte-for-byte berupa kode sumber yang sama. Array packages-dev menampung versi terkunci untuk dependensi pengembangan, sementara plugin-api-version mencatat versi API plugin Composer yang dipakai saat berkas ini ditulis.

content-hash sebagai Penjaga Sinkronisasi #

Field paling atas, content-hash, adalah sidik jari dari seluruh pengaturan composer.json yang relevan bagi resolusi — daftar require, require-dev, minimum-stability, dan sebagainya. Setiap kali menjalankan composer install, Composer menghitung ulang hash dari composer.json saat ini lalu membandingkannya dengan hash di berkas lock. Hash yang cocok berarti lock masih mewakili manifesto; hash yang berbeda berarti keduanya sudah tidak sinkron — skenario yang kita picu pada bagian berikutnya. Karena composer.lock juga berformat JSON, kita bisa membaca isinya dari PHP layaknya manifesto:

php
<?php
$json = <<<'JSON'
{
    "content-hash": "7c9b33979202ac0eedc385f24048eb98",
    "packages": [
        {
            "name": "psr/log",
            "version": "3.0.2",
            "require": {
                "php": ">=8.0.0"
            }
        }
    ],
    "packages-dev": [],
    "platform": {},
    "plugin-api-version": "2.9.0"
}
JSON;
$lock = json_decode($json, true);

echo "content-hash: {$lock['content-hash']}\n";
foreach ($lock['packages'] as $paket) {
    echo "{$paket['name']} dikunci di versi {$paket['version']}\n";
}

Output yang Dihasilkan:

text
content-hash: 7c9b33979202ac0eedc385f24048eb98
psr/log dikunci di versi 3.0.2

Kode di atas menyalin potongan berkas lock ke dalam nowdoc, mem-parsing-nya menjadi array, lalu mencetak versi terkunci setiap paket. Pada program sungguhan, isi nowdoc diganti dengan file_get_contents('composer.lock') — misalnya untuk menampilkan versi dependensi pada halaman diagnosa aplikasi Anda.

install Membaca Berkas lock #

Setelah lock ada, composer install tidak lagi menyelesaikan kendala apa pun. Perintah ini membaca daftar versi terkunci, membandingkannya dengan isi folder vendor/, lalu hanya memasang yang kurang. Bukti nyatanya terlihat saat kita menjalankan composer install untuk kedua kalinya di proyek yang sama:

bash
composer install

Output yang Dihasilkan:

text
Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Nothing to install, update or remove
Generating autoload files

Baris Nothing to install, update or remove menegaskan bahwa seluruh isi lock sudah terpenuhi di vendor/. Inilah sifat penting install: hasilnya dapat diprediksi dan berulang. Mesin siapa pun — laptop rekan tim, server CI, atau server produksi — yang menjalankan composer install terhadap lock yang sama akan mendapatkan kumpulan versi yang persis identik.

Ketika lock Tidak Lagi Sinkron #

Skenario klasiknya begini: seseorang mengubah kendala di composer.json — misalnya lewat editor teks atau hasil merge branch — tanpa menjalankan perintah Composer apa pun, lalu berkas tersebut didorong ke repository. Kita simulasikan perubahan semacam itu dengan mengubah kendala psr/log dari ^3.0 menjadi ^2.0 langsung di composer.json, lalu menjalankan composer install:

bash
sed -i 's|"psr/log": "^3.0"|"psr/log": "^2.0"|' composer.json
composer install

Output yang Dihasilkan:

text
Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. It is recommended that you run `composer update` or `composer update <package name>`.
- Required package "psr/log" is in the lock file as "3.0.2" but that does not satisfy your constraint "^2.0".
This usually happens when composer files are incorrectly merged or the composer.json file is manually edited.
Read more about correctly resolving merge conflicts https://getcomposer.org/doc/articles/resolving-merge-conflicts.md
and prefer using the "require" command over editing the composer.json file directly https://getcomposer.org/doc/03-cli.md#require-r

Composer menolak melanjutkan, dan pesannya sangat deskriptif. Baris Warning muncul karena content-hash di lock tidak lagi cocok dengan composer.json yang berubah. Baris berikutnya menjelaskan inti masalahnya: lock memegang psr/log 3.0.2, padahal manifesto kini menuntut ^2.0 — versi terkunci tidak memenuhi kendala baru, sehingga tidak ada instalasi yang aman untuk dilakukan. Pesan penutupnya bahkan menyarankan dua kebiasaan yang lebih aman daripada menyunting manifesto secara manual.

Meregenerasi lock dengan update #

Untuk keluar dari keadaan tidak sinkron, resolusi harus dijalankan ulang agar lock ditulis kembali. Perintah composer update melakukan persis itu: membaca kendala terbaru dari composer.json, menyelesaikannya dari awal, lalu menulis berkas lock yang baru:

bash
composer update

Output yang Dihasilkan:

text
Loading composer repositories with package information
Updating dependencies
Lock file operations: 0 installs, 1 update, 0 removals
  - Downgrading psr/log (3.0.2 => 2.0.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 0 installs, 1 update, 0 removals
  - Downloading psr/log (2.0.0)
  - Downgrading psr/log (3.0.2 => 2.0.0): Extracting archive
Generating autoload files

Hasilnya terlihat pada baris - Downgrading psr/log (3.0.2 => 2.0.0): versi tertinggi yang memenuhi kendala ^2.0 adalah 2.0.0, sehingga Composer menurunkan paket yang terpasang lalu membekukan angka baru itu ke dalam lock hasil regenerasi. Perlu diingat bahwa update selalu menyelesaikan ulang seluruh dependensi (atau paket yang disebutkan sebagai argumen), jadi jalankan perintah ini secara sadar — bukan sebagai rutinitas harian.

Commit composer.lock atau Tidak #

Aturan mainnya sederhana dan tegas. Untuk aplikasi (type project): commit composer.lock ke version control, selalu. Seluruh tim dan seluruh server kemudian menjalankan composer install dan mendapatkan versi yang identik hingga ke hash commit; bug yang muncul di satu mesin dapat direproduksi di mesin lain karena lingkungan dependensinya sama. Pipeline CI yang menjalankan composer install --no-dev pun memanfaatkan lock untuk membangun dan menguji paket persis seperti produksi.

Untuk library yang dipublikasikan ke Packagist: jangan commit composer.lock. Library dipakai di dalam proyek orang lain, dan versi final yang terpasang ditentukan oleh resolusi proyek tersebut — bukan oleh lock milik library kita. Lock yang ikut terbawa justru menyesatkan pembaca seolah-olah versi itu yang akan dipakai konsumen. Library tetap boleh memiliki lock lokal untuk pengembangan, cukup pastikan berkas itu masuk daftar .gitignore sebelum repository dipublikasikan.

Dengan manifesto dan lock yang bekerja berpasangan, kita siap menjalankan siklus dependensi yang sesungguhnya: memasang paket baru, memakainya di kode, memantaunya, lalu mencopotnya — seluruh siklus itu dibahas pada chapter berikutnya.

Referensi #