diff --git a/flight/Engine.php b/flight/Engine.php
index f75f42a..c386424 100644
--- a/flight/Engine.php
+++ b/flight/Engine.php
@@ -30,6 +30,9 @@ use flight\net\Route;
* @method void stop() Stops framework and outputs current response
* @method void halt(int $code = 200, string $message = '', bool $actuallyExit = true) Stops processing and returns a given response.
*
+ * # Class registration
+ * @method EventDispatcher eventDispatcher() Gets event dispatcher
+ *
* # Routing
* @method Route route(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '')
* Routes a URL to a callback function with all applicable methods
@@ -110,7 +113,6 @@ class Engine
{
$this->loader = new Loader();
$this->dispatcher = new Dispatcher();
- $this->eventDispatcher = new EventDispatcher();
$this->init();
}
@@ -160,6 +162,9 @@ class Engine
$this->dispatcher->setEngine($this);
// Register default components
+ $this->map('eventDispatcher', function () {
+ return EventDispatcher::getInstance();
+ });
$this->loader->register('request', Request::class);
$this->loader->register('response', Response::class);
$this->loader->register('router', Router::class);
@@ -460,7 +465,9 @@ class Engine
// Here is the array callable $middlewareObject that we created earlier.
// It looks bizarre but it's really calling [ $class, $method ]($params)
// Which loosely translates to $class->$method($params)
+ $start = microtime(true);
$middlewareResult = $middlewareObject($params);
+ $this->triggerEvent('flight.middleware.executed', $route, $middleware, microtime(true) - $start);
if ($useV3OutputBuffering === true) {
$this->response()->write(ob_get_clean());
@@ -573,12 +580,12 @@ class Engine
}
// Call route handler
+ $routeStart = microtime(true);
$continue = $this->dispatcher->execute(
$route->callback,
$params
);
- $this->triggerEvent('flight.route.executed', $route);
-
+ $this->triggerEvent('flight.route.executed', $route, microtime(true) - $routeStart);
if ($useV3OutputBuffering === true) {
$response->write(ob_get_clean());
}
@@ -631,6 +638,7 @@ class Engine
*/
public function _error(Throwable $e): void
{
+ $this->triggerEvent('flight.error', $e);
$msg = sprintf(
<<<'HTML'
500 Internal Server Error
@@ -678,8 +686,6 @@ class Engine
}
$response->send();
-
- $this->triggerEvent('flight.response.sent', $response);
}
}
@@ -831,6 +837,8 @@ class Engine
$url = $base . preg_replace('#/+#', '/', '/' . $url);
}
+ $this->triggerEvent('flight.redirect', $url, $code);
+
$this->response()
->clearBody()
->status($code)
@@ -854,7 +862,9 @@ class Engine
return;
}
+ $start = microtime(true);
$this->view()->render($file, $data);
+ $this->triggerEvent('flight.view.rendered', $file, microtime(true) - $start);
}
/**
@@ -1019,7 +1029,7 @@ class Engine
*/
public function _onEvent(string $eventName, callable $callback): void
{
- $this->eventDispatcher->on($eventName, $callback);
+ $this->eventDispatcher()->on($eventName, $callback);
}
/**
@@ -1030,6 +1040,6 @@ class Engine
*/
public function _triggerEvent(string $eventName, ...$args): void
{
- $this->eventDispatcher->trigger($eventName, ...$args);
+ $this->eventDispatcher()->trigger($eventName, ...$args);
}
}
diff --git a/flight/Flight.php b/flight/Flight.php
index fb35427..7e43877 100644
--- a/flight/Flight.php
+++ b/flight/Flight.php
@@ -8,6 +8,7 @@ use flight\net\Response;
use flight\net\Router;
use flight\template\View;
use flight\net\Route;
+use flight\core\EventDispatcher;
require_once __DIR__ . '/autoload.php';
@@ -29,6 +30,9 @@ require_once __DIR__ . '/autoload.php';
* Unregisters a class to a framework method.
* @method static void registerContainerHandler(callable|object $containerHandler) Registers a container handler.
*
+ * # Class registration
+ * @method EventDispatcher eventDispatcher() Gets event dispatcher
+ *
* # Routing
* @method static Route route(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '')
* Maps a URL pattern to a callback with all applicable methods.
diff --git a/flight/core/EventDispatcher.php b/flight/core/EventDispatcher.php
index b80eb42..ea809b2 100644
--- a/flight/core/EventDispatcher.php
+++ b/flight/core/EventDispatcher.php
@@ -6,9 +6,25 @@ namespace flight\core;
class EventDispatcher
{
+ /** @var self|null Singleton instance of the EventDispatcher */
+ private static ?self $instance = null;
+
/** @var array> */
protected array $listeners = [];
+ /**
+ * Singleton instance of the EventDispatcher.
+ *
+ * @return self
+ */
+ public static function getInstance(): self
+ {
+ if (self::$instance === null) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
/**
* Register a callback for an event.
*
@@ -42,4 +58,80 @@ class EventDispatcher
}
}
}
+
+ /**
+ * Check if an event has any registered listeners.
+ *
+ * @param string $event Event name
+ *
+ * @return bool True if the event has listeners, false otherwise
+ */
+ public function hasListeners(string $event): bool
+ {
+ return isset($this->listeners[$event]) === true && count($this->listeners[$event]) > 0;
+ }
+
+ /**
+ * Get all listeners registered for a specific event.
+ *
+ * @param string $event Event name
+ *
+ * @return array Array of callbacks registered for the event
+ */
+ public function getListeners(string $event): array
+ {
+ return $this->listeners[$event] ?? [];
+ }
+
+ /**
+ * Get a list of all events that have registered listeners.
+ *
+ * @return array Array of event names
+ */
+ public function getAllRegisteredEvents(): array
+ {
+ return array_keys($this->listeners);
+ }
+
+ /**
+ * Remove a specific listener for an event.
+ *
+ * @param string $event the event name
+ * @param callable $callback the exact callback to remove
+ *
+ * @return void
+ */
+ public function removeListener(string $event, callable $callback): void
+ {
+ if (isset($this->listeners[$event]) === true && count($this->listeners[$event]) > 0) {
+ $this->listeners[$event] = array_filter($this->listeners[$event], function ($listener) use ($callback) {
+ return $listener !== $callback;
+ });
+ $this->listeners[$event] = array_values($this->listeners[$event]); // Re-index the array
+ }
+ }
+
+ /**
+ * Remove all listeners for a specific event.
+ *
+ * @param string $event the event name
+ *
+ * @return void
+ */
+ public function removeAllListeners(string $event): void
+ {
+ if (isset($this->listeners[$event]) === true) {
+ unset($this->listeners[$event]);
+ }
+ }
+
+ /**
+ * Remove the current singleton instance of the EventDispatcher.
+ *
+ * @return void
+ */
+ public static function resetInstance(): void
+ {
+ self::$instance = null;
+ }
}
diff --git a/flight/database/PdoWrapper.php b/flight/database/PdoWrapper.php
index 297121a..7cf74cf 100644
--- a/flight/database/PdoWrapper.php
+++ b/flight/database/PdoWrapper.php
@@ -4,12 +4,40 @@ declare(strict_types=1);
namespace flight\database;
+use flight\core\EventDispatcher;
use flight\util\Collection;
use PDO;
use PDOStatement;
class PdoWrapper extends PDO
{
+ /** @var bool $trackApmQueries Whether to track application performance metrics (APM) for queries. */
+ protected bool $trackApmQueries = false;
+
+ /** @var array> $queryMetrics Metrics related to the database connection. */
+ protected array $queryMetrics = [];
+
+ /** @var array $connectionMetrics Metrics related to the database connection. */
+ protected array $connectionMetrics = [];
+
+ /**
+ * Constructor for the PdoWrapper class.
+ *
+ * @param string $dsn The Data Source Name (DSN) for the database connection.
+ * @param string|null $username The username for the database connection.
+ * @param string|null $password The password for the database connection.
+ * @param array|null $options An array of options for the PDO connection.
+ * @param bool $trackApmQueries Whether to track application performance metrics (APM) for queries.
+ */
+ public function __construct(?string $dsn = null, ?string $username = '', ?string $password = '', ?array $options = null, bool $trackApmQueries = false)
+ {
+ parent::__construct($dsn, $username, $password, $options);
+ $this->trackApmQueries = $trackApmQueries;
+ if ($this->trackApmQueries === true) {
+ $this->connectionMetrics = $this->pullDataFromDsn($dsn);
+ }
+ }
+
/**
* Use this for INSERTS, UPDATES, or if you plan on using a SELECT in a while loop
*
@@ -31,8 +59,19 @@ class PdoWrapper extends PDO
$processed_sql_data = $this->processInStatementSql($sql, $params);
$sql = $processed_sql_data['sql'];
$params = $processed_sql_data['params'];
+ $start = $this->trackApmQueries === true ? microtime(true) : 0;
+ $memory_start = $this->trackApmQueries === true ? memory_get_usage() : 0;
$statement = $this->prepare($sql);
$statement->execute($params);
+ if ($this->trackApmQueries === true) {
+ $this->queryMetrics[] = [
+ 'sql' => $sql,
+ 'params' => $params,
+ 'execution_time' => microtime(true) - $start,
+ 'row_count' => $statement->rowCount(),
+ 'memory_usage' => memory_get_usage() - $memory_start
+ ];
+ }
return $statement;
}
@@ -88,9 +127,20 @@ class PdoWrapper extends PDO
$processed_sql_data = $this->processInStatementSql($sql, $params);
$sql = $processed_sql_data['sql'];
$params = $processed_sql_data['params'];
+ $start = $this->trackApmQueries === true ? microtime(true) : 0;
+ $memory_start = $this->trackApmQueries === true ? memory_get_usage() : 0;
$statement = $this->prepare($sql);
$statement->execute($params);
$results = $statement->fetchAll();
+ if ($this->trackApmQueries === true) {
+ $this->queryMetrics[] = [
+ 'sql' => $sql,
+ 'params' => $params,
+ 'execution_time' => microtime(true) - $start,
+ 'row_count' => $statement->rowCount(),
+ 'memory_usage' => memory_get_usage() - $memory_start
+ ];
+ }
if (is_array($results) === true && count($results) > 0) {
foreach ($results as &$result) {
$result = new Collection($result);
@@ -101,6 +151,56 @@ class PdoWrapper extends PDO
return $results;
}
+ /**
+ * Pulls the engine, database, and host from the DSN string.
+ *
+ * @param string $dsn The Data Source Name (DSN) string.
+ *
+ * @return array An associative array containing the engine, database, and host.
+ */
+ protected function pullDataFromDsn(string $dsn): array
+ {
+ // pull the engine from the dsn (sqlite, mysql, pgsql, etc)
+ preg_match('/^([a-zA-Z]+):/', $dsn, $matches);
+ $engine = $matches[1] ?? 'unknown';
+
+ if ($engine === 'sqlite') {
+ // pull the path from the dsn
+ preg_match('/sqlite:(.*)/', $dsn, $matches);
+ $dbname = basename($matches[1] ?? 'unknown');
+ $host = 'localhost';
+ } else {
+ // pull the database from the dsn
+ preg_match('/dbname=([^;]+)/', $dsn, $matches);
+ $dbname = $matches[1] ?? 'unknown';
+ // pull the host from the dsn
+ preg_match('/host=([^;]+)/', $dsn, $matches);
+ $host = $matches[1] ?? 'unknown';
+ }
+
+ return [
+ 'engine' => $engine,
+ 'database' => $dbname,
+ 'host' => $host
+ ];
+ }
+
+ /**
+ * Logs the executed queries through the event dispatcher.
+ *
+ * This method enables logging of all the queries executed by the PDO wrapper.
+ * It can be useful for debugging and monitoring purposes.
+ *
+ * @return void
+ */
+ public function logQueries(): void
+ {
+ if ($this->trackApmQueries === true && $this->connectionMetrics !== [] && $this->queryMetrics !== []) {
+ EventDispatcher::getInstance()->trigger('flight.db.queries', $this->connectionMetrics, $this->queryMetrics);
+ $this->queryMetrics = []; // Reset after logging
+ }
+ }
+
/**
* Don't worry about this guy. Converts stuff for IN statements
*
diff --git a/flight/net/Response.php b/flight/net/Response.php
index 9e4d58b..f9ee9a2 100644
--- a/flight/net/Response.php
+++ b/flight/net/Response.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace flight\net;
use Exception;
+use flight\core\EventDispatcher;
/**
* The Response class represents an HTTP response. The object
@@ -426,6 +427,7 @@ class Response
}
}
+ $start = microtime(true);
// Only for the v3 output buffering.
if ($this->v2_output_buffering === false) {
$this->processResponseCallbacks();
@@ -436,8 +438,9 @@ class Response
}
echo $this->body;
-
$this->sent = true;
+
+ EventDispatcher::getInstance()->trigger('flight.response.sent', $this, microtime(true) - $start);
}
/**
diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php
index 418897d..6873aa6 100644
--- a/tests/DispatcherTest.php
+++ b/tests/DispatcherTest.php
@@ -13,6 +13,7 @@ use InvalidArgumentException;
use PharIo\Manifest\InvalidEmailException;
use tests\classes\Hello;
use PHPUnit\Framework\TestCase;
+use tests\classes\ClassWithExceptionInConstruct;
use tests\classes\ContainerDefault;
use tests\classes\TesterClass;
use TypeError;
@@ -329,4 +330,17 @@ class DispatcherTest extends TestCase
$this->expectExceptionMessageMatches('#tests\\\\classes\\\\ContainerDefault::__construct\(\).+flight\\\\Engine, null given#');
$result = $this->dispatcher->execute([ContainerDefault::class, 'testTheContainer']);
}
+
+ public function testContainerDicePdoWrapperTestBadParams()
+ {
+ $dice = new \Dice\Dice();
+ $this->dispatcher->setContainerHandler(function ($class, $params) use ($dice) {
+ return $dice->create($class, $params);
+ });
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('This is an exception in the constructor');
+
+ $this->dispatcher->invokeCallable([ ClassWithExceptionInConstruct::class, '__construct' ]);
+ }
}
diff --git a/tests/EngineTest.php b/tests/EngineTest.php
index fc2e8ff..737f38d 100644
--- a/tests/EngineTest.php
+++ b/tests/EngineTest.php
@@ -845,31 +845,6 @@ class EngineTest extends TestCase
$this->expectOutputString('You got it boss!');
}
- public function testContainerDicePdoWrapperTestBadParams() {
- $engine = new Engine();
- $dice = new \Dice\Dice();
- $engine->registerContainerHandler(function ($class, $params) use ($dice) {
- return $dice->create($class, $params);
- });
-
- $engine->route('/container', Container::class.'->testThePdoWrapper');
- $engine->request()->url = '/container';
-
- // php 7.4 will throw a PDO exception, but php 8 will throw an ErrorException
- if(version_compare(PHP_VERSION, '8.1.0') >= 0) {
- $this->expectException(ErrorException::class);
- $this->expectExceptionMessageMatches("/Passing null to parameter/");
- } elseif(version_compare(PHP_VERSION, '8.0.0') >= 0) {
- $this->expectException(PDOException::class);
- $this->expectExceptionMessageMatches("/must be a valid data source name/");
- } else {
- $this->expectException(PDOException::class);
- $this->expectExceptionMessageMatches("/invalid data source name/");
- }
-
- $engine->start();
- }
-
public function testContainerDiceBadClass() {
$engine = new Engine();
$dice = new \Dice\Dice();
diff --git a/tests/EventSystemTest.php b/tests/EventSystemTest.php
index 6dba2c7..f95302a 100644
--- a/tests/EventSystemTest.php
+++ b/tests/EventSystemTest.php
@@ -16,6 +16,7 @@ class EventSystemTest extends TestCase
// Reset the Flight engine before each test to ensure a clean state
Flight::setEngine(new Engine());
Flight::app()->init();
+ Flight::eventDispatcher()->resetInstance(); // Clear any existing listeners
}
/**
@@ -224,4 +225,124 @@ class EventSystemTest extends TestCase
$this->assertTrue($secondCalled, 'Second listener should be called');
$this->assertFalse($thirdCalled, 'Third listener should not be called after propagation stopped');
}
+
+ /**
+ * Test that hasListeners() correctly identifies events with listeners.
+ */
+ public function testHasListeners()
+ {
+ $this->assertFalse(Flight::eventDispatcher()->hasListeners('test.event'), 'Event should not have listeners before registration');
+
+ Flight::onEvent('test.event', function () {
+ });
+
+ $this->assertTrue(Flight::eventDispatcher()->hasListeners('test.event'), 'Event should have listeners after registration');
+ }
+
+ /**
+ * Test that getListeners() returns the correct listeners for an event.
+ */
+ public function testGetListeners()
+ {
+ $callback1 = function () {
+ };
+ $callback2 = function () {
+ };
+
+ $this->assertEmpty(Flight::eventDispatcher()->getListeners('test.event'), 'Event should have no listeners before registration');
+
+ Flight::onEvent('test.event', $callback1);
+ Flight::onEvent('test.event', $callback2);
+
+ $listeners = Flight::eventDispatcher()->getListeners('test.event');
+ $this->assertCount(2, $listeners, 'Event should have two registered listeners');
+ $this->assertSame($callback1, $listeners[0], 'First listener should match the first callback');
+ $this->assertSame($callback2, $listeners[1], 'Second listener should match the second callback');
+ }
+
+ /**
+ * Test that getListeners() returns an empty array for events with no listeners.
+ */
+ public function testGetListenersForNonexistentEvent()
+ {
+ $listeners = Flight::eventDispatcher()->getListeners('nonexistent.event');
+ $this->assertIsArray($listeners, 'Should return an array for nonexistent events');
+ $this->assertEmpty($listeners, 'Should return an empty array for nonexistent events');
+ }
+
+ /**
+ * Test that getAllRegisteredEvents() returns all event names with registered listeners.
+ */
+ public function testGetAllRegisteredEvents()
+ {
+ $this->assertEmpty(Flight::eventDispatcher()->getAllRegisteredEvents(), 'No events should be registered initially');
+
+ Flight::onEvent('test.event1', function () {
+ });
+ Flight::onEvent('test.event2', function () {
+ });
+
+ $events = Flight::eventDispatcher()->getAllRegisteredEvents();
+ $this->assertCount(2, $events, 'Should return all registered event names');
+ $this->assertContains('test.event1', $events, 'Should contain the first event');
+ $this->assertContains('test.event2', $events, 'Should contain the second event');
+ }
+
+ /**
+ * Test that removeListener() correctly removes a specific listener from an event.
+ */
+ public function testRemoveListener()
+ {
+ $callback1 = function () {
+ return 'callback1';
+ };
+ $callback2 = function () {
+ return 'callback2';
+ };
+
+ Flight::onEvent('test.event', $callback1);
+ Flight::onEvent('test.event', $callback2);
+
+ $this->assertCount(2, Flight::eventDispatcher()->getListeners('test.event'), 'Event should have two listeners initially');
+
+ Flight::eventDispatcher()->removeListener('test.event', $callback1);
+
+ $listeners = Flight::eventDispatcher()->getListeners('test.event');
+ $this->assertCount(1, $listeners, 'Event should have one listener after removal');
+ $this->assertSame($callback2, $listeners[0], 'Remaining listener should be the second callback');
+ }
+
+ /**
+ * Test that removeAllListeners() correctly removes all listeners for an event.
+ */
+ public function testRemoveAllListeners()
+ {
+ Flight::onEvent('test.event', function () {
+ });
+ Flight::onEvent('test.event', function () {
+ });
+ Flight::onEvent('another.event', function () {
+ });
+
+ $this->assertTrue(Flight::eventDispatcher()->hasListeners('test.event'), 'Event should have listeners before removal');
+ $this->assertTrue(Flight::eventDispatcher()->hasListeners('another.event'), 'Another event should have listeners');
+
+ Flight::eventDispatcher()->removeAllListeners('test.event');
+
+ $this->assertFalse(Flight::eventDispatcher()->hasListeners('test.event'), 'Event should have no listeners after removal');
+ $this->assertTrue(Flight::eventDispatcher()->hasListeners('another.event'), 'Another event should still have listeners');
+ }
+
+ /**
+ * Test that trying to remove listeners for nonexistent events doesn't cause errors.
+ */
+ public function testRemoveListenersForNonexistentEvent()
+ {
+ // Should not throw any errors
+ Flight::eventDispatcher()->removeListener('nonexistent.event', function () {
+ });
+ Flight::eventDispatcher()->removeAllListeners('nonexistent.event');
+
+ $this->assertTrue(true, 'Removing listeners for nonexistent events should not throw errors');
+ }
}
diff --git a/tests/PdoWrapperTest.php b/tests/PdoWrapperTest.php
index 0f41a92..4e21a1b 100644
--- a/tests/PdoWrapperTest.php
+++ b/tests/PdoWrapperTest.php
@@ -5,8 +5,10 @@ declare(strict_types=1);
namespace tests;
use flight\database\PdoWrapper;
+use flight\core\EventDispatcher;
use PDOStatement;
use PHPUnit\Framework\TestCase;
+use ReflectionClass;
class PdoWrapperTest extends TestCase
{
@@ -120,4 +122,90 @@ class PdoWrapperTest extends TestCase
$rows = $this->pdo_wrapper->fetchAll('SELECT id FROM test WHERE id > ? AND name IN( ?) ', [ 0, 'one,two' ]);
$this->assertEquals(2, count($rows));
}
+
+ public function testPullDataFromDsn()
+ {
+ // Testing protected method using reflection
+ $reflection = new ReflectionClass($this->pdo_wrapper);
+ $method = $reflection->getMethod('pullDataFromDsn');
+ $method->setAccessible(true);
+
+ // Test SQLite DSN
+ $sqliteDsn = 'sqlite::memory:';
+ $sqliteResult = $method->invoke($this->pdo_wrapper, $sqliteDsn);
+ $this->assertEquals([
+ 'engine' => 'sqlite',
+ 'database' => ':memory:',
+ 'host' => 'localhost'
+ ], $sqliteResult);
+
+ // Test MySQL DSN
+ $mysqlDsn = 'mysql:host=localhost;dbname=testdb;charset=utf8';
+ $mysqlResult = $method->invoke($this->pdo_wrapper, $mysqlDsn);
+ $this->assertEquals([
+ 'engine' => 'mysql',
+ 'database' => 'testdb',
+ 'host' => 'localhost'
+ ], $mysqlResult);
+
+ // Test PostgreSQL DSN
+ $pgsqlDsn = 'pgsql:host=127.0.0.1;dbname=postgres';
+ $pgsqlResult = $method->invoke($this->pdo_wrapper, $pgsqlDsn);
+ $this->assertEquals([
+ 'engine' => 'pgsql',
+ 'database' => 'postgres',
+ 'host' => '127.0.0.1'
+ ], $pgsqlResult);
+ }
+
+ public function testLogQueries()
+ {
+ // Create a new PdoWrapper with tracking enabled
+ $trackingPdo = new PdoWrapper('sqlite::memory:', null, null, null, true);
+
+ // Create test table
+ $trackingPdo->exec('CREATE TABLE test_log (id INTEGER PRIMARY KEY, name TEXT)');
+
+ // Run some queries to populate metrics
+ $trackingPdo->runQuery('INSERT INTO test_log (name) VALUES (?)', ['test1']);
+ $trackingPdo->fetchAll('SELECT * FROM test_log');
+
+ // Setup event listener to capture triggered event
+ $eventTriggered = false;
+ $connectionData = null;
+ $queriesData = null;
+
+ $dispatcher = EventDispatcher::getInstance();
+ $dispatcher->on('flight.db.queries', function ($conn, $queries) use (&$eventTriggered, &$connectionData, &$queriesData) {
+ $eventTriggered = true;
+ $connectionData = $conn;
+ $queriesData = $queries;
+ });
+
+ // Call the logQueries method
+ $trackingPdo->logQueries();
+
+ // Assert that event was triggered
+ $this->assertTrue($eventTriggered);
+ $this->assertIsArray($connectionData);
+ $this->assertEquals('sqlite', $connectionData['engine']);
+ $this->assertIsArray($queriesData);
+ $this->assertCount(2, $queriesData); // Should have 2 queries (INSERT and SELECT)
+
+ // Verify query metrics structure for the first query
+ $this->assertArrayHasKey('sql', $queriesData[0]);
+ $this->assertArrayHasKey('params', $queriesData[0]);
+ $this->assertArrayHasKey('execution_time', $queriesData[0]);
+ $this->assertArrayHasKey('row_count', $queriesData[0]);
+ $this->assertArrayHasKey('memory_usage', $queriesData[0]);
+
+ // Clean up
+ $trackingPdo->exec('DROP TABLE test_log');
+
+ // Verify metrics are reset after logging
+ $reflection = new ReflectionClass($trackingPdo);
+ $property = $reflection->getProperty('queryMetrics');
+ $property->setAccessible(true);
+ $this->assertCount(0, $property->getValue($trackingPdo));
+ }
}
diff --git a/tests/classes/ClassWithExceptionInConstruct.php b/tests/classes/ClassWithExceptionInConstruct.php
new file mode 100644
index 0000000..32456c2
--- /dev/null
+++ b/tests/classes/ClassWithExceptionInConstruct.php
@@ -0,0 +1,13 @@
+