diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c1cbb29
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,46 @@
+# GitHub Action
+name: Tests
+on: [push, pull_request]
+
+jobs:
+ tests:
+ name: Tests PHP ${{ matrix.php-versions }}
+ runs-on: ${{ matrix.operating-system }}
+ strategy:
+ matrix:
+ operating-system: [ubuntu-latest]
+ php-versions: ['8.1']
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ # Docs: https://github.com/shivammathur/setup-php
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ coverage: xdebug
+
+ - name: Get Composer cache directory
+ id: composer-cache
+ run: echo "::set-output name=dir::$(composer config cache-files-dir)"
+
+ - name: Cache Composer dependencies
+ uses: actions/cache@v2
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - name: Install dependencies
+ run: composer install --no-progress --prefer-dist --no-interaction --optimize-autoloader
+
+ - name: Run unit tests
+ run: vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover=report/logs/clover.xml
+
+ - name: Upload coverage results to Coveralls
+ env:
+ COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ composer global require php-coveralls/php-coveralls -n
+ php-coveralls --coverage_clover=report/logs/clover.xml --json_path=report/logs/coveralls-upload.json -v
diff --git a/composer.json b/composer.json
index c6461e0..5ef7e5f 100644
--- a/composer.json
+++ b/composer.json
@@ -17,16 +17,16 @@
}
],
"require": {
- "php": "^7.2",
+ "php": "^8.1",
"ext-pdo": "*",
"ext-pdo_mysql": "*",
- "monolog/monolog": "^2.0"
+ "monolog/monolog": "^2.0 || ^3.0"
},
"require-dev": {
- "phpunit/phpunit": "^8.5",
- "doctrine/dbal": "^2.10",
- "psy/psysh": "^0.10.4",
- "fzaninotto/faker": "^1.9"
+ "phpunit/phpunit": "^9.5",
+ "doctrine/dbal": "^3.5",
+ "psy/psysh": "^0.11.10",
+ "fakerphp/faker": "^1.21"
},
"autoload": {
"psr-4": {
diff --git a/phpunit.xml b/phpunit.xml
deleted file mode 100644
index 7972d5c..0000000
--- a/phpunit.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
- ./tests
-
-
-
-
- ./app
-
-
-
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..44cf4fe
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,13 @@
+
+
+
+
+ ./src
+
+
+
+
+ ./tests
+
+
+
diff --git a/src/MySQLHandler.php b/src/MySQLHandler.php
index 92e6047..e92d68c 100644
--- a/src/MySQLHandler.php
+++ b/src/MySQLHandler.php
@@ -5,7 +5,8 @@
namespace MySQLHandler;
use Monolog\Handler\AbstractProcessingHandler;
-use Monolog\Logger;
+use Monolog\Level;
+use Monolog\LogRecord;
use PDO;
use PDOStatement;
@@ -51,7 +52,7 @@ class MySQLHandler extends AbstractProcessingHandler
* @param string $table Table in the database to store the logs in
* @param array $additionalFields Additional Context Parameters to store in database
* @param bool $initialize Defines whether attempts to alter database should be skipped
- * @param bool|int $level Debug level which this handler should store
+ * @param int|string|Level $level Debug level which this handler should store
* @param bool $bubble
*/
public function __construct(
@@ -59,7 +60,7 @@ public function __construct(
string $table,
array $additionalFields = [],
bool $initialize = false,
- int $level = Logger::DEBUG,
+ int|string|Level $level = Level::Debug,
bool $bubble = true
) {
parent::__construct($level, $bubble);
@@ -76,13 +77,13 @@ private function initialize(): void
{
$this->pdo->exec("
CREATE TABLE IF NOT EXISTS `{$this->mySQLRecord->getTable()}` (
- id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
- channel VARCHAR(255),
- level INTEGER,
- message LONGTEXT,
- time INTEGER UNSIGNED,
- INDEX(channel) USING HASH,
- INDEX(level) USING HASH,
+ id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ channel VARCHAR(255),
+ level INTEGER,
+ message LONGTEXT,
+ time INTEGER UNSIGNED,
+ INDEX(channel) USING HASH,
+ INDEX(level) USING HASH,
INDEX(time) USING BTREE
);
");
@@ -123,7 +124,7 @@ private function initialize(): void
}
/**
- * Prepare the sql statment depending on the fields that should be written to the database
+ * Prepare the sql statement depending on the fields that should be written to the database
* @param array $content
*/
private function prepareStatement(array $content): void
@@ -155,10 +156,10 @@ private function prepareStatement(array $content): void
/**
* Writes the record down to the log of the implementing handler
*
- * @param array $record
+ * @param LogRecord $record
* @return void
*/
- protected function write(array $record): void
+ protected function write(LogRecord $record): void
{
if (! $this->initialized) {
$this->initialize();
@@ -169,19 +170,19 @@ protected function write(array $record): void
* getting added to $record['extra']
* @see https://github.com/Seldaek/monolog/blob/master/doc/02-handlers-formatters-processors.md
*/
- if (isset($record['extra'])) {
- $record['context'] = array_merge($record['context'], $record['extra']);
- }
-
$content = $this->mySQLRecord->filterContent(array_merge([
'channel' => $record['channel'],
'level' => $record['level'],
'message' => $record['message'],
'time' => $record['datetime']->format('U'),
- ], $record['context']));
+ ], $record['context'], $record['extra']));
$this->prepareStatement($content);
+ if (array_key_exists('id', $content)) {
+ unset($content['id']);
+ }
+
$this->statement->execute($content);
}
}
diff --git a/tests/MySQLHandlerTest.php b/tests/MySQLHandlerTest.php
new file mode 100644
index 0000000..225517b
--- /dev/null
+++ b/tests/MySQLHandlerTest.php
@@ -0,0 +1,198 @@
+createMock(\PDO::class);
+ $columnsStmt = $this->createMock(\PDOStatement::class);
+ $insertStmt = $this->createMock(\PDOStatement::class);
+
+ $isHandling = $record['level'] >= $level;
+ $expectedResult = (!$isHandling) ? false : false === $bubble;
+ $mysqlRecord = new MySQLRecord($table, $additionalFields);
+ $addedColumns = array_diff($additionalFields, $initialColumns);
+ $removedColumns = array_diff($initialColumns, $mysqlRecord->getDefaultColumns(), $additionalFields);
+
+ if ($isHandling) {
+ if (!$initialize) {
+ $pdo->expects($this->exactly(1 + count($removedColumns) + count($addedColumns)))
+ ->method('exec')
+ ->withConsecutive(
+ [sprintf(static::$createSql, $table)],
+ ...array_map(static function (string $c) use ($table) {
+ return ["ALTER TABLE `{$table}` DROP `{$c}`;"];
+ }, $removedColumns),
+ ...array_map(static function (string $c) use ($table) {
+ return ["ALTER TABLE `{$table}` ADD `{$c}` TEXT NULL DEFAULT NULL;"];
+ }, $addedColumns)
+ )
+ ->willReturn(0);
+
+ $pdo->expects($this->once())
+ ->method('query')
+ ->with("SELECT * FROM `{$table}` LIMIT 0;")
+ ->willReturn($columnsStmt);
+ $columnsStmt->expects($this->exactly(count($initialColumns) + 1))
+ ->method('columnCount')
+ ->willReturn(count($initialColumns));
+
+ $columnsStmt->expects($this->exactly(count($initialColumns)))
+ ->method('getColumnMeta')
+ ->willReturn(...array_map(static function (string $c) {
+ return ['name' => $c];
+ }, $initialColumns));
+ } else {
+ $pdo->expects($this->never())->method('exec');
+ $pdo->expects($this->never())->method('query');
+ }
+
+ $pdo->expects($this->once())
+ ->method('prepare')
+ ->with($expectedInsert)
+ ->willReturn($insertStmt);
+
+ $insertStmt->expects($this->once())
+ ->method('execute')
+ ->with($expectedParams)
+ ->willReturn(true);
+ }
+
+ $handler = new MySQLHandler($pdo, $table, $additionalFields, $initialize, $level, $bubble);
+ $this->assertEquals($expectedResult, $handler->handle($record));
+ }
+
+ public function provideFakeData(): array
+ {
+ $faker = Factory::create();
+ $faker->seed('1234');
+ // Monolog 3.2.0 contains enum Level class while old static method deprecated
+ $loggerLevels = class_exists(\Monolog\Level::class) ? \Monolog\Level::VALUES : \Monolog\Logger::getLevels();
+ $data = [];
+
+ while (count($data) < 50) {
+ $initialColumns = $faker->unique()->words(5);
+ $additionalFields = $faker->unique()->words(5);
+ $table = $faker->unique()->word();
+ $time = $faker->unixTime();
+ $channel = $faker->unique()->word();
+ $level = $faker->randomElement($loggerLevels);
+ $msg = $faker->text;
+ $context = array_reduce(
+ $faker->randomElements(array_merge($faker->unique()->words(5), $additionalFields), 3),
+ static function (array $carry, string $f) use ($faker) {
+ $carry[$f] = $faker->word();
+ return $carry;
+ },
+ ['id' => 'foobar'],
+ );
+ $extra = array_reduce(
+ $faker->randomElements(array_merge($faker->unique()->words(5), $additionalFields), 3),
+ static function (array $carry, string $f) use ($faker) {
+ $carry[$f] = $faker->word();
+ return $carry;
+ },
+ ['id' => 'foobaz']
+ );
+
+ $record = new LogRecord(
+ \DateTimeImmutable::createFromFormat('U', strval($time)),
+ $channel,
+ \Monolog\Logger::toMonologLevel($level),
+ $msg,
+ $context,
+ $extra
+ );
+
+ $params = array_merge(
+ [
+ 'channel' => $channel,
+ 'level' => $level,
+ 'message' => $msg,
+ 'time' => strval($time),
+ ],
+ array_filter(
+ $context,
+ static function ($v, $k) use ($additionalFields) {
+ return in_array($k, $additionalFields);
+ },
+ ARRAY_FILTER_USE_BOTH
+ ),
+ array_filter(
+ $extra,
+ static function ($v, $k) use ($additionalFields) {
+ return in_array($k, $additionalFields);
+ },
+ ARRAY_FILTER_USE_BOTH
+ ),
+ );
+
+ if (array_key_exists('id', $params)) {
+ unset($params['id']);
+ }
+
+ $insertSql = sprintf(
+ 'INSERT INTO `%s` (%s) VALUES (%s);',
+ $table,
+ implode(', ', array_keys($params)),
+ ':' . implode(', :', array_keys($params))
+ );
+
+ $data[] = [
+ $insertSql,
+ $params,
+ $initialColumns,
+ $record,
+ $table,
+ $additionalFields, // additional fields
+ $faker->boolean(), // initialize,
+ $faker->randomElement($loggerLevels), // level
+ $faker->boolean(), // bubble,
+ ];
+ }
+
+ return $data;
+ }
+}
diff --git a/tests/MySQLRecordTest.php b/tests/MySQLRecordTest.php
index 46f43d7..e4f71fe 100644
--- a/tests/MySQLRecordTest.php
+++ b/tests/MySQLRecordTest.php
@@ -5,7 +5,6 @@
namespace Tests;
use Faker\Factory;
-use Monolog\Logger;
use MySQLHandler\MySQLRecord;
use PHPUnit\Framework\TestCase;
@@ -15,6 +14,14 @@
*/
class MySQLRecordTest extends TestCase
{
+ protected static array $defaultColumns = [
+ 'id',
+ 'channel',
+ 'level',
+ 'message',
+ 'time',
+ ];
+
/**
* @test
* @return void
@@ -25,14 +32,11 @@ public function get_columns_will_equals_default_columns_and_additional_columns_m
$table = strtolower($faker->unique()->word);
$columns = array_pad([], 5, strtolower($faker->unique()->word));
$record = new MySQLRecord($table, $columns);
+ $this->assertEquals(static::$defaultColumns, $record->getDefaultColumns());
+ $this->assertEquals($columns, $record->getAdditionalColumns());
- $this->assertEquals(array_merge([
- 'id',
- 'channel',
- 'level',
- 'message',
- 'time',
- ], $columns), $record->getColumns());
+ $this->assertEquals(array_merge(static::$defaultColumns, $columns), $record->getColumns());
+ $this->assertEquals($table, $record->getTable());
}
/**
@@ -45,10 +49,14 @@ public function filter_content_will_equals_argument(): void
$table = strtolower($faker->unique()->word);
$columns = array_pad([], 5, strtolower($faker->unique()->word));
$record = new MySQLRecord($table, $columns);
+ $this->assertEquals(static::$defaultColumns, $record->getDefaultColumns());
+ $this->assertEquals($columns, $record->getAdditionalColumns());
+ // Monolog 3.2.0 contains enum Level class while old static method deprecated
+ $loggerLevels = class_exists(\Monolog\Level::class) ? \Monolog\Level::VALUES : \Monolog\Logger::getLevels();
$data = array_merge([
'channel' => strtolower($faker->unique()->word),
- 'level' => $faker->randomElement(Logger::getLevels()),
+ 'level' => $faker->randomElement($loggerLevels),
'message' => $faker->text,
'time' => $faker->dateTime,
], array_fill_keys($columns, $faker->text));
@@ -56,6 +64,7 @@ public function filter_content_will_equals_argument(): void
$content = $record->filterContent($data);
$this->assertEquals($data, $content);
+ $this->assertEquals($table, $record->getTable());
}
/**
@@ -69,10 +78,14 @@ public function filter_content_will_exclude_out_of_columns(): void
$columns = array_pad([], 5, strtolower($faker->unique()->word));
$outOfColumns = array_pad([], 5, strtolower($faker->unique()->word));
$record = new MySQLRecord($table, $columns);
+ $this->assertEquals(static::$defaultColumns, $record->getDefaultColumns());
+ $this->assertEquals($columns, $record->getAdditionalColumns());
+ // Monolog 3.2.0 contains enum Level class while old static method deprecated
+ $loggerLevels = class_exists(\Monolog\Level::class) ? \Monolog\Level::VALUES : \Monolog\Logger::getLevels();
$data = array_merge([
'channel' => strtolower($faker->unique()->word),
- 'level' => $faker->randomElement(Logger::getLevels()),
+ 'level' => $faker->randomElement($loggerLevels),
'message' => $faker->text,
'time' => $faker->dateTime,
], array_fill_keys($outOfColumns, $faker->text));
@@ -87,5 +100,6 @@ public function filter_content_will_exclude_out_of_columns(): void
array_map(function ($key) use ($content) {
$this->assertArrayNotHasKey($key, $content);
}, $outOfColumns);
+ $this->assertEquals($table, $record->getTable());
}
-}
\ No newline at end of file
+}