arokettu/bencode
A PHP library for encoding and decoding Bencode data, commonly used with BitTorrent files. It provides a simple API, stream support, and flexible dictionary handling for reliable serialization in PHP applications.
Install with Composer:
composer require arokettu/bencode
The whole library is essentially a static facade around one class, Arokettu\Bencode\Bencode. Open src/Bencode.php first — it will show you the complete public surface (encode, decode, and the file helpers load / dump in current releases). Everything else in src/ is supporting types and exceptions.
The canonical first use case is a torrent/metainfo file:
use Arokettu\Bencode\Bencode;
$torrent = Bencode::load('/path/to/file.torrent'); // or Bencode::decode(file_get_contents(...))
echo $torrent['announce'];
echo $torrent['info']['name'];
echo strlen($torrent['info']['pieces']); // binary blob, 20 bytes per piece
Round-tripping is symmetric:
$encoded = Bencode::encode(['bar' => 'spam', 'foo' => 42]);
$decoded = Bencode::decode('d3:bar4:spam3:fooi42ee');
Start by testing encode/decode against a real .torrent file and against a hand-written string. If those two work, you understand the package.
Service wrapper around the facade. Don't scatter Bencode::decode() calls through controllers. Wrap it in a small service or value-object hydrator so you control error handling and collection typing in one place:
final class TorrentFile
{
public static function fromPath(string $path): self
{
try {
$data = Bencode::load($path);
} catch (\Throwable $e) {
throw new InvalidTorrentException('Unreadable torrent', 0, $e);
}
// validate required keys before touching them
return new self($data);
}
}
Always validate the decoded shape. Bencode decoding gives you plain PHP arrays/scalars plus whatever wrapper types the library uses for special cases. Treat the result as untrusted input: check that info, announce, and pieces exist and have the expected types before indexing.
Encode, don't hand-build. When you need to produce bencode (e.g. for a tracker response or a test fixture), build a PHP array and pass it to Bencode::encode(). Hand-writing d...e strings is a reliable source of bugs.
Disambiguate empty collections explicitly. PHP has a single array type, so an empty PHP array is ambiguous. The library exposes wrapper types under Arokettu\Bencode\Types (list/dictionary markers — verify exact class names against your installed version) so you can force le versus de:
use Arokettu\Bencode\Types\ListType;
use Arokettu\Bencode\Types\DictionaryType;
Bencode::encode(new ListType([])); // "le"
Bencode::encode(new DictionaryType([])); // "de"
Use these whenever a struct is legitimately empty — tracker responses and torrent metadata frequently have empty dictionaries that must not collapse to le.
Use load/dump for file-oriented workflows, decode/encode for in-memory. load reads the file for you; dump writes it. For streaming or very large payloads, decode once and cache the parsed structure rather than re-parsing per request.
Compute the info-hash from raw bytes, not from re-encoding. The BitTorrent info-hash is the SHA-1 of the exact byte range of the info dictionary as it appears in the file. Locate the 4:info key in the raw file, walk the bencode structure to find its end offset, and hash that slice with sha1($slice, true). Re-encoding via Bencode::encode($decoded['info']) may produce different bytes (key order, integer formatting, wrapper types) and silently give you the wrong hash.
Test against known vectors. Use the classic d3:bar4:spam3:fooi42ee fixture for a smoke test, then add real torrents and a malformed-input case. Bencode has enough edge cases that a two-line test suite will not catch regressions.
Integration tip: keep piece data away from string-mangling. pieces is a concatenation of raw 20-byte SHA-1 digests. Do not pass it through UTF-8-aware functions, json_encode, or anything that re-encodes strings. Slice it with substr($pieces, $i * 20, 20).
[] encodes as either le or de depending on the library's heuristic or the wrapper you use. Any protocol struct that can be empty must use the explicit list/dictionary types. This bites tracker responses (d8:intervali1800e5:peers0:e vs le) and torrent metadata.name is usually UTF-8, pieces is definitely not). Use bin2hex() when debugging binary values.int. Check whether your installed version has a big-integer option/type (the package advertises big-int support) and configure it if you decode arbitrary peer-supplied data. Otherwise a huge integer may throw or be silently coerced.int, and the encoder may reject or mishandle them. Keep dictionary keys as strings.decode/load in try/catch around the package's exception namespace (Arokettu\Bencode\Exceptions). Don't assume a false return; check the actual exception class in your version.encode(decode($x)) === $x byte-for-byte, because decode/encode may normalize integer width, key order, or collection typing. If you need byte-identical output (info-hash, signature verification), work with the original substring.decode and load materialize the entire structure. Large torrents with many pieces are typically fine, but don't decode in a hot loop — parse once and cache. If you need a true streaming decoder, check whether the current release exposes one before rolling your own.bin2hex(substr($data, 0, 64))) and compare against the expected tag (d, l, i, or a decimal length prefix). Most failures are an off-by-one in a length prefix or a missing e.Types namespace is the intended extension surface for special values (lists, dictionaries, big integers). Check whether it exposes interfaces you can implement for custom types before reaching for serialize or string hacks.composer.json, read the changelog before upgrading, and keep an integration test with a real torrent on hand — small libraries change their type/option surface between minor versions more often than you'd expect.How can I help you explore Laravel packages today?