diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 5bf4e65..be4eaf9 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -23,16 +23,10 @@ jobs: #- name: Validate composer.json and composer.lock # run: composer validate - - name: Start Redis - uses: supercharge/redis-github-action@1.2.0 - with: - redis-version: ${{ matrix.redis-version }} - - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - extensions: redis ini-values: memory_limit=128M - name: Cache Composer packages diff --git a/README.md b/README.md index 8bf88cc..e9eb026 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ pop-debug * [Storage](#storage) - [File](#file) - [Database](#database) - - [Redis](#redis) * [Formats](#formats) +* [Retrieving](#retrieving) Overview -------- @@ -398,22 +398,6 @@ $debugger->setStorage(new Database($db, 'text', 'my_debug_table')); [Top](#pop-debug) -### Redis - -Store the debugger output into the Redis server cache. - -```php -use Pop\Debug\Debugger; -use Pop\Debug\Handler\TimeHandler; -use Pop\Debug\Storage\Redis; - -$debugger = new Debugger(); -$debugger->addHandler(new TimeHandler()); -$debugger->setStorage(new Redis()); -``` - -[Top](#pop-debug) - Formats ------- @@ -449,5 +433,65 @@ $fileStorage->setFormat('JSON'); $fileStorage->setFormat('PHP'); ``` +[Top](#pop-debug) + +Retrieving +---------- + +You can retrieve the stored debug content from the debugger. Calling the `save()` method returns the +request ID generated from that method call. + +```php +use Pop\Debug\Debugger; +use Pop\Debug\Handler\MessageHandler; +use Pop\Debug\Storage\File; + +$debugger = new Debugger(new MessageHandler(), new MemoryHandler(), new File(__DIR__ . '/logs')); +$debugger['message']->addMessage('Hey! Something happened!'); +$requestId = $debugger->save(); +``` + +The auto-generated request ID will look like: + +```text +857f0869d00b64db7c9dbdee4194781a +``` + +From there, you can call `getById` to retrieve stored debug content: + +```php +// A wildcard search +print_r($debugger->getById($requestId . '*')); +``` + +```text +Array +( + [0] => 857f0869d00b64db7c9dbdee4194781a-message.log +) +``` +```php +// An exact search by ID +print_r($debugger->getById('857f0869d00b64db7c9dbdee4194781a-message')); +``` +```text +1698773755.86070 Hey! Something happened! +``` + +The method `getByType` is also available to get groups of debug content by type: + +```php +print_r($debugger->getByType('message')); +``` + +```text +Array +( + [0] => 857f0869d00b64db7c9dbdee4194781a-message.log + [1] => 966dc22f1c34489d7d61de295aa008a9-message.log + [2] => f5c21a372ba375bce9b2382f67e3b70d-message.log +) + +``` [Top](#pop-debug) \ No newline at end of file diff --git a/src/Debugger.php b/src/Debugger.php index e32c67e..0630ae7 100644 --- a/src/Debugger.php +++ b/src/Debugger.php @@ -183,22 +183,78 @@ public function getData(): array { $data = []; foreach ($this->handlers as $name => $handler) { - $data[$name] = ($this->storage->getFormat() === null) ? $handler->prepareAsString() : $handler->prepare(); + $data[$name] = ($this->storage->getFormat() == 'TEXT') ? $handler->prepareAsString() : $handler->prepare(); } return $data; } /** - * Save the debug handlers' data to storage + * Get stored request by ID + * + * @param string $id + * @return mixed + */ + public function getById(string $id): mixed + { + return $this->storage->getById($id); + } + + /** + * Get stored request by type + * + * @param string $type + * @return mixed + */ + public function getByType(string $type): mixed + { + return $this->storage->getByType($type); + } + + /** + * Determine if debug data exists by ID + * + * @param string $id + * @return bool + */ + public function has(string $id): bool + { + return $this->storage->has($id); + } + + /** + * Delete debug data by ID + * + * @param string $id + * @return void + */ + public function delete(string $id): void + { + $this->storage->delete($id); + } + + /** + * Clear storage * * @return void */ - public function save(): void + public function clear(): void + { + $this->storage->clear(); + } + + /** + * Save the debug handlers' data to storage + * + * @return string + */ + public function save(): string { foreach ($this->handlers as $name => $handler) { - $data = ($this->storage->getFormat() === null) ? $handler->prepareAsString() : $handler->prepare(); + $data = ($this->storage->getFormat() == 'TEXT') ? $handler->prepareAsString() : $handler->prepare(); $this->storage->save($this->getRequestId() . '-' . $name, $data); } + + return $this->getRequestId(); } /** diff --git a/src/Storage/AbstractStorage.php b/src/Storage/AbstractStorage.php index 5340ab0..be0eb14 100644 --- a/src/Storage/AbstractStorage.php +++ b/src/Storage/AbstractStorage.php @@ -35,9 +35,9 @@ abstract class AbstractStorage implements StorageInterface /** * Storage format (json, php or text) - * @var ?string + * @var string */ - protected ?string $format = null; + protected string $format = 'TEXT'; /** * Constructor @@ -46,7 +46,7 @@ abstract class AbstractStorage implements StorageInterface * * @param ?string $format */ - public function __construct(?string $format = null) + public function __construct(?string $format = self::TEXT) { if ($format !== null) { $this->setFormat($format); @@ -61,17 +61,25 @@ public function __construct(?string $format = null) */ public function setFormat(string $format): AbstractStorage { - switch (strtoupper($format)) { - case self::JSON: - $this->format = self::JSON; - break; - case self::PHP: - $this->format = self::PHP; - } + $this->format = match (strtoupper($format)) { + self::JSON => self::JSON, + self::PHP => self::PHP, + default => self::TEXT, + }; return $this; } + /** + * Determine if the format is PHP + * + * @return bool + */ + public function isText(): bool + { + return ($this->format == self::TEXT); + } + /** * Determine if the format is PHP * @@ -112,15 +120,23 @@ public function getFormat(): ?string abstract public function save(string $id, mixed $value): void; /** - * Get debug data + * Get debug data by ID * * @param string $id * @return mixed */ - abstract public function get(string $id): mixed; + abstract public function getById(string $id): mixed; + + /** + * Get debug data by type + * + * @param string $type + * @return mixed + */ + abstract public function getByType(string $type): mixed; /** - * Determine if debug data exists + * Determine if debug data exists by * * @param string $id * @return bool @@ -128,7 +144,7 @@ abstract public function get(string $id): mixed; abstract public function has(string $id): bool; /** - * Delete debug data + * Delete debug data by id * * @param string $id * @return void diff --git a/src/Storage/Database.php b/src/Storage/Database.php index 05815e4..c8c2fd4 100644 --- a/src/Storage/Database.php +++ b/src/Storage/Database.php @@ -183,16 +183,17 @@ public function save(string $id, mixed $value): void } /** - * Get debug data + * Get debug data by ID * * @param string $id * @return mixed */ - public function get(string $id): mixed + public function getById(string $id): mixed { $sql = $this->db->createSql(); $placeholder = $sql->getPlaceholder(); $value = false; + $isWildcard = false; if ($placeholder == ':') { $placeholder .= 'key'; @@ -200,7 +201,13 @@ public function get(string $id): mixed $placeholder .= '1'; } - $sql->select()->from($this->table)->where('key = ' . $placeholder); + if (str_ends_with($id, '*') || str_ends_with($id, '%')) { + $sql->select()->from($this->table)->where('key LIKE ' . $placeholder); + $id = substr($id, 0, -1) . '%'; + $isWildcard = true; + } else { + $sql->select()->from($this->table)->where('key = ' . $placeholder); + } $this->db->prepare($sql) ->bindParams(['key' => $id]) @@ -209,7 +216,9 @@ public function get(string $id): mixed $rows = $this->db->fetchAll(); // If the value is found, return. - if (isset($rows[0]) && isset($rows[0]['value'])) { + if (($isWildcard) && isset($rows[0])) { + $value = $rows; + } else if (isset($rows[0]) && isset($rows[0]['value'])) { $value = $this->decodeValue($rows[0]['value']); } @@ -217,7 +226,40 @@ public function get(string $id): mixed } /** - * Determine if debug data exists + * Get debug data by type + * + * @param string $type + * @return mixed + */ + public function getByType(string $type): mixed + { + $sql = $this->db->createSql(); + $placeholder = $sql->getPlaceholder(); + $value = false; + + if ($placeholder == ':') { + $placeholder .= 'key'; + } else if ($placeholder == '$') { + $placeholder .= '1'; + } + + $sql->select()->from($this->table)->where('key LIKE ' . $placeholder); + $this->db->prepare($sql) + ->bindParams(['key' => '%' . $type]) + ->execute(); + + $rows = $this->db->fetchAll(); + + // If the value is found, return. + if (isset($rows[0])) { + $value = $rows; + } + + return $value; + } + + /** + * Determine if debug data exists by ID * * @param string $id * @return bool @@ -245,7 +287,7 @@ public function has(string $id): bool } /** - * Delete debug data + * Delete debug data by ID * * @param string $id * @return void diff --git a/src/Storage/File.php b/src/Storage/File.php index 484be5e..d1d6fce 100644 --- a/src/Storage/File.php +++ b/src/Storage/File.php @@ -99,18 +99,38 @@ public function save(string $id, mixed $value): void } /** - * Get debug data + * Get debug data by ID * * @param string $id * @return mixed */ - public function get(string $id): mixed + public function getById(string $id): mixed { - return $this->decodeValue($this->dir . DIRECTORY_SEPARATOR . $id); + if (str_ends_with($id, '*') || str_ends_with($id, '%')) { + $id = substr($id, 0, -1); + return array_values(array_filter(scandir($this->dir), function($value) use ($id) { + return (($value != '.') && ($value != '..')) && str_starts_with($value, $id); + })); + } else { + return $this->decodeValue($this->dir . DIRECTORY_SEPARATOR . $id); + } + } + + /** + * Get debug data by type + * + * @param string $type + * @return mixed + */ + public function getByType(string $type): mixed + { + return array_values(array_filter(scandir($this->dir), function($value) use ($type) { + return (($value != '.') && ($value != '..')) && str_contains($value, $type); + })); } /** - * Determine if debug data exists + * Determine if debug data exists by ID * * @param string $id * @return bool @@ -120,18 +140,24 @@ public function has(string $id): bool $fileId = $this->dir . DIRECTORY_SEPARATOR . $id; if ($this->format == self::JSON) { - $fileId .= '.json'; + if (!str_ends_with($fileId, '.json')) { + $fileId .= '.json'; + } } else if ($this->format == self::PHP) { - $fileId .= '.php'; + if (!str_ends_with($fileId, '.php')) { + $fileId .= '.php'; + } } else { - $fileId .= '.log'; + if (!str_ends_with($fileId, '.log')) { + $fileId .= '.log'; + } } return (file_exists($fileId)); } /** - * Delete debug data + * Delete debug data by ID * * @param string $id * @return void @@ -141,11 +167,17 @@ public function delete(string $id): void $fileId = $this->dir . DIRECTORY_SEPARATOR . $id; if ($this->format == self::JSON) { - $fileId .= '.json'; + if (!str_ends_with($fileId, '.json')) { + $fileId .= '.json'; + } } else if ($this->format == self::PHP) { - $fileId .= '.php'; + if (!str_ends_with($fileId, '.php')) { + $fileId .= '.php'; + } } else { - $fileId .= '.log'; + if (!str_ends_with($fileId, '.log')) { + $fileId .= '.log'; + } } if (file_exists($fileId)) { @@ -204,13 +236,19 @@ public function encodeValue(mixed $value): string public function decodeValue(mixed $value): mixed { if ($this->format == self::JSON) { - $value .= '.json'; + if (!str_ends_with($value, '.json')) { + $value .= '.json'; + } $value = (file_exists($value)) ? json_decode(file_get_contents($value), true) : false; } else if ($this->format == self::PHP) { - $value .= '.php'; + if (!str_ends_with($value, '.php')) { + $value .= '.php'; + } $value = (file_exists($value)) ? include $value : false; } else { - $value .= '.log'; + if (!str_ends_with($value, '.log')) { + $value .= '.log'; + } $value = (file_exists($value)) ? file_get_contents($value) : false; } diff --git a/src/Storage/Redis.php b/src/Storage/Redis.php deleted file mode 100644 index de7cab0..0000000 --- a/src/Storage/Redis.php +++ /dev/null @@ -1,175 +0,0 @@ - - * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) - * @license http://www.popphp.org/license New BSD License - */ - -/** - * @namespace - */ -namespace Pop\Debug\Storage; - -use RedisException; - -/** - * Debug redis storage class - * - * @category Pop - * @package Pop\Debug - * @author Nick Sagona, III - * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) - * @license http://www.popphp.org/license New BSD License - * @version 2.0.0 - */ -class Redis extends AbstractStorage -{ - - /** - * Redis object - * @var \Redis|null - */ - protected \Redis|null $redis = null; - - /** - * Constructor - * - * Instantiate the Redis storage object - * - * @param ?string $format - * @param string $host - * @param int $port - * @throws Exception|RedisException - */ - public function __construct(?string $format = null, string $host = 'localhost', int $port = 6379) - { - if (!class_exists('Redis', false)) { - throw new Exception('Error: Redis is not available.'); - } - - parent::__construct($format); - - $this->redis = new \Redis(); - if (!$this->redis->connect($host, (int)$port)) { - throw new Exception('Error: Unable to connect to the redis server.'); - } - } - - /** - * Get the redis object. - * - * @return \Redis - */ - public function redis(): \Redis - { - return $this->redis; - } - - /** - * Get the current version of redis. - * - * @return string - */ - public function getVersion(): string - { - return $this->redis->info()['redis_version']; - } - - /** - * Save debug data - * - * @param string $id - * @param mixed $value - * @return void - */ - public function save(string $id, mixed $value): void - { - $this->redis->set($id, $this->encodeValue($value)); - } - - /** - * Get debug data - * - * @param string $id - * @return mixed - */ - public function get(string $id): mixed - { - return $this->decodeValue($this->redis->get($id)); - } - - /** - * Determine if debug data exists - * - * @param string $id - * @return bool - */ - public function has(string $id): bool - { - return ($this->redis->get($id) !== false); - } - - /** - * Delete debug data - * - * @param string $id - * @return void - */ - public function delete(string $id): void - { - $this->redis->del($id); - } - - /** - * Clear all debug data - * - * @return void - */ - public function clear(): void - { - $this->redis->flushDb(); - } - - /** - * Encode the value based on the format - * - * @param mixed $value - * @throws Exception - * @return string - */ - public function encodeValue(mixed $value): string - { - if ($this->format == self::JSON) { - $value = json_encode($value, JSON_PRETTY_PRINT); - } else if ($this->format == self::PHP) { - $value = serialize($value); - } else if (!is_string($value)) { - throw new Exception('Error: The value must be a string if storing as a text file.'); - } - - return $value; - } - - /** - * Decode the value based on the format - * - * @param mixed $value - * @return mixed - */ - public function decodeValue(mixed $value): mixed - { - if ($value !== false) { - if ($this->format == self::JSON) { - $value = json_decode($value, true); - } else if ($this->format == self::PHP) { - $value = unserialize($value); - } - } - - return $value; - } - -} diff --git a/src/Storage/StorageInterface.php b/src/Storage/StorageInterface.php index 6d4ca53..0ca8bb3 100644 --- a/src/Storage/StorageInterface.php +++ b/src/Storage/StorageInterface.php @@ -34,6 +34,13 @@ interface StorageInterface */ public function setFormat(string $format): StorageInterface; + /** + * Determine if the format is PHP + * + * @return bool + */ + public function isText(): bool; + /** * Determine if the format is PHP * @@ -65,15 +72,23 @@ public function getFormat(): ?string; public function save(string $id, mixed $value): void; /** - * Get debug data + * Get debug data by ID * * @param string $id * @return mixed */ - public function get(string $id): mixed; + public function getById(string $id): mixed; + + /** + * Get debug data by type + * + * @param string $type + * @return mixed + */ + public function getByType(string $type): mixed; /** - * Determine if debug data exists + * Determine if debug data exists by ID * * @param string $id * @return bool @@ -81,7 +96,7 @@ public function get(string $id): mixed; public function has(string $id): bool; /** - * Delete debug data + * Delete debug data by ID * * @param string $id * @return void diff --git a/tests/DebuggerTest.php b/tests/DebuggerTest.php index 4f58591..a74cb3c 100644 --- a/tests/DebuggerTest.php +++ b/tests/DebuggerTest.php @@ -100,7 +100,6 @@ public function testSave() $debugger['memory']->updatePeakMemoryUsage(); $debugger->save(); - $dh = @opendir(__DIR__ . '/tmp'); while (false !== ($obj = readdir($dh))) { @@ -116,6 +115,23 @@ public function testSave() } } + public function testGet() + { + $debugger = new Debugger([ + new Handler\MemoryHandler(), + new Storage\File(__DIR__ . '/tmp') + ]); + $debugger['memory']->updatePeakMemoryUsage(); + $requestId = $debugger->save(); + + $this->assertIsArray($debugger->getByType('memory')); + $this->assertTrue($debugger->has($requestId . '-memory')); + $this->assertTrue(str_contains($debugger->getById($requestId . '-memory'), 'Usage')); + $debugger->delete($requestId . '-memory'); + $debugger->clear(); + $this->assertFalse($debugger->has($requestId . '-memory')); + } + public function testRender() { $debugger = new Debugger([ diff --git a/tests/Storage/DatabaseTest.php b/tests/Storage/DatabaseTest.php index be48ad7..ea8d637 100644 --- a/tests/Storage/DatabaseTest.php +++ b/tests/Storage/DatabaseTest.php @@ -25,10 +25,10 @@ public function testSave() $db = new Storage\Database(Db::sqliteConnect(['database' => __DIR__ . '/../tmp/debug.sqlite'])); $db->save(123456, 'Hello World!'); $this->assertTrue($db->has(123456)); - $this->assertEquals('Hello World!', $db->get(123456)); + $this->assertEquals('Hello World!', $db->getById(123456)); $db->save(123456, 'Hello World 2!'); $this->assertTrue($db->has(123456)); - $this->assertEquals('Hello World 2!', $db->get(123456)); + $this->assertEquals('Hello World 2!', $db->getById(123456)); } public function testEncodeException() @@ -58,10 +58,19 @@ public function testSaveJson() $db = new Storage\Database(Db::sqliteConnect(['database' => __DIR__ . '/../tmp/debug.sqlite']), 'JSON'); $db->save(123456, ['foo' => 'bar']); $this->assertTrue($db->has(123456)); - $value = $db->get(123456); + $value = $db->getById(123456); $this->assertTrue(is_array($value)); $this->assertTrue(isset($value['foo'])); $this->assertEquals('bar', $value['foo']); + $value = $db->getById('123456*'); + $this->assertTrue(is_array($value)); + } + + public function testSaveAndGetByType() + { + $db = new Storage\Database(Db::sqliteConnect(['database' => __DIR__ . '/../tmp/debug.sqlite']), 'JSON'); + $db->save('123456-message', ['foo' => 'bar']); + $this->assertIsArray($db->getByType('message')); } public function testSavePhp() @@ -69,7 +78,7 @@ public function testSavePhp() $db = new Storage\Database(Db::sqliteConnect(['database' => __DIR__ . '/../tmp/debug.sqlite']), 'PHP'); $db->save(123456, ['foo' => 'bar']); $this->assertTrue($db->has(123456)); - $value = $db->get(123456); + $value = $db->getById(123456); $this->assertTrue(is_array($value)); $this->assertTrue(isset($value['foo'])); $this->assertEquals('bar', $value['foo']); diff --git a/tests/Storage/FileTest.php b/tests/Storage/FileTest.php index 8af4168..4ca3a59 100644 --- a/tests/Storage/FileTest.php +++ b/tests/Storage/FileTest.php @@ -13,7 +13,7 @@ public function testConstructor() $file = new Storage\File(__DIR__ . '/../tmp'); $this->assertInstanceOf('Pop\Debug\Storage\File', $file); $this->assertEquals(realpath(__DIR__ . '/../tmp'), $file->getDir()); - $this->assertNull($file->getFormat()); + $this->assertEquals('TEXT', $file->getFormat()); $this->assertFalse($file->isJson()); $this->assertFalse($file->isPhp()); @@ -31,7 +31,8 @@ public function testSaveAndGetText() $file = new Storage\File(__DIR__ . '/../tmp'); $file->save($time, 'Hello World'); $this->assertTrue($file->has($time)); - $this->assertEquals('Hello World', $file->get($time)); + $this->assertEquals('Hello World', $file->getById($time)); + $this->assertIsArray($file->getById($time . '*')); $file->delete($time); } @@ -41,7 +42,7 @@ public function testSaveAndGetJson() $file = new Storage\File(__DIR__ . '/../tmp', 'json'); $file->save($time, 'Hello World'); $this->assertTrue($file->has($time)); - $this->assertEquals('Hello World', $file->get($time)); + $this->assertEquals('Hello World', $file->getById($time)); $file->delete($time); } @@ -51,10 +52,19 @@ public function testSaveAndGetPhp() $file = new Storage\File(__DIR__ . '/../tmp', 'php'); $file->save($time, 'Hello World'); $this->assertTrue($file->has($time)); - $this->assertEquals('Hello World', $file->get($time)); + $this->assertEquals('Hello World', $file->getById($time)); $file->delete($time); } + public function testSaveAndGetByType() + { + $time = time(); + $file = new Storage\File(__DIR__ . '/../tmp'); + $file->save($time . '-message', 'Hello World'); + $this->assertIsArray($file->getByType('message')); + $file->delete($time . '-message'); + } + public function testSaveAndGetTextException() { $this->expectException('Pop\Debug\Storage\Exception'); diff --git a/tests/Storage/RedisTest.php b/tests/Storage/RedisTest.php deleted file mode 100644 index 7340110..0000000 --- a/tests/Storage/RedisTest.php +++ /dev/null @@ -1,74 +0,0 @@ -assertInstanceOf('Pop\Debug\Storage\Redis', $redis); - $this->assertInstanceOf('Redis', $redis->redis()); - $this->assertNotEmpty($redis->getVersion()); - } - - public function testSave() - { - $redis = new Storage\Redis(); - $redis->save(123456, 'Hello World!'); - $this->assertTrue($redis->has(123456)); - $this->assertEquals('Hello World!', $redis->get(123456)); - $redis->save(123456, 'Hello World 2!'); - $this->assertTrue($redis->has(123456)); - $this->assertEquals('Hello World 2!', $redis->get(123456)); - } - - public function testEncodeException() - { - $this->expectException('Pop\Debug\Storage\Exception'); - $redis = new Storage\Redis(); - $redis->save(123456, ['Hello World!']); - } - - public function testDelete() - { - $redis = new Storage\Redis(); - $this->assertTrue($redis->has(123456)); - $redis->delete(123456); - $this->assertFalse($redis->has(123456)); - } - - public function testClear() - { - $redis = new Storage\Redis(); - $redis->clear(); - $this->assertFalse($redis->has(123456)); - } - - public function testSaveJson() - { - $redis = new Storage\Redis('JSON'); - $redis->save(123456, ['foo' => 'bar']); - $this->assertTrue($redis->has(123456)); - $value = $redis->get(123456); - $this->assertTrue(is_array($value)); - $this->assertTrue(isset($value['foo'])); - $this->assertEquals('bar', $value['foo']); - } - - public function testSavePhp() - { - $redis = new Storage\Redis('PHP'); - $redis->save(123456, ['foo' => 'bar']); - $this->assertTrue($redis->has(123456)); - $value = $redis->get(123456); - $this->assertTrue(is_array($value)); - $this->assertTrue(isset($value['foo'])); - $this->assertEquals('bar', $value['foo']); - } - -} \ No newline at end of file