From d18c99736929c08d773ca36eb2e25f1b80f54b7e Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 22 Aug 2017 13:08:09 +0200 Subject: [PATCH 1/8] Create a PSR3 compatible Logger interface with tests --- composer.json | 1 + src/Logger/LogLevel.php | 20 +++ src/Logger/Logger.php | 116 ++++++++++++++++ src/Logger/LoggerAware.php | 19 +++ src/Logger/NullLogger.php | 129 ++++++++++++++++++ tests/Fixture/Logger/Psr3LoggerAdapter.php | 147 +++++++++++++++++++++ tests/Unit/Logger/NullLoggerTest.php | 33 +++++ 7 files changed, 465 insertions(+) create mode 100644 src/Logger/LogLevel.php create mode 100644 src/Logger/Logger.php create mode 100644 src/Logger/LoggerAware.php create mode 100644 src/Logger/NullLogger.php create mode 100644 tests/Fixture/Logger/Psr3LoggerAdapter.php create mode 100644 tests/Unit/Logger/NullLoggerTest.php diff --git a/composer.json b/composer.json index 425932e1..142c0a81 100755 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "mikey179/vfsStream": "^1.6", "phpunit/phpunit": "^4.8.35 || ^6.0", "psr/container": "^1.0", + "psr/log": "^1.0", "psr/simple-cache": "^1.0" }, "autoload": { diff --git a/src/Logger/LogLevel.php b/src/Logger/LogLevel.php new file mode 100644 index 00000000..c1415c68 --- /dev/null +++ b/src/Logger/LogLevel.php @@ -0,0 +1,20 @@ +logger = $logger; + } + + /** + * System is unusable. + * + * @param string $message + * @param array $context + * @return null + */ + public function emergency($message, array $context = array()) + { + return $this->logger->emergency($message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * @return null + */ + public function alert($message, array $context = array()) + { + return $this->logger->alert($message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * @return null + */ + public function critical($message, array $context = array()) + { + return $this->logger->critical($message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * @return null + */ + public function error($message, array $context = array()) + { + return $this->logger->error($message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * @return null + */ + public function warning($message, array $context = array()) + { + return $this->logger->warning($message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * @return null + */ + public function notice($message, array $context = array()) + { + return $this->logger->notice($message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * @return null + */ + public function info($message, array $context = array()) + { + return $this->logger->info($message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * @return null + */ + public function debug($message, array $context = array()) + { + return $this->logger->debug($message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * @return null + */ + public function log($level, $message, array $context = array()) + { + return $this->logger->log($leverl, $message, $context); + } +} diff --git a/tests/Unit/Logger/NullLoggerTest.php b/tests/Unit/Logger/NullLoggerTest.php new file mode 100644 index 00000000..8f63bb91 --- /dev/null +++ b/tests/Unit/Logger/NullLoggerTest.php @@ -0,0 +1,33 @@ +assertInstanceOf('\\YoutubeDownloader\\Logger\\Logger', $logger); + } + + /** + * @test NullLogger is compatible with Psr\Log\LoggerInterface + */ + public function isPsr3Compatible() + { + $logger = new NullLogger(); + + $adapter = new Psr3LoggerAdapter($logger); + + $this->assertInstanceOf('\\Psr\\Log\\LoggerInterface', $adapter); + $this->assertInstanceOf('\\YoutubeDownloader\\Logger\\Logger', $adapter); + } +} From 7e6a3f695b746538b5a728748b0b9e8d3bdbb934 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 22 Aug 2017 14:32:37 +0200 Subject: [PATCH 2/8] Create a Logger that works with Handler, add tests --- src/Logger/Handler/Entry.php | 37 ++++ src/Logger/Handler/Handler.php | 25 +++ src/Logger/Handler/SimpleEntry.php | 73 +++++++ src/Logger/HandlerAwareLogger.php | 199 ++++++++++++++++++ tests/Unit/Logger/Handler/SimpleEntryTest.php | 79 +++++++ tests/Unit/Logger/HandlerAwareLoggerTest.php | 89 ++++++++ 6 files changed, 502 insertions(+) create mode 100644 src/Logger/Handler/Entry.php create mode 100644 src/Logger/Handler/Handler.php create mode 100644 src/Logger/Handler/SimpleEntry.php create mode 100644 src/Logger/HandlerAwareLogger.php create mode 100644 tests/Unit/Logger/Handler/SimpleEntryTest.php create mode 100644 tests/Unit/Logger/HandlerAwareLoggerTest.php diff --git a/src/Logger/Handler/Entry.php b/src/Logger/Handler/Entry.php new file mode 100644 index 00000000..9ea4c1cd --- /dev/null +++ b/src/Logger/Handler/Entry.php @@ -0,0 +1,37 @@ +created_at = $created_at; + $this->level = $level; + $this->message = $message; + $this->context = $context; + } + + /** + * Returns the message + * + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * Returns the context + * + * @return array + */ + public function getContext() + { + return $this->context; + } + + /** + * Returns the level + * + * @return string + */ + public function getLevel() + { + return $this->level; + } + + /** + * Returns the created DateTime + * + * @return DateTimeImmutable + */ + public function getCreatedAt() + { + return $this->created_at; + } +} diff --git a/src/Logger/HandlerAwareLogger.php b/src/Logger/HandlerAwareLogger.php new file mode 100644 index 00000000..c2b10c05 --- /dev/null +++ b/src/Logger/HandlerAwareLogger.php @@ -0,0 +1,199 @@ +addHandler($handler); + } + + /** + * System is unusable. + * + * @param string $message + * @param array $context + * @return null + */ + public function emergency($message, array $context = array()) + { + $this->log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * @return null + */ + public function alert($message, array $context = array()) + { + return $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * @return null + */ + public function critical($message, array $context = array()) + { + return $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * @return null + */ + public function error($message, array $context = array()) + { + return $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * @return null + */ + public function warning($message, array $context = array()) + { + return $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * @return null + */ + public function notice($message, array $context = array()) + { + return $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * @return null + */ + public function info($message, array $context = array()) + { + return $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * @return null + */ + public function debug($message, array $context = array()) + { + return $this->log(LogLevel::DEBUG, $message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * @return null + */ + public function log($level, $message, array $context = array()) + { + $entry = $this->createEntry( + new DateTimeImmutable('now'), + $level, + $message, + $context + ); + + $this->handleEntry($entry); + } + + /** + * Adds a handler + * + * @param YoutubeDownloader\Logger\Handler\Handler $handler + * @return void + */ + private function addHandler(Handler $handler) + { + $this->handlers[] = $handler; + } + + /** + * Factory for a new entry + * + * @param DateTimeImmutable $created_at + * @param mixed $level + * @param string $message + * @param array $context + * @return YoutubeDownloader\Logger\Handler\Entry + */ + private function createEntry(DateTimeImmutable $created_at, $level, $message, array $context = array()) + { + return new SimpleEntry($created_at, $level, $message, $context); + } + + /** + * Search for all handler that handles this entry and call them + * + * @param YoutubeDownloader\Logger\Handler\Entry $entry + * @return void + */ + private function handleEntry(Entry $entry) + { + foreach ($this->handlers as $handler) + { + if ($handler->handles($entry->getLevel())) + { + $handler->handle($entry); + } + } + } +} diff --git a/tests/Unit/Logger/Handler/SimpleEntryTest.php b/tests/Unit/Logger/Handler/SimpleEntryTest.php new file mode 100644 index 00000000..ab770ada --- /dev/null +++ b/tests/Unit/Logger/Handler/SimpleEntryTest.php @@ -0,0 +1,79 @@ +entry = new SimpleEntry( + new \DateTimeImmutable('now'), + 'debug', + 'Log of {description}', + ['description' => 'a debug message'] + ); + } + + /** + * @test SimpleEntry implements Entry + */ + public function implementsEntry() + { + $this->assertInstanceOf( + '\\YoutubeDownloader\\Logger\\Handler\\Entry', + $this->entry + ); + } + + /** + * @test getMessage + */ + public function getMessage() + { + $this->assertSame( + 'Log of {description}', + $this->entry->getMessage() + ); + } + + /** + * @test getContext + */ + public function getContext() + { + $this->assertSame( + ['description' => 'a debug message'], + $this->entry->getContext() + ); + } + + /** + * @test getLevel + */ + public function getLevel() + { + $this->assertSame( + 'debug', + $this->entry->getLevel() + ); + } + + /** + * @test getCreatedAt + */ + public function getCreatedAt() + { + $this->assertInstanceOf( + 'DateTimeImmutable', + $this->entry->getCreatedAt() + ); + } +} diff --git a/tests/Unit/Logger/HandlerAwareLoggerTest.php b/tests/Unit/Logger/HandlerAwareLoggerTest.php new file mode 100644 index 00000000..cca4363c --- /dev/null +++ b/tests/Unit/Logger/HandlerAwareLoggerTest.php @@ -0,0 +1,89 @@ +createMock( + '\\YoutubeDownloader\\Logger\\Handler\\Handler' + ); + $logger = new HandlerAwareLogger($handler); + + $this->assertInstanceOf('\\YoutubeDownloader\\Logger\\Logger', $logger); + } + + /** + * @test all logger methods + * + * @dataProvider LoggerMethodsDataProvider + */ + public function loggerMethods($method, $message, array $context) + { + $handler = $this->createMock( + '\\YoutubeDownloader\\Logger\\Handler\\Handler' + ); + $handler->method('handles')->with($method)->willReturn(true); + $handler->expects($this->once())->method('handle')->willReturn(null); + + $logger = new HandlerAwareLogger($handler); + + $this->assertNull($logger->$method($message, $context)); + } + + /** + * LoggerMethodsDataProvider + */ + public function LoggerMethodsDataProvider() + { + return [ + [ + 'emergency', + 'Log of {description}', + ['description' => 'an emergency'], + ], + [ + 'alert', + 'Log of {description}', + ['description' => 'an alert'], + ], + [ + 'critical', + 'Log of {description}', + ['description' => 'critical'], + ], + [ + 'error', + 'Log of {description}', + ['description' => 'an error'], + ], + [ + 'warning', + 'Log of {description}', + ['description' => 'a warning'], + ], + [ + 'notice', + 'Log of {description}', + ['description' => 'a notice'], + ], + [ + 'info', + 'Log of {description}', + ['description' => 'an info message'], + ], + [ + 'debug', + 'Log of {description}', + ['description' => 'a debug message'], + ], + ]; + } +} From 7ac581c635e1bc777a7674365375a4568eafe056 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 22 Aug 2017 16:25:40 +0200 Subject: [PATCH 3/8] Create StremHandler to save logs in a stream --- src/Logger/Handler/StreamHandler.php | 146 ++++++++++++++++++ .../Unit/Logger/Handler/StreamHandlerTest.php | 88 +++++++++++ 2 files changed, 234 insertions(+) create mode 100644 src/Logger/Handler/StreamHandler.php create mode 100644 tests/Unit/Logger/Handler/StreamHandlerTest.php diff --git a/src/Logger/Handler/StreamHandler.php b/src/Logger/Handler/StreamHandler.php new file mode 100644 index 00000000..5cf4db83 --- /dev/null +++ b/src/Logger/Handler/StreamHandler.php @@ -0,0 +1,146 @@ + true, + 'w+' => true, + 'rw' => true, + 'r+' => true, + 'x+' => true, + 'c+' => true, + 'wb' => true, + 'w+b' => true, + 'r+b' => true, + 'x+b' => true, + 'c+b' => true, + 'w+t' => true, + 'r+t' => true, + 'x+t' => true, + 'c+t' => true, + 'a' => true, + 'a+' => true, + ]; + + if ( ! array_key_exists($meta['mode'], $writable_modes) ) + { + throw new \Exception('The resource must be writable.'); + } + + $this->stream = $stream; + + foreach ($levels as $level) + { + $this->levels[] = strval($level); + } + } + + /** + * Check if this handler handels a log level + * + * @param string $level A valid log level from LogLevel class + * @return boolean + */ + public function handles($level) + { + return array_key_exists(strval($level), array_flip($this->levels)); + } + + /** + * Handle an entry + * + * @param Entry $entry + * @return boolean + */ + public function handle(Entry $entry) + { + if ( ! $this->handles($entry->getLevel()) ) + { + return false; + } + + fwrite($this->stream, $this->formatEntry($entry)); + + return true; + } + + /** + * Format an entry to a single line + * + * @param Entry $entry + * @return string + */ + private function formatEntry(Entry $entry) + { + $message = $this->interpolate($entry->getMessage(), $entry->getContext()); + + $replace = [ + '%datetime%' => $entry->getCreatedAt()->format('Y-m-d\TH:i:sP'), + '%level_name%' => $entry->getLevel(), + '%message%' => $this->removeLinebreaks($message), + ]; + + return strtr($this->template, $replace); + } + + /** + * Interpolates context values into the message placeholders. + * + * @see http://www.php-fig.org/psr/psr-3/ + * + * @param string $message + * @param array $context + * @return string + */ + private function interpolate($message, array $context = []) + { + // build a replacement array with braces around the context keys + $replace = []; + + foreach ($context as $key => $val) + { + // check that the value can be casted to string + if ( ! is_array($val) && ( ! is_object($val) || method_exists($val, '__toString') ) ) + { + $replace['{' . $key . '}'] = strval($val); + } + } + + // interpolate replacement values into the message and return + return strtr($message, $replace); + } + + /** + * Remove all linebreaks in a string + * + * @param string $message + * @return string + */ + private function removeLinebreaks($message) + { + return str_replace(["\r\n", "\r", "\n"], ' ', $message); + } +} diff --git a/tests/Unit/Logger/Handler/StreamHandlerTest.php b/tests/Unit/Logger/Handler/StreamHandlerTest.php new file mode 100644 index 00000000..1641627f --- /dev/null +++ b/tests/Unit/Logger/Handler/StreamHandlerTest.php @@ -0,0 +1,88 @@ +at($root); + + $stream = fopen('vfs://logs/test.log', 'a+'); + + $handler = new StreamHandler($stream, []); + + $this->assertInstanceOf( + '\\YoutubeDownloader\\Logger\\Handler\\Handler', + $handler + ); + } + + /** + * @test Exception if stream is not writable + */ + public function expectExceptionWithoutResource() + { + $stream = new \stdClass; + + $this->expectException('\\Exception'); + $this->expectExceptionMessage('Parameter 1 must be a resource'); + + $handler = new StreamHandler($stream, []); + } + + /** + * @test Exception if stream is not writable + */ + public function expectExceptionWithNotWritableStream() + { + $root = vfsStream::setup('logs'); + vfsStream::newFile('test.log', 0600)->at($root); + + $stream = fopen('vfs://logs/test.log', 'r'); + + $this->expectException('\\Exception'); + $this->expectExceptionMessage('The resource must be writable.'); + + $handler = new StreamHandler($stream, []); + } + + /** + * @test save entry into stream + */ + public function handleEntry() + { + $root = vfsStream::setup('logs'); + vfsStream::newFile('test.log', 0600)->at($root); + + $stream = fopen('vfs://logs/test.log', 'a+'); + + $handler = new StreamHandler($stream, ['debug']); + + $entry = $this->createMock('\\YoutubeDownloader\\Logger\\Handler\\Entry'); + $entry->method('getMessage')->willReturn('Log with {message}.'); + $entry->method('getContext')->willReturn(['message' => 'a debug message']); + $entry->method('getLevel')->willReturn('debug'); + $entry->method('getCreatedAt')->willReturn( + new \DateTimeImmutable('2017-08-22 16:20:40', new \DateTimeZone('UTC')) + ); + + $this->assertTrue($handler->handles('debug')); + + $handler->handle($entry); + + $this->assertSame( + "[2017-08-22T16:20:40+00:00] debug: Log with a debug message.\n", + $root->getChild('test.log')->getContent() + ); + } +} From a2f6e25a51fc17983f704f605b9bb9480a924ffa Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 22 Aug 2017 16:39:36 +0200 Subject: [PATCH 4/8] Use DateTime instead of DateTimeImmutable to be compatible with PHP 5.4 --- src/Logger/Handler/Entry.php | 2 +- src/Logger/Handler/SimpleEntry.php | 8 ++++---- src/Logger/HandlerAwareLogger.php | 8 ++++---- tests/Unit/Logger/Handler/SimpleEntryTest.php | 4 ++-- tests/Unit/Logger/Handler/StreamHandlerTest.php | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Logger/Handler/Entry.php b/src/Logger/Handler/Entry.php index 9ea4c1cd..f64ad5c3 100644 --- a/src/Logger/Handler/Entry.php +++ b/src/Logger/Handler/Entry.php @@ -31,7 +31,7 @@ public function getLevel(); /** * Returns the created DateTime * - * @return DateTimeImmutable + * @return DateTime */ public function getCreatedAt(); } diff --git a/src/Logger/Handler/SimpleEntry.php b/src/Logger/Handler/SimpleEntry.php index 57b152e8..7b890336 100644 --- a/src/Logger/Handler/SimpleEntry.php +++ b/src/Logger/Handler/SimpleEntry.php @@ -2,7 +2,7 @@ namespace YoutubeDownloader\Logger\Handler; -use DateTimeImmutable; +use DateTime; /** * An simple entry instance @@ -17,13 +17,13 @@ class SimpleEntry implements Entry /** * Create an entry * - * @param DateTimeImmutable $created_at + * @param DateTime $created_at * @param mixed $level * @param string $message * @param array $context * @return self */ - public function __construct(DateTimeImmutable $created_at, $level, $message, array $context = array()) + public function __construct(DateTime $created_at, $level, $message, array $context = array()) { $this->created_at = $created_at; $this->level = $level; @@ -64,7 +64,7 @@ public function getLevel() /** * Returns the created DateTime * - * @return DateTimeImmutable + * @return DateTime */ public function getCreatedAt() { diff --git a/src/Logger/HandlerAwareLogger.php b/src/Logger/HandlerAwareLogger.php index c2b10c05..a5270f49 100644 --- a/src/Logger/HandlerAwareLogger.php +++ b/src/Logger/HandlerAwareLogger.php @@ -2,7 +2,7 @@ namespace YoutubeDownloader\Logger; -use DateTimeImmutable; +use DateTime; use YoutubeDownloader\Logger\Handler\Entry; use YoutubeDownloader\Logger\Handler\Handler; use YoutubeDownloader\Logger\Handler\SimpleEntry; @@ -146,7 +146,7 @@ public function debug($message, array $context = array()) public function log($level, $message, array $context = array()) { $entry = $this->createEntry( - new DateTimeImmutable('now'), + new DateTime('now'), $level, $message, $context @@ -169,13 +169,13 @@ private function addHandler(Handler $handler) /** * Factory for a new entry * - * @param DateTimeImmutable $created_at + * @param DateTime $created_at * @param mixed $level * @param string $message * @param array $context * @return YoutubeDownloader\Logger\Handler\Entry */ - private function createEntry(DateTimeImmutable $created_at, $level, $message, array $context = array()) + private function createEntry(DateTime $created_at, $level, $message, array $context = array()) { return new SimpleEntry($created_at, $level, $message, $context); } diff --git a/tests/Unit/Logger/Handler/SimpleEntryTest.php b/tests/Unit/Logger/Handler/SimpleEntryTest.php index ab770ada..4249db3a 100644 --- a/tests/Unit/Logger/Handler/SimpleEntryTest.php +++ b/tests/Unit/Logger/Handler/SimpleEntryTest.php @@ -15,7 +15,7 @@ class SimpleEntryTest extends TestCase public function setUp() { $this->entry = new SimpleEntry( - new \DateTimeImmutable('now'), + new \DateTime('now'), 'debug', 'Log of {description}', ['description' => 'a debug message'] @@ -72,7 +72,7 @@ public function getLevel() public function getCreatedAt() { $this->assertInstanceOf( - 'DateTimeImmutable', + 'DateTime', $this->entry->getCreatedAt() ); } diff --git a/tests/Unit/Logger/Handler/StreamHandlerTest.php b/tests/Unit/Logger/Handler/StreamHandlerTest.php index 1641627f..019d72e5 100644 --- a/tests/Unit/Logger/Handler/StreamHandlerTest.php +++ b/tests/Unit/Logger/Handler/StreamHandlerTest.php @@ -73,7 +73,7 @@ public function handleEntry() $entry->method('getContext')->willReturn(['message' => 'a debug message']); $entry->method('getLevel')->willReturn('debug'); $entry->method('getCreatedAt')->willReturn( - new \DateTimeImmutable('2017-08-22 16:20:40', new \DateTimeZone('UTC')) + new \DateTime('2017-08-22 16:20:40', new \DateTimeZone('UTC')) ); $this->assertTrue($handler->handles('debug')); From a5172ecd18c019909888ca501a8299fe8523dc48 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 22 Aug 2017 17:09:16 +0200 Subject: [PATCH 5/8] Create new logs folder, App and Controller startup are logged for debug --- .gitignore | 1 + bootstrap.php | 34 ++++++++++++++++++++++++++ logs/.gitkeep | 0 src/Application/App.php | 2 ++ src/Application/ControllerAbstract.php | 5 ++++ 5 files changed, 42 insertions(+) create mode 100644 logs/.gitkeep diff --git a/.gitignore b/.gitignore index 2d737240..8661afe1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ composer.phar composer.lock config/custom.php Deciphers.log +logs/* vendor diff --git a/bootstrap.php b/bootstrap.php index c9818ed6..0a6ab2c4 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -81,6 +81,40 @@ function($custom = 'custom') $container->set('cache', $cache); + // Create Logger + $now = new \DateTime('now', new \DateTimeZone($config->get('default_timezone'))); + + $filepath = sprintf( + '%s' . \DIRECTORY_SEPARATOR . '%s', + __DIR__ . \DIRECTORY_SEPARATOR . 'logs', + $now->format('Y') + ); + + if ( ! file_exists($filepath) ) + { + mkdir($filepath); + } + + $stream = fopen( + $filepath . \DIRECTORY_SEPARATOR . $now->format('Y-m-d') . '.log', + 'a+' + ); + + $handler = new \YoutubeDownloader\Logger\Handler\StreamHandler($stream, [ + \YoutubeDownloader\Logger\LogLevel::EMERGENCY, + \YoutubeDownloader\Logger\LogLevel::ALERT, + \YoutubeDownloader\Logger\LogLevel::CRITICAL, + \YoutubeDownloader\Logger\LogLevel::ERROR, + \YoutubeDownloader\Logger\LogLevel::WARNING, + \YoutubeDownloader\Logger\LogLevel::NOTICE, + \YoutubeDownloader\Logger\LogLevel::INFO, + \YoutubeDownloader\Logger\LogLevel::DEBUG, + ]); + + $logger = new \YoutubeDownloader\Logger\HandlerAwareLogger($handler); + + $container->set('logger', $logger); + return $container; }, [getenv('CONFIG_ENV') ?: 'custom'] diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/Application/App.php b/src/Application/App.php index f84ec6ca..167fe68e 100644 --- a/src/Application/App.php +++ b/src/Application/App.php @@ -29,6 +29,8 @@ class App public function __construct(Container $container) { $this->container = $container; + + $this->getContainer()->get('logger')->debug('App started'); } /** diff --git a/src/Application/ControllerAbstract.php b/src/Application/ControllerAbstract.php index 18b02075..f47ea261 100644 --- a/src/Application/ControllerAbstract.php +++ b/src/Application/ControllerAbstract.php @@ -19,6 +19,11 @@ abstract class ControllerAbstract implements Controller public function __construct(App $app) { $this->app = $app; + + $this->get('logger')->debug( + '{controller_name} created', + ['controller_name' => get_class($this)] + ); } /** From 8df1b2a1684be1f5eddeaf3a4b0e4a9247327f5f Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 22 Aug 2017 21:47:25 +0200 Subject: [PATCH 6/8] Fix tests --- tests/Unit/Application/AppTest.php | 16 ++++++++++++---- tests/Unit/Application/ControllerFactoryTest.php | 6 ++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/Unit/Application/AppTest.php b/tests/Unit/Application/AppTest.php index d71f0488..bdf9b6a6 100644 --- a/tests/Unit/Application/AppTest.php +++ b/tests/Unit/Application/AppTest.php @@ -13,7 +13,10 @@ class AppTest extends TestCase */ public function getContainer() { + $logger = $this->createMock('\\YoutubeDownloader\\Logger\\Logger'); + $container = $this->createMock('\\YoutubeDownloader\\Container\\Container'); + $container->method('get')->with('logger')->willReturn($logger); $app = new App($container); @@ -25,7 +28,10 @@ public function getContainer() */ public function getVersion() { + $logger = $this->createMock('\\YoutubeDownloader\\Logger\\Logger'); + $container = $this->createMock('\\YoutubeDownloader\\Container\\Container'); + $container->method('get')->with('logger')->willReturn($logger); $app = new App($container); @@ -49,11 +55,13 @@ public function runWithRoute() ->method('make') ->willReturn($controller); + $logger = $this->createMock('\\YoutubeDownloader\\Logger\\Logger'); + $container = $this->createMock('\\YoutubeDownloader\\Container\\Container'); - $container->expects($this->once()) - ->method('get') - ->with('controller_factory') - ->willReturn($factory); + $container->method('get')->will($this->returnValueMap([ + ['controller_factory', $factory], + ['logger', $logger], + ])); $app = new App($container); diff --git a/tests/Unit/Application/ControllerFactoryTest.php b/tests/Unit/Application/ControllerFactoryTest.php index 6f4fe73c..8b6328de 100644 --- a/tests/Unit/Application/ControllerFactoryTest.php +++ b/tests/Unit/Application/ControllerFactoryTest.php @@ -12,7 +12,13 @@ class ControllerFactoryTest extends TestCase */ public function make() { + $logger = $this->createMock('\\YoutubeDownloader\\Logger\\Logger'); + + $container = $this->createMock('\\YoutubeDownloader\\Container\\Container'); + $container->method('get')->with('logger')->willReturn($logger); + $app = $this->createMock('\\YoutubeDownloader\\Application\\App'); + $app->method('getContainer')->willReturn($container); $factory = new ControllerFactory; From 5ffa5c32ec43b854d9779c763361db1e7f8598d8 Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 23 Aug 2017 00:08:19 +0200 Subject: [PATCH 7/8] Add logger support in SignaturDecipher, Format and VideoInfo --- .gitignore | 1 - bootstrap.php | 63 ++++++----- src/Application/App.php | 2 + src/Application/DownloadController.php | 5 + src/Application/ResultController.php | 5 + src/Format.php | 35 +++++- src/Logger/Handler/NullHandler.php | 31 ++++++ src/Logger/HandlerAwareLogger.php | 2 +- src/Logger/LoggerAwareTrait.php | 40 +++++++ src/SignatureDecipher.php | 146 +++++++++++++++++++------ src/VideoInfo.php | 15 ++- 11 files changed, 280 insertions(+), 65 deletions(-) create mode 100644 src/Logger/Handler/NullHandler.php create mode 100644 src/Logger/LoggerAwareTrait.php diff --git a/.gitignore b/.gitignore index 8661afe1..fe58c349 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,5 @@ cache/* composer.phar composer.lock config/custom.php -Deciphers.log logs/* vendor diff --git a/bootstrap.php b/bootstrap.php index 0a6ab2c4..6286f8b8 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -82,37 +82,48 @@ function($custom = 'custom') $container->set('cache', $cache); // Create Logger - $now = new \DateTime('now', new \DateTimeZone($config->get('default_timezone'))); - - $filepath = sprintf( - '%s' . \DIRECTORY_SEPARATOR . '%s', - __DIR__ . \DIRECTORY_SEPARATOR . 'logs', - $now->format('Y') + $logger = new \YoutubeDownloader\Logger\HandlerAwareLogger( + new \YoutubeDownloader\Logger\Handler\NullHandler() ); - if ( ! file_exists($filepath) ) + if ( $config->get('debug') === true ) { - mkdir($filepath); + # code... + $now = new \DateTime('now', new \DateTimeZone($config->get('default_timezone'))); + + $filepath = sprintf( + '%s' . \DIRECTORY_SEPARATOR . '%s', + __DIR__ . \DIRECTORY_SEPARATOR . 'logs', + $now->format('Y') + ); + + if ( ! file_exists($filepath) ) + { + mkdir($filepath); + } + + $stream = fopen( + $filepath . \DIRECTORY_SEPARATOR . $now->format('Y-m-d') . '.log', + 'a+' + ); + + if ( is_resource($stream) ) + { + $handler = new \YoutubeDownloader\Logger\Handler\StreamHandler($stream, [ + \YoutubeDownloader\Logger\LogLevel::EMERGENCY, + \YoutubeDownloader\Logger\LogLevel::ALERT, + \YoutubeDownloader\Logger\LogLevel::CRITICAL, + \YoutubeDownloader\Logger\LogLevel::ERROR, + \YoutubeDownloader\Logger\LogLevel::WARNING, + \YoutubeDownloader\Logger\LogLevel::NOTICE, + \YoutubeDownloader\Logger\LogLevel::INFO, + \YoutubeDownloader\Logger\LogLevel::DEBUG, + ]); + + $logger->addHandler($handler); + } } - $stream = fopen( - $filepath . \DIRECTORY_SEPARATOR . $now->format('Y-m-d') . '.log', - 'a+' - ); - - $handler = new \YoutubeDownloader\Logger\Handler\StreamHandler($stream, [ - \YoutubeDownloader\Logger\LogLevel::EMERGENCY, - \YoutubeDownloader\Logger\LogLevel::ALERT, - \YoutubeDownloader\Logger\LogLevel::CRITICAL, - \YoutubeDownloader\Logger\LogLevel::ERROR, - \YoutubeDownloader\Logger\LogLevel::WARNING, - \YoutubeDownloader\Logger\LogLevel::NOTICE, - \YoutubeDownloader\Logger\LogLevel::INFO, - \YoutubeDownloader\Logger\LogLevel::DEBUG, - ]); - - $logger = new \YoutubeDownloader\Logger\HandlerAwareLogger($handler); - $container->set('logger', $logger); return $container; diff --git a/src/Application/App.php b/src/Application/App.php index 167fe68e..a599aeeb 100644 --- a/src/Application/App.php +++ b/src/Application/App.php @@ -67,5 +67,7 @@ public function runWithRoute($route) $controller = $controller_factory->make($route, $this); $controller->execute(); + + $this->getContainer()->get('logger')->debug('Controller executed. App closed.'); } } diff --git a/src/Application/DownloadController.php b/src/Application/DownloadController.php index 24f0d65e..5449ee8d 100644 --- a/src/Application/DownloadController.php +++ b/src/Application/DownloadController.php @@ -69,6 +69,11 @@ public function execute() $video_info = \YoutubeDownloader\VideoInfo::createFromStringWithConfig($video_info_string, $config); $video_info->setCache($this->get('cache')); + if ( $video_info instanceOf \YoutubeDownloader\Logger\LoggerAware ) + { + $video_info->setLogger($this->get('logger')); + } + try { $mp3_info = $toolkit->getDownloadMP3($video_info, $config); diff --git a/src/Application/ResultController.php b/src/Application/ResultController.php index 63c86eab..543b7b01 100644 --- a/src/Application/ResultController.php +++ b/src/Application/ResultController.php @@ -63,6 +63,11 @@ public function execute() $video_info = \YoutubeDownloader\VideoInfo::createFromStringWithConfig($video_info_string, $config); $video_info->setCache($this->get('cache')); + if ( $video_info instanceOf \YoutubeDownloader\Logger\LoggerAware ) + { + $video_info->setLogger($this->get('logger')); + } + if ($video_info->getStatus() == 'fail') { $message = 'Error in video ID: ' . $video_info->getErrorReason(); diff --git a/src/Format.php b/src/Format.php index e289ea34..ae0b301e 100644 --- a/src/Format.php +++ b/src/Format.php @@ -2,11 +2,16 @@ namespace YoutubeDownloader; +use YoutubeDownloader\Logger\LoggerAware; +use YoutubeDownloader\Logger\LoggerAwareTrait; + /** * a video format */ -class Format +class Format implements LoggerAware { + use LoggerAwareTrait; + /** * Creates a Stream from array * @@ -37,6 +42,8 @@ public static function createFromArray( private $data = []; + private $data_parsed = false; + private $raw_data = []; /** @@ -74,8 +81,6 @@ private function __construct(VideoInfo $video_info, array $data, array $config) } $this->raw_data = $data; - - $this->parseUrl(); } /** @@ -85,6 +90,11 @@ private function __construct(VideoInfo $video_info, array $data, array $config) */ private function parseUrl() { + if ( $this->data_parsed === true ) + { + return; + } + parse_str(urldecode($this->data['url']), $url_info); if (isset($this->raw_data['bitrate'])) @@ -122,7 +132,8 @@ private function parseUrl() $sig = SignatureDecipher::decipherSignatureWithRawPlayerScript( $decipherScript, - $this->raw_data['s'] + $this->raw_data['s'], + $this->getLogger() ); if ( strpos($this->raw_data['url'], 'ratebypass=') === false ) @@ -141,6 +152,8 @@ private function parseUrl() $this->data['expires'] = isset($url_info['expire']) ? date("G:i:s T", $url_info['expire']) : ''; $this->data['ipbits'] = isset($url_info['ipbits']) ? $url_info['ipbits'] : ''; $this->data['ip'] = isset($url_info['ip']) ? $url_info['ip'] : ''; + + $this->data_parsed = true; } /** @@ -160,6 +173,8 @@ public function getVideoId() */ public function getUrl() { + $this->parseUrl(); + return $this->data['url']; } @@ -170,6 +185,8 @@ public function getUrl() */ public function getItag() { + $this->parseUrl(); + return $this->data['itag']; } @@ -180,6 +197,8 @@ public function getItag() */ public function getQuality() { + $this->parseUrl(); + return $this->data['quality']; } @@ -190,6 +209,8 @@ public function getQuality() */ public function getType() { + $this->parseUrl(); + return $this->data['type']; } @@ -200,6 +221,8 @@ public function getType() */ public function getExpires() { + $this->parseUrl(); + return $this->data['expires']; } @@ -210,6 +233,8 @@ public function getExpires() */ public function getIpbits() { + $this->parseUrl(); + return $this->data['ipbits']; } @@ -220,6 +245,8 @@ public function getIpbits() */ public function getIp() { + $this->parseUrl(); + return $this->data['ip']; } } diff --git a/src/Logger/Handler/NullHandler.php b/src/Logger/Handler/NullHandler.php new file mode 100644 index 00000000..908918e4 --- /dev/null +++ b/src/Logger/Handler/NullHandler.php @@ -0,0 +1,31 @@ +handlers[] = $handler; } diff --git a/src/Logger/LoggerAwareTrait.php b/src/Logger/LoggerAwareTrait.php new file mode 100644 index 00000000..1a5d722b --- /dev/null +++ b/src/Logger/LoggerAwareTrait.php @@ -0,0 +1,40 @@ +logger = $logger; + } + + /** + * Gets a logger instance + * + * @return Logger + */ + public function getLogger() + { + if ( $this->logger === null ) + { + $this->logger = new NullLogger; + } + + return $this->logger; + } +} diff --git a/src/SignatureDecipher.php b/src/SignatureDecipher.php index 6160e3d7..42c5641c 100644 --- a/src/SignatureDecipher.php +++ b/src/SignatureDecipher.php @@ -3,6 +3,9 @@ namespace YoutubeDownloader; +use YoutubeDownloader\Logger\Logger; +use YoutubeDownloader\Logger\NullLogger; + class SignatureDecipher { /** @@ -58,15 +61,28 @@ public static function downloadRawPlayerScript($playerURL) * * @param string $decipherScript * @param string $signature + * @param Logger $logger * @return string returns the decipherd signature */ - public static function decipherSignatureWithRawPlayerScript($decipherScript, $signature) + public static function decipherSignatureWithRawPlayerScript($decipherScript, $signature, Logger $logger = null) { - ob_start(); //For debugging - echo("==== Load player script and execute patterns from player script ====\n\n"); + // BC: Use NullLogger if no Logger was set + if ( $logger === null ) + { + $logger = new NullLogger; + } + + $logger->debug( + '{method}: Load player script and execute patterns from player script', + ['method' => __METHOD__] + ); if ( ! $decipherScript ) { + $logger->debug( + '{method}: No decipher script was provided. Abort.', + ['method' => __METHOD__] + ); return ''; } @@ -79,27 +95,46 @@ public static function decipherSignatureWithRawPlayerScript($decipherScript, $si $signatureFunction = ""; for ($i=$callCount-1; $i > 0; $i--){ $signatureCall[$i] = explode(');', $signatureCall[$i])[0]; - if(strpos($signatureCall[$i], '(')){ + + if(strpos($signatureCall[$i], '(')) + { $signatureFunction = explode('(', $signatureCall[$i])[0]; break; } - else if($i==0) die("\n==== Failed to get signature function ===="); + elseif($i==0) + { + die("\n==== Failed to get signature function ===="); + } } - echo('signatureFunction = '.$signatureFunction."\n"); + + $logger->debug( + '{method}: signatureFunction = {signatureFunction}', + ['method' => __METHOD__, 'signatureFunction' => $signatureFunction] + ); $decipherPatterns = explode($signatureFunction."=function(", $decipherScript)[1]; $decipherPatterns = explode('};', $decipherPatterns)[0]; - echo('decipherPatterns = '.$decipherPatterns."\n"); + + $logger->debug( + '{method}: decipherPatterns = {decipherPatterns}', + ['method' => __METHOD__, 'decipherPatterns' => $decipherPatterns] + ); $deciphers = explode("(a", $decipherPatterns); - for ($i=0; $i < count($deciphers); $i++) { + for ($i=0; $i < count($deciphers); $i++) + { $deciphers[$i] = explode('.', explode(';', $deciphers[$i])[1])[0]; - if(count(explode($deciphers[$i], $decipherPatterns))>=2){ + + if(count(explode($deciphers[$i], $decipherPatterns))>=2) + { // This object was most called, that's mean this is the deciphers $deciphers = $deciphers[$i]; break; } - else if($i==count($deciphers)-1) die("\n==== Failed to get deciphers function ===="); + else if($i==count($deciphers)-1) + { + die("\n==== Failed to get deciphers function ===="); + } } $deciphersObjectVar = $deciphers; @@ -107,11 +142,17 @@ public static function decipherSignatureWithRawPlayerScript($decipherScript, $si $decipher = str_replace(["\n", "\r"], "", $decipher); $decipher = explode('}};', $decipher)[0]; $decipher = explode("},", $decipher); - print_r($decipher); + + $logger->debug( + '{method}: decipher = {decipher}', + ['method' => __METHOD__, 'decipher' => print_r($decipher, true)] + ); // Convert deciphers to object $deciphers = []; - foreach ($decipher as &$function) { + + foreach ($decipher as &$function) + { $deciphers[explode(':function', $function)[0]] = explode('){', $function)[1]; } @@ -120,27 +161,49 @@ public static function decipherSignatureWithRawPlayerScript($decipherScript, $si $decipherPatterns = str_replace('(a,', '->(', $decipherPatterns); $decipherPatterns = explode(';', explode('){', $decipherPatterns)[1]); - $decipheredSignature = self::executeSignaturePattern($decipherPatterns, $deciphers, $signature); + $decipheredSignature = self::executeSignaturePattern( + $decipherPatterns, + $deciphers, + $signature, + $logger + ); // For debugging - echo("\n\n\n==== Result ====\n"); - echo("Signature : ".$signature."\n"); - echo("Deciphered : ".$decipheredSignature); + $logger->debug( + '{method}: Results:', + ['method' => __METHOD__] + ); + + $logger->debug( + '{method}: Signature = {signature}', + ['method' => __METHOD__, 'signature' => $signature] + ); + + $logger->debug( + '{method}: Deciphered = {decipheredSignature}', + ['method' => __METHOD__, 'decipheredSignature' => $decipheredSignature] + ); + //file_put_contents("Deciphers".rand(1, 100000).".log", ob_get_contents()); // If you need to debug all video - file_put_contents("Deciphers.log", ob_get_contents()); - ob_end_clean(); //Return signature return $decipheredSignature; } - private static function executeSignaturePattern($patterns, $deciphers, $signature) + private static function executeSignaturePattern($patterns, $deciphers, $signature, Logger $logger) { - echo("\n\n\n==== Retrieved deciphers ====\n\n"); - print_r($patterns); - print_r($deciphers); - - echo("\n\n\n==== Processing ====\n\n"); + $logger->debug( + '{method}: Patterns = {patterns}', + ['method' => __METHOD__, 'patterns' => print_r($patterns, true)] + ); + $logger->debug( + '{method}: Deciphers = {deciphers}', + ['method' => __METHOD__, 'deciphers' => print_r($deciphers, true)] + ); + $logger->debug( + '{method}: ==== Processing ====', + ['method' => __METHOD__] + ); // Execute every $patterns with $deciphers dictionary $processSignature = $signature; @@ -153,14 +216,23 @@ private static function executeSignaturePattern($patterns, $deciphers, $signatur if(strpos($patterns[$i], '.split("")')!==false) { $processSignature = str_split($processSignature); - echo("String splitted\n"); + $logger->debug( + '{method}: String splitted', + ['method' => __METHOD__] + ); } else if(strpos($patterns[$i], '.join("")')!==false) { $processSignature = implode('', $processSignature); - echo("String combined\n"); + $logger->debug( + '{method}: String combined', + ['method' => __METHOD__] + ); + } + else + { + die("\n==== Decipher dictionary was not found ===="); } - else die("\n==== Decipher dictionary was not found ===="); } else { @@ -174,21 +246,33 @@ private static function executeSignaturePattern($patterns, $deciphers, $signatur $execute = $deciphers[$executes[0]]; //Find matched command dictionary - echo("Executing $executes[0] -> $number"); + $logger->debug( + "{method}: Executing $executes[0] -> $number", + ['method' => __METHOD__] + ); switch($execute){ case "a.reverse()": $processSignature = array_reverse($processSignature); - echo(" (Reversing array)\n"); + $logger->debug( + '{method}: (Reversing array)', + ['method' => __METHOD__] + ); break; case "var c=a[0];a[0]=a[b%a.length];a[b]=c": $c = $processSignature[0]; $processSignature[0] = $processSignature[$number%count($processSignature)]; $processSignature[$number] = $c; - echo(" (Swapping array)\n"); + $logger->debug( + '{method}: (Swapping array)', + ['method' => __METHOD__] + ); break; case "a.splice(0,b)": $processSignature = array_slice($processSignature, $number); - echo(" (Removing array)\n"); + $logger->debug( + '{method}: Removing array', + ['method' => __METHOD__] + ); break; default: die("\n==== Decipher dictionary was not found ===="); diff --git a/src/VideoInfo.php b/src/VideoInfo.php index 4bc01963..0d985709 100644 --- a/src/VideoInfo.php +++ b/src/VideoInfo.php @@ -3,6 +3,8 @@ namespace YoutubeDownloader; use YoutubeDownloader\Cache\Cache; +use YoutubeDownloader\Logger\LoggerAware; +use YoutubeDownloader\Logger\LoggerAwareTrait; /** * VideoInfo @@ -128,8 +130,10 @@ * - 'reason', * - 'errordetail', */ -class VideoInfo +class VideoInfo implements LoggerAware { + use LoggerAwareTrait; + /** * Creates a VideoInfo from string * @@ -243,7 +247,14 @@ private function parseFormats(array $format_array, array $config) continue; } - $formats[] = Format::createFromArray($this, $format_info, $config); + $format = Format::createFromArray($this, $format_info, $config); + + if ( $format instanceOf LoggerAware ) + { + $format->setLogger($this->getLogger()); + } + + $formats[] = $format; } return $formats; From c5346c0b7e6623c7c86e7479e3bfdd0558dbccb4 Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 23 Aug 2017 00:25:02 +0200 Subject: [PATCH 8/8] Update CHANGELOG.md --- CHANGELOG.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65fc4497..eebbf9a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- new PSR-3 compatible logger implementation `Logger\Logger` to log all kind of events +- `SimpleContainer` has a new `logger` service with a `Logger\Logger` instance +- new folder `/logs` for log files +- `Format` implements `Logger\LoggerAware` interface +- `VideoInfo` implements `Logger\LoggerAware` interface +- `SignatureDecipher::decipherSignatureWithRawPlayerScript()` expects an optional logger as 3rd parameter + +### Changed + +- Logs are now stored in `/logs`, the file `Deciphers.log` can be deleted + ### Removed - **Breaking** method `SignatureDecipher::downloadPlayerScript()` was removed, use `SignatureDecipher::downloadRawPlayerScript()` instead @@ -16,7 +29,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added -- new PSR-16 compatible cache implemantation `Cache\FileCache` to store data in the filesystem +- new PSR-16 compatible cache implementation `Cache\FileCache` to store data in the filesystem - `SignatureDecipher::getPlayerInfoByVideoId()` to get the player ID and player url of a cipher video - `SignatureDecipher::downloadRawPlayerScript()` to download the raw player script of a cipher video - `SignatureDecipher::decipherSignatureWithRawPlayerScript()` to decipher a signature with a raw dicipher script