DEV Community

Taha Rafi
Taha Rafi

Posted on Edited on

Connecting Physical Biometric Devices in PHP 8: Pure Sockets Without Windows DLLs

php-zkteco Architecture and Official Banner

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:

  1. Running a dedicated 32-bit Windows Server virtual machine executing the vendor COM component (zkemkeeper.dll), querying it via PHP COM extensions or external VB/C# scripts.
  2. Deploying Python or Node.js intermediate middleware daemons to communicate with hardware and push data into MySQL via REST or database polling.
  3. 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).

GitHub logo 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)

Tests Latest Stable Version Total Downloads PHP Version Laravel License Comm Key Auth Protocol Webhooks

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:

  1. 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 internal ATT_LOG memory (ZKDB.db), plus selective attendance deletion by time range (CMD_TIME_ATTLOG_DELE = 10005).
  2. 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 (ASCII PP\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
Enter fullscreen mode Exit fullscreen mode

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";
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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)