The 15-Year Legacy Problem in PHP Biometric Integration
For more than a decade, integrating physical biometric hardware (specifically ZKTeco time-and-attendance and access control terminals) into PHP web applications was notoriously painful.
Historically, developers were forced into one of three brittle workarounds:
- Running a dedicated 32-bit Windows Server virtual machine executing the vendor COM component (
zkemkeeper.dll), querying it via PHPCOMextensions or external VB/C# scripts. - Deploying Python or Node.js intermediate middleware daemons to communicate with hardware and push data into MySQL via REST or database polling.
- Using unmaintained PHP 5.x packages written in 2012 that relied on raw UDP sockets without stream framing, dropping packets across VLANs, failing silently on network partitions, and throwing fatal errors on PHP 8.1+ due to deprecated dynamic properties and type mismatches.
With modern PHP 8, userland network socket capabilities are mature, high-performance, and resilient. The package requires PHP ext-sockets and ext-json but no separate vendor SDK, background daemon, or Windows DLL (no zkemkeeper.dll).
taha-rafi
/
php-zkteco
🚀 Pure Native PHP 8.x ZKTeco Biometric Library: Direct Hardware ATT_LOG Writer (CMD_SET_DATA 3001), Selective Deletion, Dual TCP/UDP Socket Engine, CommKey Auth, Cloud POST Sync, ADMS Push Server & HMAC-SHA256 Webhooks
🚀 php-zkteco v2.1 (Pure Native PHP 8.x & Laravel ZKTeco TCP/UDP, Direct Hardware ATT_LOG Writer, Cloud POST Sync, ADMS Push & Webhook Engine)
A next-generation, standalone Pure Native PHP 8.x & Laravel communication, Direct Hardware Attendance Writer, Cloud POST Sync, ADMS Push Server, and Real-Time Webhook library for ZKTeco Biometric Time & Attendance Devices (TX628, iClock, K-Series, uFace, SpeedFace, MB-Series, ZMM-Series).
Unlike legacy PHP ZKTeco packages that only support unreliable UDP polling and only GET raw arrays, php-zkteco v2.1 introduces:
-
Direct Hardware Attendance Writing (
CMD_SET_DATA = 3001): The first and only ZKTeco library capable of writing attendance punches (Check-In/Check-Out) directly into the physical ZKTeco machine's internalATT_LOGmemory (ZKDB.db), plus selective attendance deletion by time range (CMD_TIME_ATTLOG_DELE = 10005). -
Dual-Protocol Socket Engine (
auto,tcp,udp): Full native ZKTeco TCP framing (\x50\x50\x82\x7d) with…
Core Socket Architecture: TCP Framing and Dual-Protocol Fallback
Standard ZKTeco terminal firmware communicates using a proprietary binary packet protocol across port 4370. Older legacy libraries only supported UDP. While UDP is lightweight, corporate enterprise networks, firewalls, and multi-subnet VLANs regularly drop UDP packets under high traffic or cross-switch hops.
The modern taha-rafi/php-zkteco engine solves this by implementing dual-protocol negotiation with native TCP stream encapsulation.
TCP Stream Framing
TCP is a byte-stream protocol without inherent packet boundaries. To prevent packet fragmentation or stream coalescing, the package encapsulates standard ZKTeco binary commands inside an 8-byte framing envelope:
- 4-Byte Magic Header:
\x50\x50\x82\x7d(ASCIIPP\x82}) - 4-Byte Little-Endian Payload Length
- Raw ZK binary payload (Header, Checksum, Session ID, Reply Code, Command Data)
When initialized with Constants::PROTOCOL_AUTO, the driver establishes a TCP socket handshake first. If the physical machine operates an older firmware revision lacking TCP daemon support, it automatically catches the timeout and falls back to UDP without crashing the parent process.
Native CommKey Authentication
When a terminal is protected by a communication password (CommKey), unauthenticated requests return reply code CMD_ACK_UNAUTH (2005). Legacy libraries failed at this point. The modern engine reverses the firmware MakeKey algorithm: it takes the numeric communication key and session tick count, executes 32-bit arithmetic bitwise scrambling, and dispatches CMD_AUTH (1102) to unlock privileged device operations.
Complete Standalone Implementation
Let us construct a production-ready script that connects to a physical device, gathers hardware diagnostic telemetry, synchronizes system clocks, retrieves attendance records, and performs safe database archiving.
1. Installation & Core Extension Requirements
Install the package via Composer:
composer require taha-rafi/php-zkteco
The package requires PHP ext-sockets and ext-json but no separate vendor SDK, background daemon, or Windows DLL (no zkemkeeper.dll). Ensure both ext-sockets and ext-json are enabled in your php.ini.
2. Device Diagnostics, Clock Synchronization, and Immutable Attendance Retrieval
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use TahaRafi\ZKTeco\ZKTeco;
use TahaRafi\ZKTeco\Constants;
/**
* Production script demonstrating device connection, diagnostics,
* immutable attendance retrieval, and safe database archiving.
*/
$zk = new ZKTeco(
ip: '192.168.1.201',
port: 4370,
commKey: 0,
protocol: Constants::PROTOCOL_AUTO,
timeoutSec: 5
);
// Connection retry loop
$maxRetries = 3;
$connected = false;
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
echo "[INFO] Connection attempt {$attempt}/{$maxRetries}...\n";
if ($zk->connect()) {
$connected = true;
break;
}
if ($attempt < $maxRetries) {
usleep(500000); // 500ms backoff
}
}
if (!$connected) {
echo "[ERROR] Failed to connect to biometric device at 192.168.1.201:4370.\n";
exit(1);
}
try {
// 1. Diagnostics and Device Metadata
echo "\n--- Device Diagnostics ---\n";
echo "Device Name: " . $zk->getDeviceName() . "\n";
echo "Serial Number: " . $zk->getSerialNumber() . "\n";
echo "Platform: " . $zk->getPlatform() . "\n";
echo "Firmware Ver: " . $zk->getVersion() . "\n";
echo "OS Version: " . $zk->getOSVersion() . "\n";
echo "PIN Width: " . $zk->getPinWidth() . " digits\n";
echo "Device Time: " . $zk->getTime() . "\n";
// Synchronize device clock if drifted by more than 5 seconds
$deviceTimestamp = strtotime($zk->getTime());
$serverTimestamp = time();
if (abs($serverTimestamp - $deviceTimestamp) > 5) {
echo "[INFO] Clock drift detected (" . ($serverTimestamp - $deviceTimestamp) . "s). Synchronizing clock...\n";
$zk->setTime(date('Y-m-d H:i:s', $serverTimestamp));
echo "[INFO] Device clock synchronized to: " . $zk->getTime() . "\n";
}
// 2. Fetch Enrolled Users
echo "\n--- Enrolled Users ---\n";
$users = $zk->getUser();
echo "Total Enrolled Users: " . count($users) . "\n";
foreach (array_slice($users, 0, 5) as $user) {
echo sprintf(
" UID: %-4d | ID: %-8s | Name: %-15s | Role: %d\n",
$user['uid'],
$user['userid'],
$user['name'],
$user['role']
);
}
// 3. Retrieve Attendance Records (Immutable Read-Only)
echo "\n--- Attendance Punch Logs ---\n";
$attendanceLogs = $zk->getAttendance();
echo "Total Punch Records in Terminal: " . count($attendanceLogs) . "\n";
$stateLabels = [
Constants::ATT_STATE_CHECK_IN => 'Check-In',
Constants::ATT_STATE_CHECK_OUT => 'Check-Out',
Constants::ATT_STATE_BREAK_OUT => 'Break-Out',
Constants::ATT_STATE_BREAK_IN => 'Break-In',
Constants::ATT_STATE_OVERTIME_IN => 'Overtime-In',
Constants::ATT_STATE_OVERTIME_OUT => 'Overtime-Out',
];
$verifyLabels = [
Constants::VERIFY_PASSWORD => 'Password',
Constants::VERIFY_FINGERPRINT => 'Fingerprint',
Constants::VERIFY_CARD => 'RFID Card',
];
$recentPunches = array_slice($attendanceLogs, -10);
foreach ($recentPunches as $punch) {
$stateText = $stateLabels[$punch['state']] ?? "State({$punch['state']})";
$verifyText = $verifyLabels[$punch['type']] ?? "Type({$punch['type']})";
echo sprintf(
" User ID: %-8s | Timestamp: %s | State: %-12s | Verify: %s\n",
$punch['id'],
$punch['timestamp'],
$stateText,
$verifyText
);
}
// 4. Immutable Read-Only Log Retrieval & Safe Database Archiving
// Terminal punch logs should always be treated as an immutable audit trail.
// Rather than purging or wiping device logs, ingest and archive records safely into
// your application database using idempotent upserts (e.g. compound unique key on user_id + timestamp).
echo "\n[ARCHIVING] Ingesting " . count($attendanceLogs) . " logs into persistent storage...\n";
/*
foreach ($attendanceLogs as $log) {
$db->execute(
'INSERT IGNORE INTO attendance_records (device_uid, user_pin, punched_at, state, verify_type)
VALUES (?, ?, ?, ?, ?)',
[$log['uid'], $log['id'], $log['timestamp'], $log['state'], $log['type']]
);
}
*/
echo "[ARCHIVING] Immutable attendance logs safely archived.\n";
} catch (\Throwable $e) {
echo "[EXCEPTION] Communication error: " . $e->getMessage() . "\n";
} finally {
$zk->disconnect();
echo "\n[INFO] Device connection terminated cleanly.\n";
}
Real-Time Event Streaming: Subscribing to Live Punches
In addition to polling, php-zkteco allows your PHP process to register as an event listener directly on the device using command CMD_REG_EVENT (500). When an employee clocks in with fingerprint, facial recognition, or badge, the device immediately dispatches a real-time UDP packet to your listening socket.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use TahaRafi\ZKTeco\ZKTeco;
use TahaRafi\ZKTeco\Constants;
$zk = new ZKTeco(
ip: '192.168.1.201',
port: 4370,
commKey: 0,
protocol: Constants::PROTOCOL_AUTO,
timeoutSec: 5
);
if (!$zk->connect()) {
die("Connection failed\n");
}
echo "Subscribed to live biometric attendance events. Press Ctrl+C to exit...\n";
// Register specifically for attendance punch events (EF_ATTLOG)
$zk->listenRealTime(
onEvent: function (array $event) {
echo sprintf(
"[%s] Live Punch -> User PIN: %s | State: %d | Punch Time: %s\n",
date('Y-m-d H:i:s'),
$event['user_id'] ?? 'Unknown',
$event['state'] ?? 0,
$event['timestamp'] ?? date('Y-m-d H:i:s')
);
},
eventMask: Constants::EF_ATTLOG
);
$zk->disconnect();
Conclusion
By shifting from legacy 32-bit Windows binaries to a native PHP 8 socket engine, engineering teams can containerize attendance services in Docker, deploy onto lightweight Alpine Linux instances, and achieve sub-second synchronization with physical hardware across standard local networks.

Top comments (0)