PHPackages                             ysfkc/clickhouse - 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. [HTTP &amp; Networking](/categories/http)
4. /
5. ysfkc/clickhouse

ActiveLibrary[HTTP &amp; Networking](/categories/http)

ysfkc/clickhouse
================

Lightweight ClickHouse HTTP client with a fluent QueryBuilder and ORM-style base model for PHP 8.1+.

v1.0.1(3mo ago)0148↑50%MITPHP &gt;=8.1

Since Apr 9Compare

[ Source](https://github.com/ysfkc/clickhouse)[ Packagist](https://packagist.org/packages/ysfkc/clickhouse)[ RSS](/packages/ysfkc-clickhouse/feed)WikiDiscussions Synced 3w ago

READMEChangelogDependencies (3)Versions (3)Used By (0)

ysfkc/clickhouse
================

[](#ysfkcclickhouse)

A lightweight, framework-agnostic ClickHouse HTTP client for PHP 8.1+ with a fluent QueryBuilder and an ORM-style model layer.

[![PHP](https://camo.githubusercontent.com/83dd395020c37276225039739320f6c8e7e99963ab21ee3d09282cb48dad2a60/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e312532422d626c7565)](https://www.php.net/)[![License](https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e)](LICENSE)

Table of Contents
-----------------

[](#table-of-contents)

- [Architecture](#architecture)
- [Installation](#installation)
- [Configuration](#configuration)
    - [Step 1 — Configure](#step-1--configure-once-at-bootstrap)
    - [Step 2 — getInstance()](#step-2--obtain-the-client-via-getinstance)
    - [Step 3 — Usage](#step-3--use-the-client-directly-or-via-querybuilder--model)
- [QueryBuilder](#querybuilder)
    - [SELECT](#select)
    - [WHERE Conditions](#where-conditions)
    - [JOIN](#join)
    - [GROUP BY / HAVING](#group-by--having)
    - [ORDER BY / LIMIT / OFFSET](#order-by--limit--offset)
    - [INSERT](#insert)
    - [Raw Query — raw()](#raw-query--raw)
- [Model System (ORM)](#model-system-orm)
    - [Defining a Model](#defining-a-model)
    - [Querying Records](#querying-records)
    - [Inserting Records](#inserting-records)
    - [rawQuery / rawCommand](#rawquery--rawcommand)
- [ClickHouseResponse](#clickhouseresponse)
- [ClickHouseCollection](#clickhousecollection)
- [Direct Client Usage](#direct-client-usage)
- [Security](#security)
- [ClickHouse Type Reference](#clickhouse-type-reference)
- [Phalcon Integration](#phalcon-integration-optional)

---

Architecture
------------

[](#architecture)

```
ClickHouseClientService   →  Singleton factory — creates and holds the client
ClickHouseClient          →  HTTP layer (Guzzle), sends parameterized queries
QueryBuilder              →  Fluent API — builds safe SQL queries
ClickHouseResponse        →  Value object wrapping the HTTP response
ClickHouseCollection      →  Typed model list (ArrayAccess, Countable, foreach)

ClickHouseBaseModel  ←  AppBaseModel  ←  MetricSnapshot, OrderEvent, …

```

---

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

[](#installation)

```
composer require ysfkc/clickhouse
```

**Requirements:** PHP 8.1+, `guzzlehttp/guzzle ^7.0`, `psr/log ^1|^2|^3`

---

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

[](#configuration)

### Step 1 — Configure once at bootstrap

[](#step-1--configure-once-at-bootstrap)

Call `ClickHouseClientService::configure()` **once** at application startup (service provider, bootstrap file, etc.):

```
use Ysfkc\ClickHouse\ClickHouseClientService;

ClickHouseClientService::configure([
    'host'           => 'http://clickhouse-server:8123',
    'database'       => 'analytics',
    'username'       => 'default',
    'password'       => 'secret',
    'timeout'        => 30,   // seconds
    'connectTimeout' => 10,   // seconds
]);
```

**With a PSR-3 logger:**

```
ClickHouseClientService::configure([...], $myPsrLogger);
```

---

### Step 2 — Obtain the client via getInstance()

[](#step-2--obtain-the-client-via-getinstance)

After `configure()` is called, retrieve the shared `ClickHouseClient` instance from anywhere in your application:

```
$client = ClickHouseClientService::getInstance();
```

`getInstance()` creates the client **lazily on the first call** and returns the same instance on every subsequent call. Calling `configure()` again invalidates the old instance and forces a fresh one on the next `getInstance()` call.

> ⚠️ Calling `getInstance()` before `configure()` throws a `\RuntimeException`.

---

### Step 3 — Use the client directly or via QueryBuilder / Model

[](#step-3--use-the-client-directly-or-via-querybuilder--model)

Once configured, you can use any of the three layers — they all resolve the client internally via `getInstance()`:

```
use Ysfkc\ClickHouse\ClickHouseClientService;
use Ysfkc\ClickHouse\QueryBuilder;

// Direct client
$client   = ClickHouseClientService::getInstance();
$response = $client->select('SELECT 1');

// QueryBuilder — no manual getInstance() needed
$response = QueryBuilder::table('events')
    ->where('user_id', '=', 123, 'Int32')
    ->get();

// ORM Model — no manual getInstance() needed
$snapshots = MetricSnapshot::find(['status' => 'ok']);
```

---

### Optional — Inject a PSR-3 logger for models

[](#optional--inject-a-psr-3-logger-for-models)

```
use Ysfkc\ClickHouse\Model\ClickHouseBaseModel;

ClickHouseBaseModel::setDefaultLogger($myPsrLogger);
```

---

QueryBuilder
------------

[](#querybuilder)

`QueryBuilder` is started via the static `table()` method. All methods support fluent chaining.

### SELECT

[](#select)

```
use Ysfkc\ClickHouse\QueryBuilder;

// All columns
$response = QueryBuilder::table('events')->get();

// Specific columns
$response = QueryBuilder::table('events')
    ->select(['user_id', 'event_type', 'created_at'])
    ->get();

// Aggregate functions and aliases
$response = QueryBuilder::table('events')
    ->select([
        'user_id',
        'event_type',
        'COUNT()          as total',
        'uniqExact(uuid)  as unique_users',
        'SUM(duration)    as total_duration',
        'toDate(created_at) as day',
    ])
    ->get();
```

---

### WHERE Conditions

[](#where-conditions)

#### Basic Equality

[](#basic-equality)

```
$response = QueryBuilder::table('events')
    ->where('user_id',    '=', 123,     'Int32')
    ->where('event_type', '=', 'click', 'String')
    ->get();
```

#### Comparison Operators

[](#comparison-operators)

```
// Allowed: =  !=    >  <  >=  where('user_id',      '>',    100,        'Int32')
    ->where('browser',      'LIKE', '%Chrome%', 'String')
    ->where('country_code', '!=',   'XX',       'String')
    ->get();
```

#### IN / NOT IN

[](#in--not-in)

```
$response = QueryBuilder::table('events')
    ->where('event_type', 'IN',     ['click', 'scroll', 'view'], 'String')
    ->where('user_id',    'NOT IN', [0, -1],                     'Int32')
    ->get();
```

#### BETWEEN (Date Range)

[](#between-date-range)

```
$response = QueryBuilder::table('events')
    ->whereBetween('event_date', '2026-01-01', '2026-03-31', 'Date')
    ->get();

// DateTime column
$response = QueryBuilder::table('events')
    ->whereBetween('created_at', '2026-01-01 00:00:00', '2026-01-31 23:59:59', 'DateTime')
    ->get();
```

#### NULL Checks

[](#null-checks)

```
QueryBuilder::table('events')->whereNull('referrer')->get();
QueryBuilder::table('events')->whereNotNull('utm_source')->get();
```

#### whereRaw — Raw Condition

[](#whereraw--raw-condition)

> ⚠️ Use only when `where()` cannot express the condition. Never embed user input directly — use `QueryBuilder::raw()` with parameters instead.

```
// ✅ Safe — no user input, column expression only
QueryBuilder::table('events')
    ->whereRaw('toYear(event_date) = toYear(today())')
    ->get();

// ✅ Safe — raw + parameterized
QueryBuilder::raw(
    'SELECT * FROM events WHERE toYear(event_date) = {yr:Int32}',
    ['yr' => 2026]
)->get();

// ❌ FORBIDDEN
QueryBuilder::table('events')
    ->whereRaw('user_id = ' . $userInput)
    ->get();
```

---

### JOIN

[](#join)

```
// INNER JOIN
$response = QueryBuilder::table('orders')
    ->select(['orders.user_id', 'users.name', 'COUNT() as cnt'])
    ->join('users', 'orders.user_id = users.id', 'INNER')
    ->where('orders.user_id', '=', 123, 'Int32')
    ->groupBy(['orders.user_id', 'users.name'])
    ->get();

// LEFT JOIN (shorthand)
$response = QueryBuilder::table('orders')
    ->select(['orders.uuid', 'sessions.started_at'])
    ->leftJoin('sessions', 'orders.session_id = sessions.id')
    ->get();

// ClickHouse ANY JOIN
$response = QueryBuilder::table('orders')
    ->join('sessions', 'orders.session_id = sessions.id', 'LEFT ANY')
    ->get();
```

---

### GROUP BY / HAVING

[](#group-by--having)

```
// GROUP BY
$response = QueryBuilder::table('events')
    ->select(['user_id', 'event_type', 'COUNT() as cnt'])
    ->where('event_date', '>=', '2026-01-01', 'Date')
    ->groupBy(['user_id', 'event_type'])
    ->get();

// HAVING — parameterized (recommended)
$response = QueryBuilder::table('events')
    ->select(['user_id', 'COUNT() as cnt'])
    ->groupBy(['user_id'])
    ->having('cnt >', 100, 'Int64')   // → HAVING cnt > {having_0:Int64}
    ->get();

// HAVING — raw (injection-scanned)
$response = QueryBuilder::table('events')
    ->select(['user_id', 'COUNT() as cnt'])
    ->groupBy(['user_id'])
    ->having('cnt > 0')
    ->get();
```

---

### ORDER BY / LIMIT / OFFSET

[](#order-by--limit--offset)

```
$response = QueryBuilder::table('events')
    ->select(['user_id', 'event_date', 'event_type'])
    ->where('user_id', '=', 123, 'Int32')
    ->orderBy('event_date', 'DESC')
    ->orderBy('event_type', 'ASC')
    ->limit(50)
    ->offset(100)
    ->get();
```

#### First Row

[](#first-row)

```
$row = QueryBuilder::table('events')
    ->where('user_id', '=', 123, 'Int32')
    ->orderBy('event_date', 'DESC')
    ->first();  // array|null, LIMIT 1 added automatically
```

#### COUNT

[](#count)

```
$total    = QueryBuilder::table('events')->where('user_id', '=', 123, 'Int32')->count();
$distinct = QueryBuilder::table('events')->where('user_id', '=', 123, 'Int32')->count('uuid');
```

---

### INSERT

[](#insert)

#### Single Row

[](#single-row)

```
$response = QueryBuilder::table('events')
    ->insertData([
        'uuid'       => ['value' => 'a1b2c3d4-...', 'type' => 'String'],
        'user_id'    => ['value' => 123,             'type' => 'Int32'],
        'event_type' => ['value' => 'click',         'type' => 'String'],
        'event_date' => ['value' => '2026-04-06',    'type' => 'Date'],
    ]);
```

#### Batch INSERT

[](#batch-insert)

```
$columns = ['uuid', 'user_id', 'event_type', 'event_date'];
$types   = ['String', 'Int32', 'String', 'Date'];
$rows    = [
    ['uuid-1', 123, 'click',  '2026-04-06'],
    ['uuid-2', 456, 'scroll', '2026-04-06'],
];

$response = QueryBuilder::table('events')
    ->insertBatch($columns, $rows, $types);
```

---

### Raw Query — raw()

[](#raw-query--raw)

For complex queries that cannot be expressed with the structured methods:

```
$response = QueryBuilder::raw(
    'SELECT
        toDate(event_date)  AS day,
        COUNT()             AS total,
        uniqExact(uuid)     AS unique_users
     FROM events
     WHERE user_id    = {userId:Int32}
       AND event_date BETWEEN {start:Date} AND {end:Date}
     GROUP BY day
     ORDER BY day ASC',
    ['userId' => 123, 'start' => '2026-01-01', 'end' => '2026-03-31']
)->get();
```

#### Debug — toSql()

[](#debug--tosql)

```
$debug = QueryBuilder::table('events')
    ->select(['user_id', 'COUNT() as cnt'])
    ->where('user_id', '=', 123, 'Int32')
    ->groupBy(['user_id'])
    ->toSql();

// [
//   'query'  => 'SELECT user_id, COUNT() as cnt FROM events WHERE user_id = {user_id_0:Int32} GROUP BY user_id',
//   'params' => ['user_id_0' => 123],
// ]
```

---

Model System (ORM)
------------------

[](#model-system-orm)

### Defining a Model

[](#defining-a-model)

```
