Compare commits

...
9 Commits
Author SHA1 Message Date
Dmitry Rashupkin 22c6dbc6d8 Merge remote-tracking branch 'upstream/master'
CI Tests / test (push) Has been cancelled
2026-04-29 15:04:34 +03:00
Denis Glushkov 184d0cb9c4 Fix: propagate connection override to Builder from BaseModel (#58) 2026-04-19 17:27:37 +05:00
Denis Glushkov ec76c2d640 Fix Laravel 13 compatibility issue (#55), Integrations/Laravel/Connection::select() 2026-04-19 16:42:53 +05:00
Thomas AliasandGitHub b45f5de151 Fix CurlerRollingWithRetries::execOne definition to match smi2/phpClickHouse v1.26.406 (#57) 2026-04-17 18:21:58 +05:00
Mark BeechandGitHub a94b55ded2 Fix: Dynamic property deprecation in Builder::aggregate() due to incompatible cloneWithout() call (#52) 2026-03-24 15:08:46 +05:00
Oral ÜNALandGitHub dab4243872 fix(Connection): add parameter to select() for Laravel 13 compatibility (#54) 2026-03-24 14:27:47 +05:00
glushkovdsanddglushkov a6e0f56b2e Updated CHANGELOG.md 2026-02-10 11:57:14 +05:00
Felix BernhardandGitHub 62620360e7 add query logging to builder (#49) 2026-02-10 11:46:08 +05:00
Anton SamofalandGitHub d10acef42a fix(BaseModel): replace Eloquent HasEvents trait with a minimal custom implementation (#48)
Larastan v3 added @phpstan-require-extends to Eloquent's HasEvents trait, causing class.missingExtends errors for all ClickHouse models since BaseModel does not extend Eloquent Model.

Replaced with a minimal HasEvents trait that provides only the event
dispatching BaseModel actually uses (creating, created, saved), without requiring Eloquent Model inheritance.

Closes #47
2026-02-10 11:45:45 +05:00
9 changed files with 221 additions and 17 deletions
+6
View File
@@ -1,3 +1,9 @@
## 2.5.0 [2026-02-10]
### Features
1. Replaced the Eloquent HasEvents trait with a minimal custom implementation
2. Added query logging to the Builder class
## 2.3.0 [2025-04-21]
### Features
+1 -1
View File
@@ -19,7 +19,7 @@
"require": {
"php": ">=8.0",
"smi2/phpclickhouse": "^1.4.2",
"glushkovds/clickhouse-builder": "^6.1.2",
"glushkovds/clickhouse-builder": "^7",
"illuminate/support": ">=7",
"illuminate/database": ">=7",
"glushkovds/php-clickhouse-schema-builder": "^1.0.2"
+2 -10
View File
@@ -7,9 +7,8 @@ namespace PhpClickHouseLaravel;
use ClickHouseDB\Client;
use ClickHouseDB\Statement;
use Exception;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Concerns\HasAttributes;
use Illuminate\Database\Eloquent\Concerns\HasEvents;
use PhpClickHouseLaravel\Concerns\HasEvents;
use Illuminate\Database\Eloquent\Concerns\HidesAttributes;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
@@ -59,13 +58,6 @@ class BaseModel
*/
public $wasRecentlyCreated = false;
/**
* The event dispatcher instance.
*
* @var Dispatcher
*/
protected static $dispatcher;
/**
* The name of the database connection to use.
*
@@ -327,7 +319,7 @@ class BaseModel
protected function newQuery(): Builder
{
return new Builder($this->getThisClient());
return new Builder($this->getThisClient(), $this->connection);
}
/**
+23 -2
View File
@@ -31,10 +31,13 @@ class Builder extends BaseBuilder
*/
protected $connection = Connection::DEFAULT_NAME;
public function __construct(?Client $client = null)
public function __construct(?Client $client = null, ?string $connection = null)
{
$this->grammar = new Grammar();
$this->client = $client ?? $this->getThisClient();
if ($connection !== null) {
$this->connection = $connection;
}
}
/**
@@ -54,12 +57,30 @@ class Builder extends BaseBuilder
return $this->settings;
}
/**
* Get the elapsed time in milliseconds since a given starting point.
*
* @param float $start
* @return float
*/
protected function getElapsedTime($start)
{
return round((microtime(true) - $start) * 1000, 2);
}
/**
* @return Statement
*/
public function get(array $bindings = []): Statement
{
return $this->client->select($this->toSql(), $bindings);
$query = $this->toSql();
$start = microtime(true);
$statement = $this->client->select($query, $bindings);
$this->resolveConnection()->logQuery($query, $bindings, $this->getElapsedTime($start));
return $statement;
}
/**
+2 -2
View File
@@ -220,7 +220,7 @@ trait BuilderMethodsFromLaravel
*/
public function aggregate($function, $columns = ['*'])
{
$results = $this->cloneWithout($this->unions || $this->havings ? [] : ['columns'])
$results = $this->cloneWithout($this->unions || $this->havings ? [] : ['columns' => []])
->setAggregate($function, $columns)
->getRows();
@@ -249,4 +249,4 @@ trait BuilderMethodsFromLaravel
$table = $this->tableSources ?? (string)$this->getFrom()->getTable();
$this->client->insertAssocBulk($table, $values);
}
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace PhpClickHouseLaravel\Concerns;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Events\NullDispatcher;
/**
* Minimal event dispatching for ClickHouse models.
*
* Unlike Eloquent's HasEvents trait, this does not require extending
* Illuminate\Database\Eloquent\Model, making it compatible with
* ClickHouse's BaseModel, which is not an Eloquent model.
*
* Supports: creating, created, saved events (the events BaseModel actually fires).
* Does not include: observers, $dispatchesEvents mapping, bootHasEvents, or
* attribute-based observer registration — these are Eloquent-specific features.
*/
trait HasEvents
{
/**
* @var Dispatcher|null
*/
protected static $dispatcher;
/**
* @param string $event
* @param bool $halt
*
* @return mixed
*/
protected function fireModelEvent(string $event, bool $halt = true): mixed
{
if (!isset(static::$dispatcher)) {
return true;
}
$method = $halt ? 'until' : 'dispatch';
return static::$dispatcher->{$method}(
"eloquent.{$event}: " . static::class,
$this
);
}
/**
* @return Dispatcher|null
*/
public static function getEventDispatcher(): ?Dispatcher
{
return static::$dispatcher;
}
/**
* @param Dispatcher $dispatcher
*
* @return void
*/
public static function setEventDispatcher(Dispatcher $dispatcher): void
{
static::$dispatcher = $dispatcher;
}
/**
* @return void
*/
public static function unsetEventDispatcher(): void
{
static::$dispatcher = null;
}
/**
* @param callable $callback
*
* @return mixed
*/
public static function withoutEvents(callable $callback): mixed
{
$dispatcher = static::getEventDispatcher();
if ($dispatcher) {
static::setEventDispatcher(new NullDispatcher($dispatcher));
}
try {
return $callback();
} finally {
if ($dispatcher) {
static::setEventDispatcher($dispatcher);
}
}
}
}
+1 -1
View File
@@ -66,7 +66,7 @@ class Connection extends BaseConnection
}
/** @inheritDoc */
public function select($query, $bindings = [], $useReadPdo = true): array
public function select($query, $bindings = [], $useReadPdo = true, array $fetchUsing = []): array
{
$query = QueryGrammar::prepareParameters($query);
return $this->run($query, $bindings, function ($query, $bindings) {
+1 -1
View File
@@ -15,7 +15,7 @@ class CurlerRollingWithRetries extends CurlerRolling
protected $retries = 0;
/** @inheritDoc */
public function execOne(CurlerRequest $request, $auto_close = false)
public function execOne(CurlerRequest $request, bool $auto_close = false): int
{
$attempts = 1 + max(0, $this->retries);
$httpCode = 0;
+90
View File
@@ -0,0 +1,90 @@
<?php
namespace Tests;
use Illuminate\Contracts\Events\Dispatcher;
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
use PhpClickHouseLaravel\BaseModel;
class EventsTestModel extends BaseModel
{
protected $table = 'events_test';
public function fireModelEvent(string $event, bool $halt = true): mixed
{
return parent::fireModelEvent($event, $halt);
}
}
class EventsTest extends PHPUnitTestCase
{
protected function tearDown(): void
{
BaseModel::unsetEventDispatcher();
parent::tearDown();
}
public function testFireModelEventReturnsTrueWithoutDispatcher(): void
{
BaseModel::unsetEventDispatcher();
$model = new EventsTestModel();
$this->assertTrue($model->fireModelEvent('creating'));
}
public function testCreatingEventCanHaltCreation(): void
{
$dispatcher = $this->createMock(Dispatcher::class);
$dispatcher->method('until')->willReturn(false);
BaseModel::setEventDispatcher($dispatcher);
$result = EventsTestModel::create(['field1' => 'value1']);
$this->assertFalse($result);
}
public function testCreatedEventFires(): void
{
$firedEvents = [];
BaseModel::setEventDispatcher($this->mockDispatcher($firedEvents));
$model = new EventsTestModel();
$model->fireModelEvent('created', false);
$this->assertContains('eloquent.created: ' . EventsTestModel::class, $firedEvents);
}
public function testSavedEventFires(): void
{
$firedEvents = [];
BaseModel::setEventDispatcher($this->mockDispatcher($firedEvents));
$model = new EventsTestModel();
$model->fireModelEvent('saved', false);
$this->assertContains('eloquent.saved: ' . EventsTestModel::class, $firedEvents);
}
/**
* @param array<string> $firedEvents
*/
private function mockDispatcher(array &$firedEvents): Dispatcher
{
$dispatcher = $this->createMock(Dispatcher::class);
$dispatcher->method('until')
->willReturnCallback(function (string $event) use (&$firedEvents) {
$firedEvents[] = $event;
return true;
});
$dispatcher->method('dispatch')
->willReturnCallback(function (string $event) use (&$firedEvents) {
$firedEvents[] = $event;
return [true];
});
return $dispatcher;
}
}