Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Zkteco Adms Core Laravel Package

dewanlab/laravel-zkteco-adms-core

Laravel package implementing the ZKTeco ADMS push protocol for biometric attendance terminals. It receives punches and heartbeats, syncs users and fingerprints, and stores records through configurable models and migrations, with ready-made routes and services for wiring ZKTeco devices into your app.

View on GitHub
Deep Wiki
Context7

Getting Started

dewanlab/laravel-zkteco-adms-core is a server-side implementation of the ZKTeco ADMS push protocol. That framing matters: you are not writing a client that dials out to a terminal. You install this package, it registers the /iclock/* HTTP endpoints, and your physical devices are configured (on the device itself, via its comm/menu settings) to point their ADMS server address at your Laravel app. Everything else — punches, users, commands — flows through those inbound requests.

Minimal path to a first punch:

  1. Install the package and run its migrations. The package owns tables for devices, users, attendance logs, and the command queue; it adds composite unique indexes on (device_id, pin, recorded_at). Check the service provider for publishable config/migrations before running php artisan migrate.
  2. Make the routes reachable. The device will call GET /iclock/cdata and GET /iclock/registry on boot. If you're behind a proxy, load balancer, or Cloudflare, confirm the handshake actually reaches Laravel — device firmware is unforgiving about redirects, HTTPS interception, and non-200 responses.
  3. Configure a terminal's server address to http(s)://your-app/iclock/ with the correct port, then power-cycle or wait for the device's next poll.
  4. Watch storage/logs/zkteco-adms-YYYY-MM-DD.log. This package-scoped daily channel is your first debugging surface — handshake, polling, command delivery, and ingestion are all logged in formatted form. If you see nothing there, the device never reached your app.
  5. Punch a finger on the terminal. You should see an AttendanceLogged event fire and a row land in the attendance table.

First real use case: attendance capture with zero custom code. Register a listener for AttendanceLogged, map the device pin to your users table, and you have a working time-and-attendance ingest loop before you've written a single controller.

Implementation Patterns

Fluent device scoping is the whole ergonomics story. Every operation is chained off a serial number:

use ZktecoAdms;

ZktecoAdms::device('AYZ1234567890')
    ->getTodayAttendanceLogs();

ZktecoAdms::device('AYZ1234567890')
    ->unlockDoor();

Treat the serial as the aggregate root. In a multi-branch deployment, resolve the device from a devices table keyed by serial and never hard-code SNs in application code — build a small DeviceResolver or a Device Eloquent model with a zkteco() accessor that returns the scoped builder.

Read paths vs. write paths behave very differently. getUsers(), getAttendanceLogs(), and friends are database queries against already-ingested data — fast, no device round-trip. pullUsers(), pullLogs(), push(), sync(), unlockDoor(), reboot(), enrollBio() are device operations, and the device-facing ones are asynchronous: they enqueue a command that the terminal only picks up on its next GET /iclock/getrequest poll. Don't write code that assumes unlockDoor() opened the door by the next line; if you need certainty, listen for CommandAcknowledged (or CommandFailed) and correlate by device.

Choose the sync verb deliberately:

  • push($users = null) — local DB employees → terminal. Pass a collection to push a targeted batch; omit to push everything.
  • pullUsers() — terminal roster → DB via DATA QUERY USERINFO.
  • pullLogs() — full historical attendance pull. This resets the watermark cursor to Stamp=0 and requests DATA QUERY ATTLOG, so it re-reads device memory from the beginning. Use for backfills and disaster recovery, not on a schedule.
  • pull() — users plus all historical attendance.
  • sync($users = null) — both directions in one call.

A common pattern is pullUsers() + pullLogs() on first provisioning of a branch, then relying purely on the passive push ingestion for steady state.

Wire events to queued listeners, and treat them as the integration boundary. The four lifecycle events are the cleanest place to attach business logic:

// AttendanceLogged     -> write to your T&A/HR system, notify supervisors
// UserSynchronized     -> an employee was edited on the device UI; reconcile
// CommandAcknowledged  -> mark queued command done, close the ticket
// CommandFailed        -> error code + diagnostic message; alert and retry

Because these dispatch inside the device's HTTP request, make every listener ShouldQueue. A slow listener doesn't just slow a request — it can make the terminal time out and retry, which is how you get duplicated work downstream.

Command queue as a job table. The getrequest / devicecmd pair is a polling RPC. Model it that way: persist each command you issue with its own correlation ID, state machine (queued → delivered → acknowledged / failed), and retry policy. CommandFailed carries an error code and diagnostic message, which is your signal for hardware-level problems (finger not enrolled, door sensor fault) versus transport problems (device offline).

Bulk employee onboarding. createUsers() and deleteUsers() exist precisely because per-user calls queue per-user commands. Use the bulk variants when provisioning a roster, and use getUser() / getUsers() against the local mirror to diff against your HR source before pushing, so you only ship deltas.

Biometric template ingestion. BIODATA arrives through POST /iclock/cdata?table=BIODATA. Templates are large binary payloads — decide up front whether you persist them (useful for reader-to-reader cloning) or discard them, and encrypt/restrict them if you persist, since biometric templates are sensitive personal data with real regulatory exposure in most jurisdictions.

Gotchas and Tips

The endpoints are unauthenticated by design. ZKTeco firmware does not perform Laravel auth, signed URLs, or CSRF. Your POST /iclock/cdata route must be excluded from VerifyCsrfToken and reachable without a session, and you should not expose it to the open internet. Restrict by IP allow-list at the web server, put it on a VPN/private link, or run a reverse proxy that only forwards device subnets. This is the single most common security mistake with ADMS implementations.

Serial numbers must match exactly. Firmware sends the SN in the handshake query string; your device('SN') string, your DB rows, and the device's own reported serial have to agree character-for-character. Case and whitespace mismatches present as "commands silently never execute."

Expect command latency and don't build synchronous UX on it. Devices poll on their own interval. unlockDoor() is not an API call, it's a message dropped in a mailbox. Set user-facing copy accordingly ("command sent to device") and surface CommandFailed asynchronously.

Dead RTC coin batteries cause year-2000 timestamps. The package ships drift protection for this, but verify it: a terminal whose clock resets to 2000 will produce punches dated decades in the past, and your unique index (device_id, pin, recorded_at) won't dedupe them against today's real punches. Periodically check that your ingested recorded_at distribution doesn't skew old, and use the rtdata real-time clock sync route — plus an NTP or scheduled reboot() — to keep devices honest. Also confirm app and device timezones agree before you trust any attendance report.

Firmware authorization stacking bug is mitigated by purge-before-update. On affected firmware, re-pushing a user stacks duplicate authorization records. The package purges the old record before updating. If you bypass the fluent API and write commands manually, you reintroduce the bug — always go through updateUser() / createUsers().

The unique index will throw, not silently ignore. Bulk attendance inserts that contain a duplicate (device_id, pin, recorded_at) will violate the constraint. Since devices do retry on timeouts, this is the mechanism that saves you from double-counting — but your code must insertOrIgnore / upsert rather than assume a clean insert. Don't wrap ingestion in a "fix duplicate by widening the index" migration; that's the feature working.

pullLogs() is expensive and non-incremental. It resets Stamp=0, meaning the terminal re-sends its entire stored history. Throttle it, never schedule it, and be aware that it also increases the chance of hitting duplicates that your index will reject (fine) or of large payloads that hit post_max_size / max_input_vars (not fine — raise PHP limits before backfills).

Log files grow unbounded per day. storage/logs/zkteco-adms-YYYY-MM-DD.log is daily-rotated by name but never pruned. Add it to your existing log cleanup (Laravel's log:prune, a scheduled find -mtime +N -delete, or a custom channel with retention). On a busy terminal this file is also high-volume — for many deployments you'll want the package channel set to daily with a bounded days value rather than default.

Debugging order for "device not reporting": (1) is anything in the zkteco log at all — if not, it's network/DNS/TLS reaching your app; (2) is the handshake returning 200 with the expected body — firmware rejects unexpected response shapes; (3) does the SN in the log match your device() string; (4) is the device's server port right and its clock sane. Only after those four does application code become a suspect.

Extension points to lean on: the events (decorate them or extend them with device metadata for routing), the package config for channel/retention and route prefix, and the fluent builder itself — wrap it in your own Device model rather than scattering ZktecoAdms::device($sn) across controllers, so multi-tenant scoping and audit logging live in one place.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator