PHPackages                             tes/biotime-sdk - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. tes/biotime-sdk

ActiveLibrary

tes/biotime-sdk
===============

Framework-agnostic PHP SDK for the ZKTeco BioTime device API — token auth, attendance, employees, devices, departments, positions, areas, and resignations.

v1.0.0(today)10MITPHPPHP ^8.1

Since Jul 31Pushed todayCompare

[ Source](https://github.com/Thomas-Emad/biotime-sdk)[ Packagist](https://packagist.org/packages/tes/biotime-sdk)[ Docs](https://github.com/tes/biotime-sdk)[ RSS](/packages/tes-biotime-sdk/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (5)Versions (2)Used By (0)

BioTime SDK
===========

[](#biotime-sdk)

A small, framework-agnostic PHP SDK for the [ZKTeco BioTime](https://www.zkteco.com/) device API.

It wraps token authentication, automatic 401 retry, pagination, and DTO mapping around the device's REST API, so you can write:

```
$biotime = new BioTime();

$employee = $biotime->employees()->find('EMP001');

echo $employee->firstName;
```

instead of hand-rolling Guzzle calls and re-authenticating every time your token expires.

[![PHP Version](https://camo.githubusercontent.com/a3d40f0d75d713ca57f5c82ae476ae92aed55e0868a2d6284c4634aa0f270650/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253545382e312d373737626234)](composer.json)[![License: MIT](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE)

---

Table of contents
-----------------

[](#table-of-contents)

- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Quick start](#quick-start)
- [Resources](#resources)
    - [Attendances](#attendances)
    - [Employees](#employees)
    - [Devices](#devices)
    - [Departments](#departments)
    - [Positions](#positions)
    - [Areas](#areas)
    - [Resigns](#resigns)
- [Pagination](#pagination)
- [DTOs vs raw data](#dtos-vs-raw-data)
- [Error handling](#error-handling)
- [Using inside Laravel](#using-inside-laravel)
- [Testing / mocking the client](#testing--mocking-the-client)
- [Project structure](#project-structure)
- [Contributing](#contributing)
- [License](#license)

---

Features
--------

[](#features)

- 🔐 **Automatic token auth** — authenticates once, caches the token (PSR-16 or in-memory), and transparently re-authenticates on a `401`.
- 📄 **Pagination handled for you** — every resource exposes `list()` for a single page and `all()` to walk every page and return typed DTOs.
- 🧩 **Typed DTOs** — every response row is mapped to a readonly DTO (`Employee`, `Device`, `AttendanceRecord`, ...) while still giving you the full raw payload via `->raw`.
- 🚦 **Consistent error handling** — every non-2xx response throws `ApiException` (or `AuthenticationException` for auth failures) with the status code and raw response body attached.
- 🧱 **Framework-agnostic core** with an **optional, auto-discovered Laravel service provider** — use it in plain PHP, Symfony, Slim, or Laravel without any adapter code.
- 📤 **Bulk device/employee actions** — reboot terminals, clear commands/captures, resync employees, adjust department/area, process resignations and reinstatements — all first-class methods, not raw arrays.
- 📦 **Zero business logic in your app** — the SDK owns request shaping, auth, retries, and pagination; you just call methods and get DTOs back.

Requirements
------------

[](#requirements)

- PHP `^8.1`
- `ext-json`
- A ZKTeco BioTime device (or BioTime server) reachable over HTTP(S), with API credentials (username/password used to obtain a device token)

Installation
------------

[](#installation)

```
composer require tes/biotime-sdk
```

If you're using Laravel, the service provider is auto-discovered — there's nothing else to register. See [Using inside Laravel](#using-inside-laravel).

Configuration
-------------

[](#configuration)

No connection is made until you actually call a resource method (e.g. `attendances()->all()`) — constructing `BioTime` or `Config` never touches the device. `Config` just needs to know where to load its values from, and resolves them lazily.

**Option A — plain PHP, using a `.env` file**

Copy the example env file and fill in your device's details:

```
cp vendor/tes/biotime-sdk/src/.env.example .env
```

```
BIOTIME_IP=192.168.1.50
BIOTIME_PORT=8081
BIOTIME_USERNAME=admin
BIOTIME_PASSWORD=secret
BIOTIME_HTTPS=false
BIOTIME_TIMEOUT=15
```

```
use BioTime\BioTime;

// Loads ./config/biotime.php if it exists, otherwise reads BIOTIME_* env vars
$biotime = new BioTime();
```

**Option B — a `config/biotime.php` file**

Copy `src/config/biotime.php` into your project's `config/` directory and edit it directly — env vars are still picked up as fallback values, so it's safe to commit.

**Option C — build `Config` explicitly**

Useful for multiple devices, a custom cache implementation, or tests:

```
use BioTime\BioTime;
use BioTime\Config;

$config = Config::fromArray([
    'ip'       => '192.168.1.50',
    'port'     => 8081,
    'username' => 'admin',
    'password' => 'secret',
    'https'    => false,
    'timeout'  => 15,
    'cache'    => $psr16Cache, // optional, any PSR-16 CacheInterface
]);

// or:
$config = Config::fromFile(__DIR__ . '/config/biotime.php');
$config = Config::fromEnv('BIOTIME_'); // reads BIOTIME_IP, BIOTIME_PORT, ...

$biotime = new BioTime($config);
```

KeyEnv varDefaultDescription`host``BIOTIME_IP``127.0.0.1`Device/server hostname or IP`port``BIOTIME_PORT``8081`Device/server port`username``BIOTIME_USERNAME`—API username`password``BIOTIME_PASSWORD`—API password`https``BIOTIME_HTTPS``false`Use `https://` instead of `http://``timeout``BIOTIME_TIMEOUT``15`Guzzle request timeout (seconds)`token_ttl``BIOTIME_TOKEN_TTL``43200` (12h)Cached token TTL, in seconds`cache`—`null` (in-memory)Any PSR-16 `CacheInterface`Quick start
-----------

[](#quick-start)

```
use BioTime\BioTime;

$biotime = new BioTime();

// Attendance records for a date range, single page
$page = $biotime->attendances()->list('2026-07-01', '2026-07-31', page: 1, perPage: 50);

// Every page, already mapped to DTOs
$records = $biotime->attendances()->all('2026-07-01', '2026-07-31');

foreach ($records as $record) {
    echo "{$record->empCode} punched {$record->punchState} at {$record->punchTime}\n";
}

// Employees
$employee = $biotime->employees()->find('EMP001');
$biotime->employees()->update($employee->id, ['first_name' => 'Jane']);

// Devices
$devices = $biotime->devices()->all();
$biotime->devices()->reboot([1, 2, 3]);

// Departments / positions / areas
$departments = $biotime->departments()->all();
$positions   = $biotime->positions()->all();
$areas       = $biotime->areas()->all();
```

Resources
---------

[](#resources)

Every list-style resource follows the same two-method convention:

- **`list(...)`** — one page, returns the raw envelope (`count`, `next`, `previous`, `data`) with `data` mapped to DTOs.
- **`all(...)`** — walks every page automatically and returns a flat `DTO[]` array.

### Attendances

[](#attendances)

```
$biotime->attendances()->list(
    startTime: '2026-07-01',
    endTime:   '2026-07-31',
    page: 1,
    perPage: 50,
    filters: ['emp_code' => 'EMP001'], // emp_code, terminal_sn, terminal_alias
);

$biotime->attendances()->all('2026-07-01', '2026-07-31');

$biotime->attendances()->find(123);       // single transaction by id
$biotime->attendances()->delete(123);

// Export raw csv/txt/xls bytes instead of JSON
use BioTime\Enums\ExportType;

$csvBytes = $biotime->attendances()->export(ExportType::CSV, [
    'start_time' => '2026-07-01',
    'end_time'   => '2026-07-31',
]);

file_put_contents('attendance.csv', $csvBytes);
```

### Employees

[](#employees)

```
$biotime->employees()->list(page: 1, perPage: 50, filters: ['department' => 3]);
$biotime->employees()->all(filters: ['areas' => 1]);

$biotime->employees()->find('EMP001');     // lookup by emp_code
$biotime->employees()->findById(3);        // lookup by numeric id

$biotime->employees()->create([
    'emp_code'   => 'EMP002',
    'first_name' => 'Jane',
    'last_name'  => 'Doe',
]);
$biotime->employees()->update(3, ['first_name' => 'Janet']);
$biotime->employees()->delete(3);

// Bulk actions
$biotime->employees()->adjustArea([1, 2], [3, 4]);
$biotime->employees()->adjustDepartment([1, 2], departmentId: 5);
$biotime->employees()->adjustResign(
    employeeIds: [1, 2],
    resignDate: '2026-07-31',
    resignType: 1,       // 1 quit, 2 dismissed, 3 resign, 4 transfer, 5 retain w/o salary
    disableAtt: true,
    reason: 'Voluntary resignation',
);
$biotime->employees()->delBioTemplate([1, 2], fingerPrint: true, face: true);
$biotime->employees()->resyncToDevice([1, 2]);
```

> **Note:** `adjustResign()` calls the device's documented `/personnel/api/employees/adjust_regsin/` endpoint — the trailing typo (`regsin` instead of `resign`) is present in ZKTeco's own API and is preserved here intentionally so the SDK matches the real endpoint.

### Devices

[](#devices)

```
$biotime->devices()->list(page: 1, perPage: 50, filters: ['state' => 1]);
$biotime->devices()->all();

$biotime->devices()->find('A6KX192060002'); // lookup by serial number
$biotime->devices()->get(5);                 // lookup by numeric id

$biotime->devices()->create([
    'sn'         => '111111111',
    'alias'      => 'Front Door',
    'ip_address' => '192.168.1.60',
    'area'       => 1,
]);
$biotime->devices()->update(5, ['alias' => 'Back Door']);
$biotime->devices()->delete(5);

// Bulk terminal commands — all accept a list of device ids
$biotime->devices()->clearCommand([1, 2]);
$biotime->devices()->clearCapture([1, 2]);
$biotime->devices()->clearAll([1, 2]);
$biotime->devices()->uploadAll([1, 2]);
$biotime->devices()->uploadTransaction([1, 2]);
$biotime->devices()->reboot([1, 2]);
```

### Departments

[](#departments)

```
$biotime->departments()->list(page: 1, perPage: 50);
$biotime->departments()->all();
$biotime->departments()->find(3);
$biotime->departments()->create(['dept_code' => 'ENG', 'dept_name' => 'Engineering']);
$biotime->departments()->update(3, ['dept_name' => 'R&D']);
$biotime->departments()->delete(3);
```

### Positions

[](#positions)

```
$biotime->positions()->list(page: 1, perPage: 50);
$biotime->positions()->all();
$biotime->positions()->find(2);
$biotime->positions()->create(['position_code' => 'DEV', 'name' => 'Developer']);
$biotime->positions()->update(2, ['name' => 'Senior Developer']);
$biotime->positions()->delete(2);
```

### Areas

[](#areas)

```
$biotime->areas()->list(page: 1, perPage: 50);
$biotime->areas()->all();
$biotime->areas()->find(1);
$biotime->areas()->create(['area_code' => 'HQ', 'name' => 'Headquarters']);
$biotime->areas()->update(1, ['name' => 'Head Office']);
$biotime->areas()->delete(1);
```

### Resigns

[](#resigns)

```
$biotime->resigns()->list(page: 1, perPage: 50);
$biotime->resigns()->all();
$biotime->resigns()->findByEmployee(employeeId: 3);
$biotime->resigns()->find(5);

$biotime->resigns()->create([
    'employee'    => 3,
    'disableatt'  => true,
    'resign_type' => 1,
    'resign_date' => '2026-06-01',
    'reason'      => 'Voluntary',
]);
$biotime->resigns()->update(5, ['resign_date' => '2026-06-02']);
$biotime->resigns()->delete(5);

// Reinstate one or more previously resigned employees
$biotime->resigns()->reinstatement([5, 6]);
```

Pagination
----------

[](#pagination)

The API returns a standard envelope on every list endpoint:

```
{
    "count": 42,
    "next": "http://.../?page=2",
    "previous": null,
    "data": [ /* ... */ ]
}
```

- `list(...)` returns this envelope as-is, with `data` mapped to DTOs — use this when you need `count`/`next` to build your own pagination UI.
- `all(...)` walks every page under the hood (via `AbstractResource::fetchAll()`) and returns a single flat array of DTOs — use this when you just want "everything that matches these filters."

```
// Single page, with envelope
$page = $biotime->employees()->list(page: 2, perPage: 100);
echo $page['count'];
foreach ($page['data'] as $employee) { /* ... */ }

// Every page, flattened
$allEmployees = $biotime->employees()->all(perPage: 200);
```

DTOs vs raw data
----------------

[](#dtos-vs-raw-data)

Every DTO exposes the fields the SDK cares about as typed, readonly properties, plus the complete original payload under `->raw` in case the device returns extra fields the DTO doesn't model:

```
$employee = $biotime->employees()->find('EMP001');

$employee->empCode;      // 'EMP001'
$employee->firstName;    // 'Jane'
$employee->department;   // 'Engineering'
$employee->raw;          // full original array from the API
```

Error handling
--------------

[](#error-handling)

All request failures throw an exception you can catch and inspect:

```
use BioTime\Exceptions\ApiException;
use BioTime\Exceptions\AuthenticationException;

try {
    $biotime->devices()->reboot([999]);
} catch (AuthenticationException $e) {
    // Bad credentials, or the device rejected re-authentication
} catch (ApiException $e) {
    echo $e->getStatusCode();     // e.g. 404
    echo $e->getResponseBody();   // raw response body from the device
}
```

- `AuthenticationException` — thrown for token/auth failures specifically (extends `ApiException`, so catching `ApiException` also catches this).
- `ApiException` — thrown for any other non-2xx response.

On a `401`, the client automatically clears the cached token, re-authenticates once, and retries the original request before giving up and throwing.

Using inside Laravel
--------------------

[](#using-inside-laravel)

The package auto-registers a Laravel service provider (via composer's `extra.laravel.providers`), so in most apps there's nothing to wire up:

```
use BioTime\BioTime;

$biotime = app(BioTime::class);
// or type-hint BioTime in a controller/job constructor
```

The token is cached using Laravel's configured cache store, so it survives across requests instead of re-authenticating on every request.

Publish the config file if you'd rather edit it directly instead of relying solely on `.env`:

```
php artisan vendor:publish --tag=biotime-config
```

That copies the package's `config/biotime.php` into your app's `config/`directory, where Laravel will merge in `.env` values as usual.

You only need to register your own binding manually if you want non-default wiring (e.g. a specific cache store other than Laravel's default, or a custom Guzzle client for logging/middleware).

Testing / mocking the client
----------------------------

[](#testing--mocking-the-client)

`BioTime::__construct()` accepts an optional Guzzle `ClientInterface`, so you can inject a mock handler in tests without hitting a real device:

```
use BioTime\BioTime;
use BioTime\Config;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;

$mock = new MockHandler([
    new Response(200, [], json_encode(['token' => 'fake-token'])),
    new Response(200, [], json_encode(['count' => 0, 'next' => null, 'previous' => null, 'data' => []])),
]);

$httpClient = new Client(['handler' => HandlerStack::create($mock)]);

$biotime = new BioTime(
    Config::fromArray(['ip' => 'test', 'username' => 'x', 'password' => 'y']),
    $httpClient
);

$biotime->employees()->all(); // exercises the mocked responses above
```

Project structure
-----------------

[](#project-structure)

```
src/
    BioTime.php                    // Entry point — lazily instantiates each resource
    Client.php                     // HTTP client: token injection, 401 retry, error handling
    Config.php                     // Connection + cache configuration
    Auth/
        TokenManager.php           // Authenticates and caches the device token
    Resources/
        AbstractResource.php       // Shared `fetchAll()` pagination helper
        Attendances.php
        Employees.php
        Devices.php
        Departments.php
        Positions.php
        Areas.php
        Resigns.php
    DTO/
        AttendanceRecord.php
        Employee.php
        Device.php
        Department.php
        Position.php
        Area.php
        Resign.php
    Enums/
        ExportType.php             // csv | txt | xls
    Exceptions/
        ApiException.php
        AuthenticationException.php
    Laravel/
        BioTimeServiceProvider.php // Auto-discovered, optional
    config/
        biotime.php                 // Publishable Laravel config / plain-PHP config file
    .env.example

```

Contributing
------------

[](#contributing)

Issues and pull requests are welcome. Please run the test suite before submitting a PR:

```
composer install
composer test
```

License
-------

[](#license)

The MIT License (MIT). See [LICENSE](LICENSE) for details.

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b5fd781b0c5e39d4ab6f530d21891e0f24c977b4042fc89f0bb3e1495af9f913?d=identicon)[thomas-emad](/maintainers/thomas-emad)

---

Top Contributors

[![Thomas-Emad](https://avatars.githubusercontent.com/u/54818496?v=4)](https://github.com/Thomas-Emad "Thomas-Emad (3 commits)")

---

Tags

laravelsdkhrzktecoattendancetime clockbiometricbiotime

###  Code Quality

TestsPHPUnit

Code StylePHP CS Fixer

### Embed Badge

![Health badge](/badges/tes-biotime-sdk/health.svg)

```
[![Health](https://phpackages.com/badges/tes-biotime-sdk/health.svg)](https://phpackages.com/packages/tes-biotime-sdk)
```

###  Alternatives

[laravel/framework

The Laravel Framework.

34.8k556.2M20.8k](/packages/laravel-framework)[aws/aws-sdk-php

AWS SDK for PHP - Use Amazon Web Services in your PHP project

6.2k555.0M2.8k](/packages/aws-aws-sdk-php)[nutgram/nutgram

The Telegram bot library that doesn't drive you nuts

740315.8k8](/packages/nutgram-nutgram)[civicrm/civicrm-core

Open source constituent relationship management for non-profits, NGOs and advocacy organizations.

758297.9k48](/packages/civicrm-civicrm-core)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

595.8M629](/packages/shopware-core)[eslazarev/wildberries-sdk

Wildberries OpenAPI clients (generated).

293.1k](/packages/eslazarev-wildberries-sdk)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
