From b28a0561fe60ae0ae5824eae94d76f89607dd6f6 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 2 Aug 2012 10:39:34 -0500 Subject: [PATCH 01/16] [zendframework/zf2#2083] Better fix for issue, and CS cleanup - Revert to previous constructor behavior, but instead have doWrite() call getFirePhp() to lazy-load the bridge instance. - Do not import classes from a subnamespace of the current namespace - trailing whitespace --- src/Writer/FirePhp.php | 34 ++++++++++++++-------------- src/Writer/FirePhp/FirePhpBridge.php | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Writer/FirePhp.php b/src/Writer/FirePhp.php index 95f85c97..a96de9aa 100644 --- a/src/Writer/FirePhp.php +++ b/src/Writer/FirePhp.php @@ -13,8 +13,6 @@ use FirePHP as FirePHPService; use Zend\Log\Formatter\FirePhp as FirePhpFormatter; use Zend\Log\Logger; -use Zend\Log\Writer\FirePhp\FirePhpBridge; -use Zend\Log\Writer\FirePhp\FirePhpInterface; /** * @category Zend @@ -26,19 +24,19 @@ class FirePhp extends AbstractWriter /** * A FirePhpInterface instance that is used to log messages to. * - * @var FirePhpInterface + * @var FirePhp\FirePhpInterface */ protected $firephp; /** * Initializes a new instance of this class. * - * @param null|FirePhpInterface $instance An instance of FirePhpInterface + * @param null|FirePhp\FirePhpInterface $instance An instance of FirePhpInterface * that should be used for logging */ - public function __construct(FirePhpInterface $instance = null) + public function __construct(FirePhp\FirePhpInterface $instance = null) { - $this->firephp = $instance === null ? $this->getFirePhp() : $instance; + $this->firephp = $instance; $this->formatter = new FirePhpFormatter(); } @@ -50,7 +48,9 @@ public function __construct(FirePhpInterface $instance = null) */ protected function doWrite(array $event) { - if (!$this->firephp->getEnabled()) { + $firephp = $this->getFirePhp(); + + if (!$firephp->getEnabled()) { return; } @@ -61,20 +61,20 @@ protected function doWrite(array $event) case Logger::ALERT: case Logger::CRIT: case Logger::ERR: - $this->firephp->error($line); + $firephp->error($line); break; case Logger::WARN: - $this->firephp->warn($line); + $firephp->warn($line); break; case Logger::NOTICE: case Logger::INFO: - $this->firephp->info($line); + $firephp->info($line); break; case Logger::DEBUG: - $this->firephp->trace($line); + $firephp->trace($line); break; default: - $this->firephp->log($line); + $firephp->log($line); break; } } @@ -82,18 +82,18 @@ protected function doWrite(array $event) /** * Gets the FirePhpInterface instance that is used for logging. * - * @return FirePhpInterface + * @return FirePhp\FirePhpInterface */ public function getFirePhp() { // Remember: class names in strings are absolute; thus the class_exists // here references the canonical name for the FirePHP class - if (!$this->firephp instanceof FirePhpInterface + if (!$this->firephp instanceof FirePhp\FirePhpInterface && class_exists('FirePHP') ) { // FirePHPService is an alias for FirePHP; otherwise the class // names would clash in this file on this line. - $this->setFirePhp(new FirePhpBridge(new FirePHPService())); + $this->setFirePhp(new FirePhp\FirePhpBridge(new FirePHPService())); } return $this->firephp; } @@ -101,10 +101,10 @@ public function getFirePhp() /** * Sets the FirePhpInterface instance that is used for logging. * - * @param FirePhpInterface $instance A FirePhpInterface instance to set. + * @param FirePhp\FirePhpInterface $instance A FirePhpInterface instance to set. * @return FirePhp */ - public function setFirePhp(FirePhpInterface $instance) + public function setFirePhp(FirePhp\FirePhpInterface $instance) { $this->firephp = $instance; return $this; diff --git a/src/Writer/FirePhp/FirePhpBridge.php b/src/Writer/FirePhp/FirePhpBridge.php index 4f5137b1..806c81d3 100644 --- a/src/Writer/FirePhp/FirePhpBridge.php +++ b/src/Writer/FirePhp/FirePhpBridge.php @@ -21,7 +21,7 @@ class FirePhpBridge implements FirePhpInterface { /** * FirePHP instance - * + * * @var FirePHP */ protected $firephp; From 3a454e36d59a4133c58e50615a27347eed2d180c Mon Sep 17 00:00:00 2001 From: Michael Kliewe Date: Sat, 11 Aug 2012 11:27:30 +0200 Subject: [PATCH 02/16] removed all "@return void" in constructors --- src/Writer/MongoDB.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Writer/MongoDB.php b/src/Writer/MongoDB.php index f7b4f8f6..f9cae3dc 100644 --- a/src/Writer/MongoDB.php +++ b/src/Writer/MongoDB.php @@ -50,7 +50,6 @@ class MongoDB extends AbstractWriter * @param string|MongoDB $database * @param string $collection * @param array $saveOptions - * @return void */ public function __construct($mongo, $database, $collection, array $saveOptions = array()) { From 17b56b84d1bd2ec83c8548493bbc6e4da5cda022 Mon Sep 17 00:00:00 2001 From: Ralph Schindler Date: Thu, 16 Aug 2012 12:45:41 -0500 Subject: [PATCH 03/16] Zend\Log: ZF2-453 test for xml extra array prossing bug --- src/Formatter/Xml.php | 2 +- test/Formatter/XmlTest.php | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Formatter/Xml.php b/src/Formatter/Xml.php index 1e519c00..64bb6da2 100644 --- a/src/Formatter/Xml.php +++ b/src/Formatter/Xml.php @@ -153,7 +153,7 @@ public function format($event) ) { if ($key == "message") { $value = htmlspecialchars($value, ENT_COMPAT, $enc); - } elseif($key == "extra" && empty($value)) { + } elseif ($key == "extra" && empty($value)) { continue; } $elt->appendChild(new DOMElement($key, (string)$value)); diff --git a/test/Formatter/XmlTest.php b/test/Formatter/XmlTest.php index dc58a41c..660aeaee 100644 --- a/test/Formatter/XmlTest.php +++ b/test/Formatter/XmlTest.php @@ -178,4 +178,23 @@ public function testObjectsWithStringSerializationAreIncludedInFormattedString() $output = $formatter->format($event); $this->assertContains($expected, $output); } + + /** + * @group ZF2-453 + */ + public function testFormatWillRemoveExtraEmptyArrayFromEvent() + { + $formatter = new XmlFormatter; + $d = new DateTime('2001-01-01T12:00:00-06:00'); + $event = array( + 'timestamp' => $d, + 'message' => 'test', + 'priority' => 1, + 'priorityName' => 'CRIT', + 'extra' => array() + ); + $expected = '2001-01-01T12:00:00-06:00test1CRIT'; + $expected .= PHP_EOL . PHP_EOL; + $this->assertEquals($expected, $formatter->format($event)); + } } From c1a43ce854d8bc954a6db983e2bb00e858e27cea Mon Sep 17 00:00:00 2001 From: tr Date: Mon, 20 Aug 2012 18:56:43 +0100 Subject: [PATCH 04/16] making table name optional in logger\writer\db constructor, but checking that both table name and db have been sent in constructor and throwing exceptions if not --- src/Logger.php | 1 - src/Writer/Db.php | 10 +++++++++- test/Writer/DbTest.php | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Logger.php b/src/Logger.php index 259018f1..e6b54814 100644 --- a/src/Logger.php +++ b/src/Logger.php @@ -172,7 +172,6 @@ public function addWriter($writer, $priority = 1, array $options = null) is_object($writer) ? get_class($writer) : gettype($writer) )); } - $this->writers->insert($writer, $priority); return $this; diff --git a/src/Writer/Db.php b/src/Writer/Db.php index 99e141b0..207df2ee 100644 --- a/src/Writer/Db.php +++ b/src/Writer/Db.php @@ -62,7 +62,7 @@ class Db extends AbstractWriter * @return Db * @throw Exception\InvalidArgumentException */ - public function __construct($db, $tableName, array $columnMap = null, $separator = null) + public function __construct($db, $tableName = null, array $columnMap = null, $separator = null) { if ($db instanceof Traversable) { $db = iterator_to_array($db); @@ -75,6 +75,14 @@ public function __construct($db, $tableName, array $columnMap = null, $separator $db = isset($db['db']) ? $db['db'] : null; } + if (null === $db){ + throw new Exception\InvalidArgumentException('You must specify a database adapter either explicitly in the constructor or in options array with key "db"'); + } + + if (null === $tableName){ + throw new Exception\InvalidArgumentException('You must specify a table name. Either directly in the constructor, or via options'); + } + if (!$db instanceof Adapter) { throw new Exception\InvalidArgumentException('You must pass a valid Zend\Db\Adapter\Adapter'); } diff --git a/test/Writer/DbTest.php b/test/Writer/DbTest.php index 789df5e5..6bc43513 100644 --- a/test/Writer/DbTest.php +++ b/test/Writer/DbTest.php @@ -38,6 +38,24 @@ public function testFormattingIsNotSupported() $this->writer->setFormatter(new SimpleFormatter); } + public function testNotPassingTableNameToConstructorThrowsException(){ + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'You must specify a table name. Either directly in the constructor, or via options'); + new DbWriter($this->db); + } + + public function testNotPassingDbToConstructorThrowsException(){ + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'You must specify a database adapter either explicitly in the constructor or in options array with key "db"'); + new DbWriter(array()); + } + + public function testPassingTableNameAsArgIsOK(){ + $options = array( + 'db' => $this->db, + 'table' => $this->tableName, + ); + new DbWriter ($options); + } + public function testWriteWithDefaults() { // log to the mock db adapter From 492076c1abb7e3d1af657f253f5f808fa71c234c Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Aug 2012 10:35:40 -0500 Subject: [PATCH 05/16] [zendframework/zf2#2209] CS review - Incorporate feedback from issue comments - Ensure CS is followed - Added assertions to testPassingTableNameAsArgIsOK() - Simplified messages in expected exception matching in new tests --- src/Writer/Db.php | 11 ++++------- test/Writer/DbTest.php | 23 ++++++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/Writer/Db.php b/src/Writer/Db.php index 207df2ee..7f0eba9e 100644 --- a/src/Writer/Db.php +++ b/src/Writer/Db.php @@ -75,18 +75,15 @@ public function __construct($db, $tableName = null, array $columnMap = null, $se $db = isset($db['db']) ? $db['db'] : null; } - if (null === $db){ - throw new Exception\InvalidArgumentException('You must specify a database adapter either explicitly in the constructor or in options array with key "db"'); + if (!$db instanceof Adapter) { + throw new Exception\InvalidArgumentException('You must pass a valid Zend\Db\Adapter\Adapter'); } - if (null === $tableName){ + $tableName = (string) $tableName; + if ('' === $tableName){ throw new Exception\InvalidArgumentException('You must specify a table name. Either directly in the constructor, or via options'); } - if (!$db instanceof Adapter) { - throw new Exception\InvalidArgumentException('You must pass a valid Zend\Db\Adapter\Adapter'); - } - $this->db = $db; $this->tableName = $tableName; $this->columnMap = $columnMap; diff --git a/test/Writer/DbTest.php b/test/Writer/DbTest.php index 6bc43513..f20717ce 100644 --- a/test/Writer/DbTest.php +++ b/test/Writer/DbTest.php @@ -38,22 +38,27 @@ public function testFormattingIsNotSupported() $this->writer->setFormatter(new SimpleFormatter); } - public function testNotPassingTableNameToConstructorThrowsException(){ - $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'You must specify a table name. Either directly in the constructor, or via options'); - new DbWriter($this->db); + public function testNotPassingTableNameToConstructorThrowsException() + { + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'table name'); + $writer = new DbWriter($this->db); } - public function testNotPassingDbToConstructorThrowsException(){ - $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'You must specify a database adapter either explicitly in the constructor or in options array with key "db"'); - new DbWriter(array()); + public function testNotPassingDbToConstructorThrowsException() + { + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'Adapter'); + $writer = new DbWriter(array()); } - public function testPassingTableNameAsArgIsOK(){ + public function testPassingTableNameAsArgIsOK() + { $options = array( - 'db' => $this->db, + 'db' => $this->db, 'table' => $this->tableName, ); - new DbWriter ($options); + $writer = new DbWriter($options); + $this->assertInstanceOf('Zend\Log\Writer\Db', $writer); + $this->assertAttributeEquals($this->tableName, 'tableName', $writer); } public function testWriteWithDefaults() From 5709f3ec57ae1482c0f317ede98d5938bb268fe3 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Aug 2012 11:13:05 -0500 Subject: [PATCH 06/16] [zendframework/zf2#2210] Pass ErrorHandler::stop() result as previous exception - Per @mark-mabe - Any place where an exception is throw immediately following an ErrorHandler::stop() call should pass the result of that call as the previous exception. --- src/Filter/Regex.php | 4 ++-- src/Writer/Stream.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Filter/Regex.php b/src/Filter/Regex.php index 5aa8e4dc..b699480d 100644 --- a/src/Filter/Regex.php +++ b/src/Filter/Regex.php @@ -45,12 +45,12 @@ public function __construct($regex) } ErrorHandler::start(E_WARNING); $result = preg_match($regex, ''); - ErrorHandler::stop(); + $error = ErrorHandler::stop(); if ($result === false) { throw new Exception\InvalidArgumentException(sprintf( 'Invalid regular expression "%s"', $regex - )); + ), 0, $error); } $this->regex = $regex; } diff --git a/src/Writer/Stream.php b/src/Writer/Stream.php index b7aa2a2e..c5db6d1a 100644 --- a/src/Writer/Stream.php +++ b/src/Writer/Stream.php @@ -82,13 +82,13 @@ public function __construct($streamOrUrl, $mode = null, $logSeparator = null) } else { ErrorHandler::start(); $this->stream = fopen($streamOrUrl, $mode, false); - ErrorHandler::stop(); + $error = ErrorHandler::stop(); if (!$this->stream) { throw new Exception\RuntimeException(sprintf( '"%s" cannot be opened with mode "%s"', $streamOrUrl, $mode - )); + ), 0, $error); } } @@ -112,9 +112,9 @@ protected function doWrite(array $event) ErrorHandler::start(E_WARNING); $result = fwrite($this->stream, $line); - ErrorHandler::stop(); + $error = ErrorHandler::stop(); if (false === $result) { - throw new Exception\RuntimeException("Unable to write to stream"); + throw new Exception\RuntimeException("Unable to write to stream", 0, $error); } } From 97863cc3a6b84658e54ada6fdbbdb03b700973c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Durand?= Date: Thu, 23 Aug 2012 19:54:39 +0200 Subject: [PATCH 07/16] Add tests for the new DbFormatter Each Formatter should be covered by unit tests. --- test/Formatter/DbTest.php | 73 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 test/Formatter/DbTest.php diff --git a/test/Formatter/DbTest.php b/test/Formatter/DbTest.php new file mode 100644 index 00000000..b1de2ee2 --- /dev/null +++ b/test/Formatter/DbTest.php @@ -0,0 +1,73 @@ +assertEquals(DbFormatter::DEFAULT_DATETIME_FORMAT, $formatter->getDateTimeFormat()); + } + + /** + * @dataProvider provideDateTimeFormats + */ + public function testSetDateTimeFormat($dateTimeFormat) + { + $formatter = new DbFormatter(); + $formatter->setDateTimeFormat($dateTimeFormat); + + $this->assertEquals($dateTimeFormat, $formatter->getDateTimeFormat()); + } + + /** + * @return array + */ + public function provideDateTimeFormats() + { + return array( + array('r'), + array('U'), + array(DateTime::RSS), + ); + } + + /** + * @dataProvider provideDateTimeFormats + */ + public function testAllowsSpecifyingDateTimeFormatAsConstructorArgument($dateTimeFormat) + { + $formatter = new DbFormatter($dateTimeFormat); + + $this->assertEquals($dateTimeFormat, $formatter->getDateTimeFormat()); + } + + public function testFormatDateTimeInEvent() + { + $datetime = new DateTime(); + $event = array('timestamp' => $datetime); + $formatter = new DbFormatter(); + + $format = DbFormatter::DEFAULT_DATETIME_FORMAT; + $this->assertContains($datetime->format($format), $formatter->format($event)); + } +} From 4176aeb5e7c06960ed6475899ce9207cd8f5b9e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Durand?= Date: Thu, 23 Aug 2012 20:36:11 +0200 Subject: [PATCH 08/16] Improve DbFormatter to support DateTime in extra --- src/Formatter/Db.php | 9 ++++++--- test/Writer/DbTest.php | 20 +++++++++++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Formatter/Db.php b/src/Formatter/Db.php index e33f1b02..6f8a45d2 100644 --- a/src/Formatter/Db.php +++ b/src/Formatter/Db.php @@ -49,9 +49,12 @@ public function __construct($dateTimeFormat = null) */ public function format($event) { - if (isset($event['timestamp']) && $event['timestamp'] instanceof DateTime) { - $event['timestamp'] = $event['timestamp']->format($this->getDateTimeFormat()); - } + $format = $this->getDateTimeFormat(); + array_walk_recursive($event, function (&$value) use ($format) { + if ($value instanceof DateTime) { + $value = $value->format($format); + } + }); return $event; } diff --git a/test/Writer/DbTest.php b/test/Writer/DbTest.php index 4d14a303..65d68230 100644 --- a/test/Writer/DbTest.php +++ b/test/Writer/DbTest.php @@ -207,7 +207,7 @@ public function testThrowStrictSetFormatter() $this->writer->setFormatter(new \StdClass()); } - public function testWriteDateTime() + public function testWriteDateTimeAsTimestamp() { $date = new DateTime(); $event = array('timestamp'=> $date); @@ -220,4 +220,22 @@ public function testWriteDateTime() 'timestamp' => $date->format(FormatterInterface::DEFAULT_DATETIME_FORMAT) )), $this->db->calls['execute'][0]); } + + public function testWriteDateTimeAsExtraValue() + { + $date = new DateTime(); + $event = array( + 'extra'=> array( + 'request_time' => $date + ) + ); + $this->writer->write($event); + + $this->assertContains('query', array_keys($this->db->calls)); + $this->assertEquals(1, count($this->db->calls['query'])); + + $this->assertEquals(array(array( + 'extra_request_time' => $date->format(FormatterInterface::DEFAULT_DATETIME_FORMAT) + )), $this->db->calls['execute'][0]); + } } From 1ab09b6b37d9f8c6f1c5b6efaf552307c7f26230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Durand?= Date: Sun, 26 Aug 2012 18:19:14 +0200 Subject: [PATCH 09/16] Fix BC for extra in Simple Formatter ZF2 key is 'extra', Simple Formatter uses the legacy (ZF1) key (= 'info'). It is not functional with Logger. The refactor introduces a base formatter in order to normalize in string value all non-scalar data in extra array. --- src/Formatter/Base.php | 106 ++++++++++++++++++++++++++++++++++ src/Formatter/Simple.php | 19 +----- test/Formatter/SimpleTest.php | 12 ++-- 3 files changed, 116 insertions(+), 21 deletions(-) create mode 100644 src/Formatter/Base.php diff --git a/src/Formatter/Base.php b/src/Formatter/Base.php new file mode 100644 index 00000000..e23d7561 --- /dev/null +++ b/src/Formatter/Base.php @@ -0,0 +1,106 @@ +dateTimeFormat = $dateTimeFormat; + } + } + + /** + * Formats data to be written by the writer. + * + * @param array $event event data + * @return array + */ + public function format($event) + { + foreach ($event as $key => $value) { + $event[$key] = $this->normalize($value); + } + + return $event; + } + + /** + * Normalize all non-scalar data types (except null) in a string value + * + * @param mixed $value + * @return mixed + */ + protected function normalize($value) + { + if (is_scalar($value)) { + return $value; + } + + if ($value instanceof DateTime) { + $value = $value->format($this->getDateTimeFormat()); + } elseif (is_array($value) || $value instanceof Traversable) { + foreach ($value as $key => $subvalue) { + $value[$key] = $this->normalize($subvalue); + } + $value = json_encode($value); + } elseif (is_object($value) && !method_exists($value,'__toString')) { + $value = sprintf('object(%s) %s', get_class($value), json_encode($value)); + } elseif (is_resource($value)) { + $value = sprintf('resource(%s)', get_resource_type($value)); + } elseif (!is_object($value)) { + $value = gettype($value); + } + + return (string) $value; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeFormat() + { + return $this->dateTimeFormat; + } + + /** + * {@inheritDoc} + */ + public function setDateTimeFormat($dateTimeFormat) + { + $this->dateTimeFormat = (string) $dateTimeFormat; + return $this; + } +} \ No newline at end of file diff --git a/src/Formatter/Simple.php b/src/Formatter/Simple.php index 761ed799..42ec6823 100644 --- a/src/Formatter/Simple.php +++ b/src/Formatter/Simple.php @@ -18,9 +18,9 @@ * @package Zend_Log * @subpackage Formatter */ -class Simple implements FormatterInterface +class Simple extends Base { - const DEFAULT_FORMAT = '%timestamp% %priorityName% (%priority%): %message% %info%'; + const DEFAULT_FORMAT = '%timestamp% %priorityName% (%priority%): %message% %extra%'; /** * Format specifier for log messages @@ -68,21 +68,8 @@ public function format($event) { $output = $this->format; - if (!isset($event['info'])) { - $event['info'] = ''; - } - - if (isset($event['timestamp']) && $event['timestamp'] instanceof DateTime) { - $event['timestamp'] = $event['timestamp']->format($this->getDateTimeFormat()); - } - + $event = parent::format($event); foreach ($event as $name => $value) { - if ((is_object($value) && !method_exists($value,'__toString')) - || is_array($value) - ) { - $value = gettype($value); - } - $output = str_replace("%$name%", $value, $output); } diff --git a/test/Formatter/SimpleTest.php b/test/Formatter/SimpleTest.php index 7d1d1c9c..e736fa78 100644 --- a/test/Formatter/SimpleTest.php +++ b/test/Formatter/SimpleTest.php @@ -37,7 +37,8 @@ public function testDefaultFormat() 'timestamp' => $date, 'message' => 'foo', 'priority' => 42, - 'priorityName' => 'bar' + 'priorityName' => 'bar', + 'extra' => array() ); $f = new Simple(); @@ -54,7 +55,8 @@ public function testComplexValues() $fields = array( 'timestamp' => new DateTime(), 'priority' => 42, - 'priorityName' => 'bar' + 'priorityName' => 'bar', + 'extra' => array() ); $f = new Simple(); @@ -77,12 +79,12 @@ public function testComplexValues() $fields['message'] = fopen('php://stdout', 'w'); $line = $f->format($fields); - $this->assertContains('Resource id ', $line); + $this->assertContains('resource(stream)', $line); fclose($fields['message']); $fields['message'] = range(1, 10); $line = $f->format($fields); - $this->assertContains('array', $line); + $this->assertContains('[1,2,3,4,5,6,7,8,9,10]', $line); $fields['message'] = new StringObject(); $line = $f->format($fields); @@ -139,7 +141,7 @@ public function testDefaultFormatShouldDisplayExtraInformations() 'message' => 'Application error', 'priority' => 2, 'priorityName' => 'CRIT', - 'info' => $exception, + 'extra' => array($exception), ); $formatter = new Simple(); From e93625db1bfee0794326fb1fd64d2dfa5a429d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Durand?= Date: Mon, 27 Aug 2012 20:18:12 +0200 Subject: [PATCH 10/16] Refactor test complex messages with a dataprovider --- test/Formatter/SimpleTest.php | 54 ++++++++++++++--------------------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/test/Formatter/SimpleTest.php b/test/Formatter/SimpleTest.php index e736fa78..6796977a 100644 --- a/test/Formatter/SimpleTest.php +++ b/test/Formatter/SimpleTest.php @@ -50,7 +50,10 @@ public function testDefaultFormat() $this->assertContains((string) $fields['priority'], $line); } - public function testComplexValues() + /** + * @dataProvider provideMessages + */ + public function testComplexMessages($message, $printExpected) { $fields = array( 'timestamp' => new DateTime(), @@ -59,40 +62,25 @@ public function testComplexValues() 'extra' => array() ); - $f = new Simple(); - - $fields['message'] = 'Foo'; - $line = $f->format($fields); - $this->assertContains($fields['message'], $line); - - $fields['message'] = 10; - $line = $f->format($fields); - $this->assertContains((string) $fields['message'], $line); - - $fields['message'] = 10.5; - $line = $f->format($fields); - $this->assertContains((string) $fields['message'], $line); - - $fields['message'] = true; - $line = $f->format($fields); - $this->assertContains('1', $line); - - $fields['message'] = fopen('php://stdout', 'w'); - $line = $f->format($fields); - $this->assertContains('resource(stream)', $line); - fclose($fields['message']); - - $fields['message'] = range(1, 10); - $line = $f->format($fields); - $this->assertContains('[1,2,3,4,5,6,7,8,9,10]', $line); + $formatter = new Simple(); - $fields['message'] = new StringObject(); - $line = $f->format($fields); - $this->assertContains($fields['message']->__toString(), $line); + $fields['message'] = $message; + $line = $formatter->format($fields); + $this->assertContains($printExpected, $line); + } - $fields['message'] = new stdClass(); - $line = $f->format($fields); - $this->assertContains('object', $line); + public function provideMessages() + { + return array( + array('Foo', 'Foo'), + array(10, '10'), + array(10.5, '10.5'), + array(true, '1'), + array(fopen('php://stdout', 'w'), 'resource(stream)'), + array(range(1, 10), '[1,2,3,4,5,6,7,8,9,10]'), + array(new StringObject(), 'Hello World'), + array(new stdClass(), 'object'), + ); } /** From 4c2635c3f999380f76a2a90ce70fe01f2ac6eee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Durand?= Date: Tue, 28 Aug 2012 19:49:33 +0200 Subject: [PATCH 11/16] Improve the coverage of tests for SimpleFormatter We can modify the format of the output with an argument in the constructor. --- test/Formatter/SimpleTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/Formatter/SimpleTest.php b/test/Formatter/SimpleTest.php index 6796977a..1c1cc000 100644 --- a/test/Formatter/SimpleTest.php +++ b/test/Formatter/SimpleTest.php @@ -137,4 +137,11 @@ public function testDefaultFormatShouldDisplayExtraInformations() $this->assertContains($message, $output); } + + public function testAllowsSpecifyingFormatAsConstructorArgument() + { + $format = '[%timestamp%] %message%'; + $formatter = new Simple($format); + $this->assertEquals($format, $formatter->format(array())); + } } From 55929e27552d07b0e33333a61ccd66312ba7ddce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Durand?= Date: Tue, 28 Aug 2012 19:52:27 +0200 Subject: [PATCH 12/16] Add unit tests for BaseFormatter We keep the type array for the extra of the event, because some writers may use extra as one dimension with a double key (like the DbWriter). I fixed the use case Iterators: the goal is to have the same JSON encoding as array. --- src/Formatter/Base.php | 12 +++- src/Formatter/Simple.php | 3 + test/Formatter/BaseTest.php | 113 ++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 test/Formatter/BaseTest.php diff --git a/src/Formatter/Base.php b/src/Formatter/Base.php index e23d7561..162b95ce 100644 --- a/src/Formatter/Base.php +++ b/src/Formatter/Base.php @@ -51,7 +51,12 @@ public function __construct($dateTimeFormat = null) public function format($event) { foreach ($event as $key => $value) { - $event[$key] = $this->normalize($value); + // Keep extra as an array + if ('extra' === $key) { + $event[$key] = self::format($value); + } else { + $event[$key] = $this->normalize($value); + } } return $event; @@ -65,13 +70,16 @@ public function format($event) */ protected function normalize($value) { - if (is_scalar($value)) { + if (is_scalar($value) || null === $value) { return $value; } if ($value instanceof DateTime) { $value = $value->format($this->getDateTimeFormat()); } elseif (is_array($value) || $value instanceof Traversable) { + if ($value instanceof Traversable) { + $value = iterator_to_array($value); + } foreach ($value as $key => $subvalue) { $value[$key] = $this->normalize($subvalue); } diff --git a/src/Formatter/Simple.php b/src/Formatter/Simple.php index 42ec6823..deba3acc 100644 --- a/src/Formatter/Simple.php +++ b/src/Formatter/Simple.php @@ -69,6 +69,9 @@ public function format($event) $output = $this->format; $event = parent::format($event); + if (array_key_exists('extra', $event)) { + $event['extra'] = $this->normalize($event['extra']); + } foreach ($event as $name => $value) { $output = str_replace("%$name%", $value, $output); } diff --git a/test/Formatter/BaseTest.php b/test/Formatter/BaseTest.php new file mode 100644 index 00000000..d5d5f8a5 --- /dev/null +++ b/test/Formatter/BaseTest.php @@ -0,0 +1,113 @@ +assertEquals(BaseFormatter::DEFAULT_DATETIME_FORMAT, $formatter->getDateTimeFormat()); + } + + /** + * @dataProvider provideDateTimeFormats + */ + public function testAllowsSpecifyingDateTimeFormatAsConstructorArgument($dateTimeFormat) + { + $formatter = new BaseFormatter($dateTimeFormat); + + $this->assertEquals($dateTimeFormat, $formatter->getDateTimeFormat()); + } + + /** + * @return array + */ + public function provideDateTimeFormats() + { + return array( + array('r'), + array('U'), + array(DateTime::RSS), + ); + } + + /** + * @dataProvider provideDateTimeFormats + */ + public function testSetDateTimeFormat($dateTimeFormat) + { + $formatter = new BaseFormatter(); + $formatter->setDateTimeFormat($dateTimeFormat); + + $this->assertEquals($dateTimeFormat, $formatter->getDateTimeFormat()); + } + + public function testFormatAllTypes() + { + $datetime = new DateTime(); + $object = new stdClass(); + $object->foo = 'bar'; + $formatter = new BaseFormatter(); + + $event = array( + 'timestamp' => $datetime, + 'priority' => 1, + 'message' => 'tottakai', + 'extra' => array( + 'float' => 0.2, + 'boolean' => false, + 'array_empty' => array(), + 'array' => range(0, 4), + 'traversable_empty' => new EmptyIterator(), + 'traversable' => new ArrayIterator(array('id', 42)), + 'null' => null, + 'object_empty' => new stdClass(), + 'object' => $object, + 'string object' => new StringObject(), + 'resource' => fopen('php://stdout', 'w'), + ), + ); + $outputExpected = array( + 'timestamp' => $datetime->format($formatter->getDateTimeFormat()), + 'priority' => 1, + 'message' => 'tottakai', + 'extra' => array( + 'boolean' => false, + 'float' => 0.2, + 'array_empty' => '[]', + 'array' => '[0,1,2,3,4]', + 'traversable_empty' => '[]', + 'traversable' => '["id",42]', + 'null' => null, + 'object_empty' => 'object(stdClass) {}', + 'object' => 'object(stdClass) {"foo":"bar"}', + 'string object' => 'Hello World', + 'resource' => 'resource(stream)', + ), + ); + + $this->assertEquals($outputExpected, $formatter->format($event)); + } +} From 03349125111b44258d494344a493361b87245638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Durand?= Date: Tue, 28 Aug 2012 20:28:06 +0200 Subject: [PATCH 13/16] Clean duplicate code in SimpleFormatter --- src/Formatter/Simple.php | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/src/Formatter/Simple.php b/src/Formatter/Simple.php index deba3acc..dfd0540f 100644 --- a/src/Formatter/Simple.php +++ b/src/Formatter/Simple.php @@ -29,14 +29,6 @@ class Simple extends Base */ protected $format; - /** - * Format specifier for DateTime objects in event data (default: ISO 8601) - * - * @see http://php.net/manual/en/function.date.php - * @var string - */ - protected $dateTimeFormat = self::DEFAULT_DATETIME_FORMAT; - /** * Class constructor * @@ -53,9 +45,7 @@ public function __construct($format = null, $dateTimeFormat = null) $this->format = isset($format) ? $format : static::DEFAULT_FORMAT; - if (isset($dateTimeFormat)) { - $this->dateTimeFormat = $dateTimeFormat; - } + parent::__construct($dateTimeFormat); } /** @@ -78,21 +68,4 @@ public function format($event) return $output; } - - /** - * {@inheritDoc} - */ - public function getDateTimeFormat() - { - return $this->dateTimeFormat; - } - - /** - * {@inheritDoc} - */ - public function setDateTimeFormat($dateTimeFormat) - { - $this->dateTimeFormat = (string) $dateTimeFormat; - return $this; - } } From d14be1a502c5331924e28883fb536ab94f448d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Durand?= Date: Tue, 28 Aug 2012 21:14:07 +0200 Subject: [PATCH 14/16] Fix issue for SimpleFormatter when extra is empty We have the same issue as the XmlFormatter in ZF2-453. We don't know to write an empty array when the extra is not used by an event. --- src/Formatter/Simple.php | 14 +++++++++++--- test/Formatter/SimpleTest.php | 11 ++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Formatter/Simple.php b/src/Formatter/Simple.php index dfd0540f..09e92768 100644 --- a/src/Formatter/Simple.php +++ b/src/Formatter/Simple.php @@ -59,13 +59,21 @@ public function format($event) $output = $this->format; $event = parent::format($event); - if (array_key_exists('extra', $event)) { - $event['extra'] = $this->normalize($event['extra']); - } foreach ($event as $name => $value) { + if ('extra' == $name && count($value)) { + $value = $this->normalize($value); + } elseif ('extra' == $name) { + // Don't print an empty array + $value = ''; + } $output = str_replace("%$name%", $value, $output); } + if (isset($event['extra']) && empty($event['extra']) + && false !== strpos($this->format, '%extra%') + ) { + $output = rtrim($output, ' '); + } return $output; } } diff --git a/test/Formatter/SimpleTest.php b/test/Formatter/SimpleTest.php index 1c1cc000..a98f220e 100644 --- a/test/Formatter/SimpleTest.php +++ b/test/Formatter/SimpleTest.php @@ -32,7 +32,7 @@ public function testConstructorThrowsOnBadFormatString() public function testDefaultFormat() { - $date = new DateTime(); + $date = new DateTime('2012-08-28T18:15:00Z'); $fields = array( 'timestamp' => $date, 'message' => 'foo', @@ -41,13 +41,10 @@ public function testDefaultFormat() 'extra' => array() ); - $f = new Simple(); - $line = $f->format($fields); + $outputExpected = '2012-08-28T18:15:00+00:00 bar (42): foo'; + $formatter = new Simple(); - $this->assertContains($date->format('c'), $line, 'Default date format is ISO 8601'); - $this->assertContains($fields['message'], $line); - $this->assertContains($fields['priorityName'], $line); - $this->assertContains((string) $fields['priority'], $line); + $this->assertEquals($outputExpected, $formatter->format($fields)); } /** From f8189a2b5978858054be634f58db93f8cdfe493e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Durand?= Date: Tue, 28 Aug 2012 21:18:28 +0200 Subject: [PATCH 15/16] Clean up duplicate tests in Log\SimpleFormatter All variable types are already tested since SimpleFormatter extends BaseFormatter. --- test/Formatter/SimpleTest.php | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/test/Formatter/SimpleTest.php b/test/Formatter/SimpleTest.php index a98f220e..18fe0f62 100644 --- a/test/Formatter/SimpleTest.php +++ b/test/Formatter/SimpleTest.php @@ -47,39 +47,6 @@ public function testDefaultFormat() $this->assertEquals($outputExpected, $formatter->format($fields)); } - /** - * @dataProvider provideMessages - */ - public function testComplexMessages($message, $printExpected) - { - $fields = array( - 'timestamp' => new DateTime(), - 'priority' => 42, - 'priorityName' => 'bar', - 'extra' => array() - ); - - $formatter = new Simple(); - - $fields['message'] = $message; - $line = $formatter->format($fields); - $this->assertContains($printExpected, $line); - } - - public function provideMessages() - { - return array( - array('Foo', 'Foo'), - array(10, '10'), - array(10.5, '10.5'), - array(true, '1'), - array(fopen('php://stdout', 'w'), 'resource(stream)'), - array(range(1, 10), '[1,2,3,4,5,6,7,8,9,10]'), - array(new StringObject(), 'Hello World'), - array(new stdClass(), 'object'), - ); - } - /** * @dataProvider provideDateTimeFormats */ From f1bebf2eeac26407184163cb4e709fe41cc1a039 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 31 Aug 2012 14:44:59 -0500 Subject: [PATCH 16/16] [zendframework/zf2#2284][ZF2-507] Updated README - Notice about Date header --- .coveralls.yml | 3 + .gitattributes | 6 + .gitignore | 14 + .php_cs | 43 ++ .travis.yml | 35 ++ CONTRIBUTING.md | 229 +++++++++++ LICENSE.txt | 27 ++ README.md | 9 + composer.json | 50 +++ phpunit.xml.dist | 34 ++ phpunit.xml.travis | 34 ++ src/Exception/ExceptionInterface.php | 18 + src/Exception/InvalidArgumentException.php | 23 ++ src/Exception/RuntimeException.php | 23 ++ src/Filter/FilterInterface.php | 26 ++ src/Filter/Mock.php | 38 ++ src/Filter/Priority.php | 72 ++++ src/Filter/Regex.php | 72 ++++ src/Filter/SuppressFilter.php | 51 +++ src/Filter/Validator.php | 64 +++ src/Formatter/Base.php | 114 ++++++ src/Formatter/Db.php | 78 ++++ src/Formatter/ErrorHandler.php | 51 +++ src/Formatter/ExceptionHandler.php | 96 +++++ src/Formatter/FirePhp.php | 51 +++ src/Formatter/FormatterInterface.php | 49 +++ src/Formatter/Simple.php | 79 ++++ src/Formatter/Xml.php | 185 +++++++++ src/Logger.php | 451 +++++++++++++++++++++ src/LoggerAwareInterface.php | 24 ++ src/LoggerInterface.php | 74 ++++ src/Writer/AbstractWriter.php | 167 ++++++++ src/Writer/Db.php | 206 ++++++++++ src/Writer/FilterPluginManager.php | 67 +++ src/Writer/FirePhp.php | 112 +++++ src/Writer/FirePhp/FirePhpBridge.php | 113 ++++++ src/Writer/FirePhp/FirePhpInterface.php | 61 +++ src/Writer/Mail.php | 217 ++++++++++ src/Writer/Mock.php | 54 +++ src/Writer/MongoDB.php | 119 ++++++ src/Writer/Null.php | 28 ++ src/Writer/Stream.php | 154 +++++++ src/Writer/Syslog.php | 244 +++++++++++ src/Writer/WriterInterface.php | 52 +++ src/Writer/ZendMonitor.php | 106 +++++ src/WriterPluginManager.php | 67 +++ test/Filter/MockTest.php | 33 ++ test/Filter/PriorityTest.php | 49 +++ test/Filter/RegexTest.php | 37 ++ test/Filter/SuppressFilterTest.php | 57 +++ test/Filter/ValidatorTest.php | 45 ++ test/Formatter/BaseTest.php | 113 ++++++ test/Formatter/DbTest.php | 73 ++++ test/Formatter/ErrorHandlerTest.php | 53 +++ test/Formatter/ExceptionHandlerTest.php | 120 ++++++ test/Formatter/FirePhpTest.php | 54 +++ test/Formatter/SimpleTest.php | 111 +++++ test/Formatter/XmlTest.php | 200 +++++++++ test/LoggerTest.php | 262 ++++++++++++ test/TestAsset/ConcreteWriter.php | 20 + test/TestAsset/CustomSyslogWriter.php | 21 + test/TestAsset/MockDbAdapter.php | 38 ++ test/TestAsset/MockDbDriver.php | 21 + test/TestAsset/MockDbPlatform.php | 21 + test/TestAsset/MockFirePhp.php | 46 +++ test/TestAsset/SerializableObject.php | 19 + test/TestAsset/StringObject.php | 19 + test/Writer/AbstractTest.php | 72 ++++ test/Writer/DbTest.php | 241 +++++++++++ test/Writer/FirePhpTest.php | 91 +++++ test/Writer/MailTest.php | 87 ++++ test/Writer/MockTest.php | 33 ++ test/Writer/MongoDBTest.php | 102 +++++ test/Writer/NullTest.php | 29 ++ test/Writer/StreamTest.php | 150 +++++++ test/Writer/SyslogTest.php | 96 +++++ test/Writer/ZendMonitorTest.php | 40 ++ test/WriterPluginManagerTest.php | 44 ++ test/_files/layout.phtml | 1 + test/bootstrap.php | 34 ++ 80 files changed, 6322 insertions(+) create mode 100644 .coveralls.yml create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .php_cs create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 phpunit.xml.travis create mode 100644 src/Exception/ExceptionInterface.php create mode 100644 src/Exception/InvalidArgumentException.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/Filter/FilterInterface.php create mode 100644 src/Filter/Mock.php create mode 100644 src/Filter/Priority.php create mode 100644 src/Filter/Regex.php create mode 100644 src/Filter/SuppressFilter.php create mode 100644 src/Filter/Validator.php create mode 100644 src/Formatter/Base.php create mode 100644 src/Formatter/Db.php create mode 100644 src/Formatter/ErrorHandler.php create mode 100644 src/Formatter/ExceptionHandler.php create mode 100644 src/Formatter/FirePhp.php create mode 100644 src/Formatter/FormatterInterface.php create mode 100644 src/Formatter/Simple.php create mode 100644 src/Formatter/Xml.php create mode 100644 src/Logger.php create mode 100644 src/LoggerAwareInterface.php create mode 100644 src/LoggerInterface.php create mode 100644 src/Writer/AbstractWriter.php create mode 100644 src/Writer/Db.php create mode 100644 src/Writer/FilterPluginManager.php create mode 100644 src/Writer/FirePhp.php create mode 100644 src/Writer/FirePhp/FirePhpBridge.php create mode 100644 src/Writer/FirePhp/FirePhpInterface.php create mode 100644 src/Writer/Mail.php create mode 100644 src/Writer/Mock.php create mode 100644 src/Writer/MongoDB.php create mode 100644 src/Writer/Null.php create mode 100644 src/Writer/Stream.php create mode 100644 src/Writer/Syslog.php create mode 100644 src/Writer/WriterInterface.php create mode 100644 src/Writer/ZendMonitor.php create mode 100644 src/WriterPluginManager.php create mode 100644 test/Filter/MockTest.php create mode 100644 test/Filter/PriorityTest.php create mode 100644 test/Filter/RegexTest.php create mode 100644 test/Filter/SuppressFilterTest.php create mode 100644 test/Filter/ValidatorTest.php create mode 100644 test/Formatter/BaseTest.php create mode 100644 test/Formatter/DbTest.php create mode 100644 test/Formatter/ErrorHandlerTest.php create mode 100644 test/Formatter/ExceptionHandlerTest.php create mode 100644 test/Formatter/FirePhpTest.php create mode 100644 test/Formatter/SimpleTest.php create mode 100644 test/Formatter/XmlTest.php create mode 100644 test/LoggerTest.php create mode 100644 test/TestAsset/ConcreteWriter.php create mode 100644 test/TestAsset/CustomSyslogWriter.php create mode 100644 test/TestAsset/MockDbAdapter.php create mode 100644 test/TestAsset/MockDbDriver.php create mode 100644 test/TestAsset/MockDbPlatform.php create mode 100644 test/TestAsset/MockFirePhp.php create mode 100644 test/TestAsset/SerializableObject.php create mode 100644 test/TestAsset/StringObject.php create mode 100644 test/Writer/AbstractTest.php create mode 100644 test/Writer/DbTest.php create mode 100644 test/Writer/FirePhpTest.php create mode 100644 test/Writer/MailTest.php create mode 100644 test/Writer/MockTest.php create mode 100644 test/Writer/MongoDBTest.php create mode 100644 test/Writer/NullTest.php create mode 100644 test/Writer/StreamTest.php create mode 100644 test/Writer/SyslogTest.php create mode 100644 test/Writer/ZendMonitorTest.php create mode 100644 test/WriterPluginManagerTest.php create mode 100644 test/_files/layout.phtml create mode 100644 test/bootstrap.php diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 00000000..53bda829 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,3 @@ +coverage_clover: clover.xml +json_path: coveralls-upload.json +src_dir: src diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..85dc9a8c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/test export-ignore +/vendor export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +.php_cs export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4cac0a21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.buildpath +.DS_Store +.idea +.project +.settings/ +.*.sw* +.*.un~ +nbproject +tmp/ + +clover.xml +coveralls-upload.json +phpunit.xml +vendor diff --git a/.php_cs b/.php_cs new file mode 100644 index 00000000..bf4b799f --- /dev/null +++ b/.php_cs @@ -0,0 +1,43 @@ +notPath('TestAsset') + ->notPath('_files') + ->filter(function (SplFileInfo $file) { + if (strstr($file->getPath(), 'compatibility')) { + return false; + } + }); +$config = Symfony\CS\Config\Config::create(); +$config->level(null); +$config->fixers( + array( + 'braces', + 'duplicate_semicolon', + 'elseif', + 'empty_return', + 'encoding', + 'eof_ending', + 'function_call_space', + 'function_declaration', + 'indentation', + 'join_function', + 'line_after_namespace', + 'linefeed', + 'lowercase_keywords', + 'parenthesis', + 'multiple_use', + 'method_argument_space', + 'object_operator', + 'php_closing_tag', + 'psr0', + 'remove_lines_between_uses', + 'short_tag', + 'standardize_not_equal', + 'trailing_spaces', + 'unused_use', + 'visibility', + 'whitespacy_lines', + ) +); +$config->finder($finder); +return $config; diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..fe909ecb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +sudo: false + +language: php + +matrix: + fast_finish: true + include: + - php: 5.5 + - php: 5.6 + env: + - EXECUTE_TEST_COVERALLS=true + - EXECUTE_CS_CHECK=true + - php: 7 + - php: hhvm + allow_failures: + - php: 7 + - php: hhvm + +notifications: + irc: "irc.freenode.org#zftalk.dev" + email: false + +before_install: + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi + +install: + - composer install --no-interaction --prefer-source + +script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis --coverage-clover clover.xml ; fi + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis ; fi + - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run --config-file=.php_cs ; fi + +after_script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/coveralls ; fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..4af1fe27 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,229 @@ +# CONTRIBUTING + +## RESOURCES + +If you wish to contribute to Zend Framework, please be sure to +read/subscribe to the following resources: + + - [Coding Standards](https://github.com/zendframework/zf2/wiki/Coding-Standards) + - [Contributor's Guide](http://framework.zend.com/participate/contributor-guide) + - ZF Contributor's mailing list: + Archives: http://zend-framework-community.634137.n4.nabble.com/ZF-Contributor-f680267.html + Subscribe: zf-contributors-subscribe@lists.zend.com + - ZF Contributor's IRC channel: + #zftalk.dev on Freenode.net + +If you are working on new features or refactoring [create a proposal](https://github.com/zendframework/zend-log/issues/new). + +## Reporting Potential Security Issues + +If you have encountered a potential security vulnerability, please **DO NOT** report it on the public +issue tracker: send it to us at [zf-security@zend.com](mailto:zf-security@zend.com) instead. +We will work with you to verify the vulnerability and patch it as soon as possible. + +When reporting issues, please provide the following information: + +- Component(s) affected +- A description indicating how to reproduce the issue +- A summary of the security vulnerability and impact + +We request that you contact us via the email address above and give the project +contributors a chance to resolve the vulnerability and issue a new release prior +to any public exposure; this helps protect users and provides them with a chance +to upgrade and/or update in order to protect their applications. + +For sensitive email communications, please use [our PGP key](http://framework.zend.com/zf-security-pgp-key.asc). + +## RUNNING TESTS + +> ### Note: testing versions prior to 2.4 +> +> This component originates with Zend Framework 2. During the lifetime of ZF2, +> testing infrastructure migrated from PHPUnit 3 to PHPUnit 4. In most cases, no +> changes were necessary. However, due to the migration, tests may not run on +> versions < 2.4. As such, you may need to change the PHPUnit dependency if +> attempting a fix on such a version. + +To run tests: + +- Clone the repository: + + ```console + $ git clone git@github.com:zendframework/zend-log.git + $ cd + ``` + +- Install dependencies via composer: + + ```console + $ curl -sS https://getcomposer.org/installer | php -- + $ ./composer.phar install + ``` + + If you don't have `curl` installed, you can also download `composer.phar` from https://getcomposer.org/ + +- Run the tests via `phpunit` and the provided PHPUnit config, like in this example: + + ```console + $ ./vendor/bin/phpunit + ``` + +You can turn on conditional tests with the phpunit.xml file. +To do so: + + - Copy `phpunit.xml.dist` file to `phpunit.xml` + - Edit `phpunit.xml` to enable any specific functionality you + want to test, as well as to provide test values to utilize. + +## Running Coding Standards Checks + +This component uses [php-cs-fixer](http://cs.sensiolabs.org/) for coding +standards checks, and provides configuration for our selected checks. +`php-cs-fixer` is installed by default via Composer. + +To run checks only: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --dry-run --config-file=.php_cs +``` + +To have `php-cs-fixer` attempt to fix problems for you, omit the `--dry-run` +flag: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --config-file=.php_cs +``` + +If you allow php-cs-fixer to fix CS issues, please re-run the tests to ensure +they pass, and make sure you add and commit the changes after verification. + +## Recommended Workflow for Contributions + +Your first step is to establish a public repository from which we can +pull your work into the master repository. We recommend using +[GitHub](https://github.com), as that is where the component is already hosted. + +1. Setup a [GitHub account](http://github.com/), if you haven't yet +2. Fork the repository (http://github.com/zendframework/zend-log) +3. Clone the canonical repository locally and enter it. + + ```console + $ git clone git://github.com:zendframework/zend-log.git + $ cd zend-log + ``` + +4. Add a remote to your fork; substitute your GitHub username in the command + below. + + ```console + $ git remote add {username} git@github.com:{username}/zend-log.git + $ git fetch {username} + ``` + +### Keeping Up-to-Date + +Periodically, you should update your fork or personal repository to +match the canonical ZF repository. Assuming you have setup your local repository +per the instructions above, you can do the following: + + +```console +$ git checkout master +$ git fetch origin +$ git rebase origin/master +# OPTIONALLY, to keep your remote up-to-date - +$ git push {username} master:master +``` + +If you're tracking other branches -- for example, the "develop" branch, where +new feature development occurs -- you'll want to do the same operations for that +branch; simply substitute "develop" for "master". + +### Working on a patch + +We recommend you do each new feature or bugfix in a new branch. This simplifies +the task of code review as well as the task of merging your changes into the +canonical repository. + +A typical workflow will then consist of the following: + +1. Create a new local branch based off either your master or develop branch. +2. Switch to your new local branch. (This step can be combined with the + previous step with the use of `git checkout -b`.) +3. Do some work, commit, repeat as necessary. +4. Push the local branch to your remote repository. +5. Send a pull request. + +The mechanics of this process are actually quite trivial. Below, we will +create a branch for fixing an issue in the tracker. + +```console +$ git checkout -b hotfix/9295 +Switched to a new branch 'hotfix/9295' +``` + +... do some work ... + + +```console +$ git commit +``` + +... write your log message ... + + +```console +$ git push {username} hotfix/9295:hotfix/9295 +Counting objects: 38, done. +Delta compression using up to 2 threads. +Compression objects: 100% (18/18), done. +Writing objects: 100% (20/20), 8.19KiB, done. +Total 20 (delta 12), reused 0 (delta 0) +To ssh://git@github.com/{username}/zend-log.git + b5583aa..4f51698 HEAD -> master +``` + +To send a pull request, you have two options. + +If using GitHub, you can do the pull request from there. Navigate to +your repository, select the branch you just created, and then select the +"Pull Request" button in the upper right. Select the user/organization +"zendframework" as the recipient. + +If using your own repository - or even if using GitHub - you can use `git +format-patch` to create a patchset for us to apply; in fact, this is +**recommended** for security-related patches. If you use `format-patch`, please +send the patches as attachments to: + +- zf-devteam@zend.com for patches without security implications +- zf-security@zend.com for security patches + +#### What branch to issue the pull request against? + +Which branch should you issue a pull request against? + +- For fixes against the stable release, issue the pull request against the + "master" branch. +- For new features, or fixes that introduce new elements to the public API (such + as new public methods or properties), issue the pull request against the + "develop" branch. + +### Branch Cleanup + +As you might imagine, if you are a frequent contributor, you'll start to +get a ton of branches both locally and on your remote. + +Once you know that your changes have been accepted to the master +repository, we suggest doing some cleanup of these branches. + +- Local branch cleanup + + ```console + $ git branch -d + ``` + +- Remote branch removal + + ```console + $ git push {username} : + ``` diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..6eab5aa1 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2005-2015, Zend Technologies USA, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Zend Technologies USA, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..39c3537b --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# zend-log + +`Zend\Log` is a component for general purpose logging. It supports multiple log +backends, formatting messages sent to the log, and filtering messages from being +logged. + + +- File issues at https://github.com/zendframework/zend-log/issues +- Documentation is at http://framework.zend.com/manual/current/en/index.html#zend-log diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..3ffc5aaf --- /dev/null +++ b/composer.json @@ -0,0 +1,50 @@ +{ + "name": "zendframework/zend-log", + "description": "component for general purpose logging", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "log", + "logging" + ], + "homepage": "https://github.com/zendframework/zend-log", + "autoload": { + "psr-4": { + "Zend\\Log": "src/" + } + }, + "require": { + "php": ">=5.3.3", + "zendframework/zend-servicemanager": "self.version", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "zendframework/zend-console": "self.version", + "zendframework/zend-db": "self.version", + "zendframework/zend-escaper": "self.version", + "zendframework/zend-mail": "self.version", + "zendframework/zend-validator": "self.version", + "fabpot/php-cs-fixer": "1.7.*", + "satooshi/php-coveralls": "dev-master", + "phpunit/PHPUnit": "~4.0" + }, + "suggest": { + "ext-mongo": "*", + "zendframework/zend-console": "Zend\\Console component", + "zendframework/zend-db": "Zend\\Db component", + "zendframework/zend-escaper": "Zend\\Escaper component, for use in the XML formatter", + "zendframework/zend-mail": "Zend\\Mail component", + "zendframework/zend-validator": "Zend\\Validator component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.4-dev", + "dev-develop": "2.5-dev" + } + }, + "autoload-dev": { + "psr-4": { + "ZendTest\\Log\\": "test/" + } + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..21b3d498 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,34 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + diff --git a/phpunit.xml.travis b/phpunit.xml.travis new file mode 100644 index 00000000..21b3d498 --- /dev/null +++ b/phpunit.xml.travis @@ -0,0 +1,34 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php new file mode 100644 index 00000000..58042a69 --- /dev/null +++ b/src/Exception/ExceptionInterface.php @@ -0,0 +1,18 @@ +events[] = $event; + return true; + } +} diff --git a/src/Filter/Priority.php b/src/Filter/Priority.php new file mode 100644 index 00000000..8c717a85 --- /dev/null +++ b/src/Filter/Priority.php @@ -0,0 +1,72 @@ +priority = $priority; + $this->operator = $operator === null ? '<=' : $operator; + } + + /** + * Returns TRUE to accept the message, FALSE to block it. + * + * @param array $event event data + * @return boolean accepted? + */ + public function filter(array $event) + { + return version_compare($event['priority'], $this->priority, $this->operator); + } +} diff --git a/src/Filter/Regex.php b/src/Filter/Regex.php new file mode 100644 index 00000000..b699480d --- /dev/null +++ b/src/Filter/Regex.php @@ -0,0 +1,72 @@ +regex = $regex; + } + + /** + * Returns TRUE to accept the message, FALSE to block it. + * + * @param array $event event data + * @return boolean accepted? + */ + public function filter(array $event) + { + $message = $event['message']; + if (is_array($event['message'])) { + $message = var_export($message, TRUE); + } + return preg_match($this->regex, $message) > 0; + } +} diff --git a/src/Filter/SuppressFilter.php b/src/Filter/SuppressFilter.php new file mode 100644 index 00000000..be41d73b --- /dev/null +++ b/src/Filter/SuppressFilter.php @@ -0,0 +1,51 @@ +accept = ! (bool) $suppress; + } + + /** + * Returns TRUE to accept the message, FALSE to block it. + * + * @param array $event event data + * @return boolean accepted? + */ + public function filter(array $event) + { + return $this->accept; + } +} diff --git a/src/Filter/Validator.php b/src/Filter/Validator.php new file mode 100644 index 00000000..af140e85 --- /dev/null +++ b/src/Filter/Validator.php @@ -0,0 +1,64 @@ +validator = $validator; + } + + /** + * Returns TRUE to accept the message, FALSE to block it. + * + * @param array $event event data + * @return boolean + */ + public function filter(array $event) + { + return $this->validator->isValid($event['message']); + } +} diff --git a/src/Formatter/Base.php b/src/Formatter/Base.php new file mode 100644 index 00000000..162b95ce --- /dev/null +++ b/src/Formatter/Base.php @@ -0,0 +1,114 @@ +dateTimeFormat = $dateTimeFormat; + } + } + + /** + * Formats data to be written by the writer. + * + * @param array $event event data + * @return array + */ + public function format($event) + { + foreach ($event as $key => $value) { + // Keep extra as an array + if ('extra' === $key) { + $event[$key] = self::format($value); + } else { + $event[$key] = $this->normalize($value); + } + } + + return $event; + } + + /** + * Normalize all non-scalar data types (except null) in a string value + * + * @param mixed $value + * @return mixed + */ + protected function normalize($value) + { + if (is_scalar($value) || null === $value) { + return $value; + } + + if ($value instanceof DateTime) { + $value = $value->format($this->getDateTimeFormat()); + } elseif (is_array($value) || $value instanceof Traversable) { + if ($value instanceof Traversable) { + $value = iterator_to_array($value); + } + foreach ($value as $key => $subvalue) { + $value[$key] = $this->normalize($subvalue); + } + $value = json_encode($value); + } elseif (is_object($value) && !method_exists($value,'__toString')) { + $value = sprintf('object(%s) %s', get_class($value), json_encode($value)); + } elseif (is_resource($value)) { + $value = sprintf('resource(%s)', get_resource_type($value)); + } elseif (!is_object($value)) { + $value = gettype($value); + } + + return (string) $value; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeFormat() + { + return $this->dateTimeFormat; + } + + /** + * {@inheritDoc} + */ + public function setDateTimeFormat($dateTimeFormat) + { + $this->dateTimeFormat = (string) $dateTimeFormat; + return $this; + } +} \ No newline at end of file diff --git a/src/Formatter/Db.php b/src/Formatter/Db.php new file mode 100644 index 00000000..6f8a45d2 --- /dev/null +++ b/src/Formatter/Db.php @@ -0,0 +1,78 @@ +setDateTimeFormat($dateTimeFormat); + } + } + + /** + * Formats data to be written by the writer. + * + * @param array $event event data + * @return array + */ + public function format($event) + { + $format = $this->getDateTimeFormat(); + array_walk_recursive($event, function (&$value) use ($format) { + if ($value instanceof DateTime) { + $value = $value->format($format); + } + }); + + return $event; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeFormat() + { + return $this->dateTimeFormat; + } + + /** + * {@inheritDoc} + */ + public function setDateTimeFormat($dateTimeFormat) + { + $this->dateTimeFormat = (string) $dateTimeFormat; + return $this; + } +} diff --git a/src/Formatter/ErrorHandler.php b/src/Formatter/ErrorHandler.php new file mode 100644 index 00000000..72f6a304 --- /dev/null +++ b/src/Formatter/ErrorHandler.php @@ -0,0 +1,51 @@ +format; + + if (isset($event['timestamp']) && $event['timestamp'] instanceof DateTime) { + $event['timestamp'] = $event['timestamp']->format($this->getDateTimeFormat()); + } + + foreach ($event as $name => $value) { + if (is_array($value)) { + foreach ($value as $sname => $svalue) { + $output = str_replace("%{$name}[{$sname}]%", $svalue, $output); + } + } else { + $output = str_replace("%$name%", $value, $output); + } + } + + return $output; + } +} diff --git a/src/Formatter/ExceptionHandler.php b/src/Formatter/ExceptionHandler.php new file mode 100644 index 00000000..bb1e2bc5 --- /dev/null +++ b/src/Formatter/ExceptionHandler.php @@ -0,0 +1,96 @@ +format($this->getDateTimeFormat()); + } + + $output = $event['timestamp'] . ' ' . $event['priorityName'] . ' (' + . $event['priority'] . ') ' . $event['message'] .' in ' + . $event['extra']['file'] . ' on line ' . $event['extra']['line']; + + if (!empty($event['extra']['trace'])) { + $outputTrace = ''; + foreach ($event['extra']['trace'] as $trace) { + $outputTrace .= "File : {$trace['file']}\n" + . "Line : {$trace['line']}\n" + . "Func : {$trace['function']}\n" + . "Class : {$trace['class']}\n" + . "Type : " . $this->getType($trace['type']) . "\n" + . "Args : " . print_r($trace['args'], true) . "\n"; + } + $output .= "\n[Trace]\n" . $outputTrace; + } + + return $output; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeFormat() + { + return $this->dateTimeFormat; + } + + /** + * {@inheritDoc} + */ + public function setDateTimeFormat($dateTimeFormat) + { + $this->dateTimeFormat = (string) $dateTimeFormat; + return $this; + } + + /** + * Get the type of a function + * + * @param string $type + * @return string + */ + protected function getType($type) + { + switch ($type) { + case "::" : + return "static"; + case "->" : + return "method"; + default : + return $type; + } + } +} diff --git a/src/Formatter/FirePhp.php b/src/Formatter/FirePhp.php new file mode 100644 index 00000000..0a21cde1 --- /dev/null +++ b/src/Formatter/FirePhp.php @@ -0,0 +1,51 @@ +format = isset($format) ? $format : static::DEFAULT_FORMAT; + + parent::__construct($dateTimeFormat); + } + + /** + * Formats data into a single line to be written by the writer. + * + * @param array $event event data + * @return string formatted line to write to the log + */ + public function format($event) + { + $output = $this->format; + + $event = parent::format($event); + foreach ($event as $name => $value) { + if ('extra' == $name && count($value)) { + $value = $this->normalize($value); + } elseif ('extra' == $name) { + // Don't print an empty array + $value = ''; + } + $output = str_replace("%$name%", $value, $output); + } + + if (isset($event['extra']) && empty($event['extra']) + && false !== strpos($this->format, '%extra%') + ) { + $output = rtrim($output, ' '); + } + return $output; + } +} diff --git a/src/Formatter/Xml.php b/src/Formatter/Xml.php new file mode 100644 index 00000000..64bb6da2 --- /dev/null +++ b/src/Formatter/Xml.php @@ -0,0 +1,185 @@ + array_shift($args) + ); + + if (count($args)) { + $options['elementMap'] = array_shift($args); + } + + if (count($args)) { + $options['encoding'] = array_shift($args); + } + + if (count($args)) { + $options['dateTimeFormat'] = array_shift($args); + } + } + + if (!array_key_exists('rootElement', $options)) { + $options['rootElement'] = 'logEntry'; + } + + if (!array_key_exists('encoding', $options)) { + $options['encoding'] = 'UTF-8'; + } + + $this->rootElement = $options['rootElement']; + $this->setEncoding($options['encoding']); + + if (array_key_exists('elementMap', $options)) { + $this->elementMap = $options['elementMap']; + } + + if (array_key_exists('dateTimeFormat', $options)) { + $this->setDateTimeFormat($options['dateTimeFormat']); + } + } + + /** + * Get encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set encoding + * + * @param string $value + * @return Xml + */ + public function setEncoding($value) + { + $this->encoding = (string) $value; + return $this; + } + + /** + * Formats data into a single line to be written by the writer. + * + * @param array $event event data + * @return string formatted line to write to the log + */ + public function format($event) + { + if (isset($event['timestamp']) && $event['timestamp'] instanceof DateTime) { + $event['timestamp'] = $event['timestamp']->format($this->getDateTimeFormat()); + } + + if ($this->elementMap === null) { + $dataToInsert = $event; + } else { + $dataToInsert = array(); + foreach ($this->elementMap as $elementName => $fieldKey) { + $dataToInsert[$elementName] = $event[$fieldKey]; + } + } + + $enc = $this->getEncoding(); + $dom = new DOMDocument('1.0', $enc); + $elt = $dom->appendChild(new DOMElement($this->rootElement)); + + foreach ($dataToInsert as $key => $value) { + if (empty($value) + || is_scalar($value) + || (is_object($value) && method_exists($value,'__toString')) + ) { + if ($key == "message") { + $value = htmlspecialchars($value, ENT_COMPAT, $enc); + } elseif ($key == "extra" && empty($value)) { + continue; + } + $elt->appendChild(new DOMElement($key, (string)$value)); + } + } + + $xml = $dom->saveXML(); + $xml = preg_replace('/<\?xml version="1.0"( encoding="[^\"]*")?\?>\n/u', '', $xml); + + return $xml . PHP_EOL; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeFormat() + { + return $this->dateTimeFormat; + } + + /** + * {@inheritDoc} + */ + public function setDateTimeFormat($dateTimeFormat) + { + $this->dateTimeFormat = (string) $dateTimeFormat; + return $this; + } +} diff --git a/src/Logger.php b/src/Logger.php new file mode 100644 index 00000000..e6b54814 --- /dev/null +++ b/src/Logger.php @@ -0,0 +1,451 @@ + priority (short) name + * + * @var array + */ + protected $priorities = array( + self::EMERG => 'EMERG', + self::ALERT => 'ALERT', + self::CRIT => 'CRIT', + self::ERR => 'ERR', + self::WARN => 'WARN', + self::NOTICE => 'NOTICE', + self::INFO => 'INFO', + self::DEBUG => 'DEBUG', + ); + + /** + * Writers + * + * @var SplPriorityQueue + */ + protected $writers; + + /** + * Writer plugins + * + * @var WriterPluginManager + */ + protected $writerPlugins; + + /** + * Registered error handler + * + * @var boolean + */ + protected static $registeredErrorHandler = false; + + /** + * Registered exception handler + * + * @var boolean + */ + protected static $registeredExceptionHandler = false; + + /** + * Constructor + * + * @todo support configuration (writers, dateTimeFormat, and writer plugin manager) + * @return Logger + */ + public function __construct() + { + $this->writers = new SplPriorityQueue(); + } + + /** + * Shutdown all writers + * + * @return void + */ + public function __destruct() + { + foreach ($this->writers as $writer) { + try { + $writer->shutdown(); + } catch (\Exception $e) {} + } + } + + /** + * Get writer plugin manager + * + * @return WriterPluginManager + */ + public function getWriterPluginManager() + { + if (null === $this->writerPlugins) { + $this->setWriterPluginManager(new WriterPluginManager()); + } + return $this->writerPlugins; + } + + /** + * Set writer plugin manager + * + * @param string|WriterPluginManager $plugins + * @return Logger + * @throws Exception\InvalidArgumentException + */ + public function setWriterPluginManager($plugins) + { + if (is_string($plugins)) { + $plugins = new $plugins; + } + if (!$plugins instanceof WriterPluginManager) { + throw new Exception\InvalidArgumentException(sprintf( + 'Writer plugin manager must extend %s\WriterPluginManager; received %s', + __NAMESPACE__, + is_object($plugins) ? get_class($plugins) : gettype($plugins) + )); + } + + $this->writerPlugins = $plugins; + return $this; + } + + /** + * Get writer instance + * + * @param string $name + * @param array|null $options + * @return Writer + */ + public function writerPlugin($name, array $options = null) + { + return $this->getWriterPluginManager()->get($name, $options); + } + + /** + * Add a writer to a logger + * + * @param string|Writer $writer + * @param int $priority + * @return Logger + * @throws Exception\InvalidArgumentException + */ + public function addWriter($writer, $priority = 1, array $options = null) + { + if (is_string($writer)) { + $writer = $this->writerPlugin($writer, $options); + } elseif (!$writer instanceof Writer\WriterInterface) { + throw new Exception\InvalidArgumentException(sprintf( + 'Writer must implement Zend\Log\Writer; received "%s"', + is_object($writer) ? get_class($writer) : gettype($writer) + )); + } + $this->writers->insert($writer, $priority); + + return $this; + } + + /** + * Get writers + * + * @return SplPriorityQueue + */ + public function getWriters() + { + return $this->writers; + } + + /** + * Set the writers + * + * @param SplPriorityQueue $writers + * @return Logger + * @throws Exception\InvalidArgumentException + */ + public function setWriters(SplPriorityQueue $writers) + { + foreach ($writers->toArray() as $writer) { + if (!$writer instanceof Writer\WriterInterface) { + throw new Exception\InvalidArgumentException('Writers must be a SplPriorityQueue of Zend\Log\Writer'); + } + } + $this->writers = $writers; + return $this; + } + + /** + * Add a message as a log entry + * + * @param int $priority + * @param mixed $message + * @param array|Traversable $extra + * @return Logger + * @throws Exception\InvalidArgumentException if message can't be cast to string + * @throws Exception\InvalidArgumentException if extra can't be iterated over + */ + public function log($priority, $message, $extra = array()) + { + if (!is_int($priority) || ($priority<0) || ($priority>=count($this->priorities))) { + throw new Exception\InvalidArgumentException(sprintf( + '$priority must be an integer > 0 and < %d; received %s', + count($this->priorities), + var_export($priority, 1) + )); + } + if (is_object($message) && !method_exists($message, '__toString')) { + throw new Exception\InvalidArgumentException( + '$message must implement magic __toString() method' + ); + } + + if (!is_array($extra) && !$extra instanceof Traversable) { + throw new Exception\InvalidArgumentException( + '$extra must be an array or implement Traversable' + ); + } elseif ($extra instanceof Traversable) { + $extra = ArrayUtils::iteratorToArray($extra); + } + + if ($this->writers->count() === 0) { + throw new Exception\RuntimeException('No log writer specified'); + } + + $timestamp = new DateTime(); + + if (is_array($message)) { + $message = var_export($message, true); + } + + foreach ($this->writers->toArray() as $writer) { + $writer->write(array( + 'timestamp' => $timestamp, + 'priority' => (int) $priority, + 'priorityName' => $this->priorities[$priority], + 'message' => (string) $message, + 'extra' => $extra + )); + } + + return $this; + } + + /** + * @param string $message + * @param array|Traversable $extra + * @return Logger + */ + public function emerg($message, $extra = array()) + { + return $this->log(self::EMERG, $message, $extra); + } + + /** + * @param string $message + * @param array|Traversable $extra + * @return Logger + */ + public function alert($message, $extra = array()) + { + return $this->log(self::ALERT, $message, $extra); + } + + /** + * @param string $message + * @param array|Traversable $extra + * @return Logger + */ + public function crit($message, $extra = array()) + { + return $this->log(self::CRIT, $message, $extra); + } + + /** + * @param string $message + * @param array|Traversable $extra + * @return Logger + */ + public function err($message, $extra = array()) + { + return $this->log(self::ERR, $message, $extra); + } + + /** + * @param string $message + * @param array|Traversable $extra + * @return Logger + */ + public function warn($message, $extra = array()) + { + return $this->log(self::WARN, $message, $extra); + } + + /** + * @param string $message + * @param array|Traversable $extra + * @return Logger + */ + public function notice($message, $extra = array()) + { + return $this->log(self::NOTICE, $message, $extra); + } + + /** + * @param string $message + * @param array|Traversable $extra + * @return Logger + */ + public function info($message, $extra = array()) + { + return $this->log(self::INFO, $message, $extra); + } + + /** + * @param string $message + * @param array|Traversable $extra + * @return Logger + */ + public function debug($message, $extra = array()) + { + return $this->log(self::DEBUG, $message, $extra); + } + + /** + * Register logging system as an error handler to log PHP errors + * + * @link http://www.php.net/manual/en/function.set-error-handler.php + * @param Logger $logger + * @return bool + * @throws Exception\InvalidArgumentException if logger is null + */ + public static function registerErrorHandler(Logger $logger) + { + // Only register once per instance + if (self::$registeredErrorHandler) { + return false; + } + + if ($logger === null) { + throw new Exception\InvalidArgumentException('Invalid Logger specified'); + } + + $errorHandlerMap = array( + E_NOTICE => self::NOTICE, + E_USER_NOTICE => self::NOTICE, + E_WARNING => self::WARN, + E_CORE_WARNING => self::WARN, + E_USER_WARNING => self::WARN, + E_ERROR => self::ERR, + E_USER_ERROR => self::ERR, + E_CORE_ERROR => self::ERR, + E_RECOVERABLE_ERROR => self::ERR, + E_STRICT => self::DEBUG, + E_DEPRECATED => self::DEBUG, + E_USER_DEPRECATED => self::DEBUG + ); + + set_error_handler(function($errno, $errstr, $errfile, $errline, $errcontext) use ($errorHandlerMap, $logger) { + $errorLevel = error_reporting(); + + if ($errorLevel && $errno) { + if (isset($errorHandlerMap[$errno])) { + $priority = $errorHandlerMap[$errno]; + } else { + $priority = Logger::INFO; + } + $logger->log($priority, $errstr, array( + 'errno' => $errno, + 'file' => $errfile, + 'line' => $errline, + 'context' => $errcontext + )); + } + }); + self::$registeredErrorHandler = true; + return true; + } + + /** + * Unregister error handler + * + */ + public static function unregisterErrorHandler() + { + restore_error_handler(); + self::$registeredErrorHandler = false; + } + + /** + * Register logging system as an exception handler to log PHP exceptions + * + * @link http://www.php.net/manual/en/function.set-exception-handler.php + * @param Logger $logger + * @return bool + * @throws Exception\InvalidArgumentException if logger is null + */ + public static function registerExceptionHandler(Logger $logger) + { + // Only register once per instance + if (self::$registeredExceptionHandler) { + return false; + } + + if ($logger === null) { + throw new Exception\InvalidArgumentException('Invalid Logger specified'); + } + + set_exception_handler(function ($exception) use ($logger) { + $extra = array( + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => $exception->getTrace() + ); + if (isset($exception->xdebug_message)) { + $extra['xdebug'] = $exception->xdebug_message; + } + $logger->log(Logger::ERR, $exception->getMessage(), $extra); + }); + self::$registeredExceptionHandler = true; + return true; + } + + /** + * Unregister exception handler + */ + public static function unregisterExceptionHandler() + { + restore_exception_handler(); + self::$registeredExceptionHandler = false; + } +} diff --git a/src/LoggerAwareInterface.php b/src/LoggerAwareInterface.php new file mode 100644 index 00000000..647be793 --- /dev/null +++ b/src/LoggerAwareInterface.php @@ -0,0 +1,24 @@ +filterPlugin($filter, $options); + } + + if (!$filter instanceof Filter\FilterInterface) { + throw new Exception\InvalidArgumentException(sprintf( + 'Writer must implement Zend\Log\Filter\FilterInterface; received "%s"', + is_object($filter) ? get_class($filter) : gettype($filter) + )); + } + + $this->filters[] = $filter; + return $this; + } + + /** + * Get filter plugin manager + * + * @return FilterPluginManager + */ + public function getFilterPluginManager() + { + if (null === $this->filterPlugins) { + $this->setFilterPluginManager(new FilterPluginManager()); + } + return $this->filterPlugins; + } + + /** + * Set filter plugin manager + * + * @param string|FilterPluginManager $plugins + * @return Logger + * @throws Exception\InvalidArgumentException + */ + public function setFilterPluginManager($plugins) + { + if (is_string($plugins)) { + $plugins = new $plugins; + } + if (!$plugins instanceof FilterPluginManager) { + throw new Exception\InvalidArgumentException(sprintf( + 'Writer plugin manager must extend %s\FilterPluginManager; received %s', + __NAMESPACE__, + is_object($plugins) ? get_class($plugins) : gettype($plugins) + )); + } + + $this->filterPlugins = $plugins; + return $this; + } + + /** + * Get filter instance + * + * @param string $name + * @param array|null $options + * @return Writer + */ + public function filterPlugin($name, array $options = null) + { + return $this->getFilterPluginManager()->get($name, $options); + } + + /** + * Log a message to this writer. + * + * @param array $event log data event + * @return void + */ + public function write(array $event) + { + foreach ($this->filters as $filter) { + if (!$filter->filter($event)) { + return; + } + } + + // exception occurs on error + $this->doWrite($event); + } + + /** + * Set a new formatter for this writer + * + * @param Formatter $formatter + * @return self + */ + public function setFormatter(Formatter $formatter) + { + $this->formatter = $formatter; + return $this; + } + + /** + * Perform shutdown activities such as closing open resources + * + * @return void + */ + public function shutdown() + {} + + /** + * Write a message to the log + * + * @param array $event log data event + * @return void + */ + abstract protected function doWrite(array $event); +} diff --git a/src/Writer/Db.php b/src/Writer/Db.php new file mode 100644 index 00000000..0d35606e --- /dev/null +++ b/src/Writer/Db.php @@ -0,0 +1,206 @@ +db = $db; + $this->tableName = $tableName; + $this->columnMap = $columnMap; + + if (!empty($separator)) { + $this->separator = $separator; + } + + $this->setFormatter(new DbFormatter()); + } + + /** + * Remove reference to database adapter + * + * @return void + */ + public function shutdown() + { + $this->db = null; + } + + /** + * Write a message to the log. + * + * @param array $event event data + * @return void + * @throws Exception\RuntimeException + */ + protected function doWrite(array $event) + { + if (null === $this->db) { + throw new Exception\RuntimeException('Database adapter is null'); + } + + $event = $this->formatter->format($event); + + // Transform the event array into fields + if (null === $this->columnMap) { + $dataToInsert = $this->eventIntoColumn($event); + } else { + $dataToInsert = $this->mapEventIntoColumn($event, $this->columnMap); + } + + $statement = $this->db->query($this->prepareInsert($this->db, $this->tableName, $dataToInsert)); + $statement->execute($dataToInsert); + + } + + /** + * Prepare the INSERT SQL statement + * + * @param Adapter $db + * @param string $tableName + * @param array $fields + * @return string + */ + protected function prepareInsert(Adapter $db, $tableName, array $fields) + { + $keys = array_keys($fields); + $sql = 'INSERT INTO ' . $db->platform->quoteIdentifier($tableName) . ' (' . + implode(",",array_map(array($db->platform, 'quoteIdentifier'), $keys)) . ') VALUES (' . + implode(",",array_map(array($db->driver, 'formatParameterName'), $keys)) . ')'; + + return $sql; + } + + /** + * Map event into column using the $columnMap array + * + * @param array $event + * @param array $columnMap + * @return array + */ + protected function mapEventIntoColumn(array $event, array $columnMap = null) + { + if (empty($event)) { + return array(); + } + + $data = array(); + foreach ($event as $name => $value) { + if (is_array($value)) { + foreach ($value as $key => $subvalue) { + if (isset($columnMap[$name][$key])) { + $data[$columnMap[$name][$key]] = $subvalue; + } + } + } elseif (isset($columnMap[$name])) { + $data[$columnMap[$name]] = $value; + } + } + return $data; + } + + /** + * Transform event into column for the db table + * + * @param array $event + * @return array + */ + protected function eventIntoColumn(array $event) + { + if (empty($event)) { + return array(); + } + + $data = array(); + foreach ($event as $name => $value) { + if (is_array($value)) { + foreach ($value as $key => $subvalue) { + $data[$name . $this->separator . $key] = $subvalue; + } + } else { + $data[$name] = $value; + } + } + return $data; + } +} diff --git a/src/Writer/FilterPluginManager.php b/src/Writer/FilterPluginManager.php new file mode 100644 index 00000000..83cd8135 --- /dev/null +++ b/src/Writer/FilterPluginManager.php @@ -0,0 +1,67 @@ + 'Zend\Log\Filter\Mock', + 'priority' => 'Zend\Log\Filter\Priority', + 'regex' => 'Zend\Log\Filter\Regex', + 'suppress' => 'Zend\Log\Filter\suppressFilter', + 'suppressfilter' => 'Zend\Log\Filter\suppressFilter', + 'validator' => 'Zend\Log\Filter\Validator', + ); + + /** + * Allow many filters of the same type + * + * @var bool + */ + protected $shareByDefault = false; + + /** + * Validate the plugin + * + * Checks that the writer loaded is an instance of Filter\FilterInterface. + * + * @param mixed $plugin + * @return void + * @throws Exception\InvalidArgumentException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Filter\FilterInterface) { + // we're okay + return; + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Plugin of type %s is invalid; must implement %s\Filter\FilterInterface', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/src/Writer/FirePhp.php b/src/Writer/FirePhp.php new file mode 100644 index 00000000..a96de9aa --- /dev/null +++ b/src/Writer/FirePhp.php @@ -0,0 +1,112 @@ +firephp = $instance; + $this->formatter = new FirePhpFormatter(); + } + + /** + * Write a message to the log. + * + * @param array $event event data + * @return void + */ + protected function doWrite(array $event) + { + $firephp = $this->getFirePhp(); + + if (!$firephp->getEnabled()) { + return; + } + + $line = $this->formatter->format($event); + + switch ($event['priority']) { + case Logger::EMERG: + case Logger::ALERT: + case Logger::CRIT: + case Logger::ERR: + $firephp->error($line); + break; + case Logger::WARN: + $firephp->warn($line); + break; + case Logger::NOTICE: + case Logger::INFO: + $firephp->info($line); + break; + case Logger::DEBUG: + $firephp->trace($line); + break; + default: + $firephp->log($line); + break; + } + } + + /** + * Gets the FirePhpInterface instance that is used for logging. + * + * @return FirePhp\FirePhpInterface + */ + public function getFirePhp() + { + // Remember: class names in strings are absolute; thus the class_exists + // here references the canonical name for the FirePHP class + if (!$this->firephp instanceof FirePhp\FirePhpInterface + && class_exists('FirePHP') + ) { + // FirePHPService is an alias for FirePHP; otherwise the class + // names would clash in this file on this line. + $this->setFirePhp(new FirePhp\FirePhpBridge(new FirePHPService())); + } + return $this->firephp; + } + + /** + * Sets the FirePhpInterface instance that is used for logging. + * + * @param FirePhp\FirePhpInterface $instance A FirePhpInterface instance to set. + * @return FirePhp + */ + public function setFirePhp(FirePhp\FirePhpInterface $instance) + { + $this->firephp = $instance; + return $this; + } +} diff --git a/src/Writer/FirePhp/FirePhpBridge.php b/src/Writer/FirePhp/FirePhpBridge.php new file mode 100644 index 00000000..806c81d3 --- /dev/null +++ b/src/Writer/FirePhp/FirePhpBridge.php @@ -0,0 +1,113 @@ +firephp = $firephp; + } + + /** + * Retrieve FirePHP instance + * + * @return FirePHP + */ + public function getFirePhp() + { + return $this->firephp; + } + + /** + * Determine whether or not FirePHP is enabled + * + * @return bool + */ + public function getEnabled() + { + return $this->firephp->getEnabled(); + } + + /** + * Log an error message + * + * @param string $line + * @return void + */ + public function error($line) + { + return $this->firephp->error($line); + } + + /** + * Log a warning + * + * @param string $line + * @return void + */ + public function warn($line) + { + return $this->firephp->warn($line); + } + + /** + * Log informational message + * + * @param string $line + * @return void + */ + public function info($line) + { + return $this->firephp->info($line); + } + + /** + * Log a trace + * + * @param string $line + * @return void + */ + public function trace($line) + { + return $this->firephp->trace($line); + } + + /** + * Log a message + * + * @param string $line + * @return void + */ + public function log($line) + { + return $this->firephp->trace($line); + } +} diff --git a/src/Writer/FirePhp/FirePhpInterface.php b/src/Writer/FirePhp/FirePhpInterface.php new file mode 100644 index 00000000..23cf4b9d --- /dev/null +++ b/src/Writer/FirePhp/FirePhpInterface.php @@ -0,0 +1,61 @@ +mail = $mail; + + // Ensure we have a valid mail transport + if (null === $transport) { + $transport = new Transport\Sendmail(); + } + if (!$transport instanceof Transport\TransportInterface) { + throw new Exception\InvalidArgumentException(sprintf( + 'Transport parameter of type %s is invalid; must be of type Zend\Mail\Transport\TransportInterface', + (is_object($transport) ? get_class($transport) : gettype($transport)) + )); + } + $this->setTransport($transport); + + $this->formatter = new SimpleFormatter(); + } + + /** + * Set the transport message + * + * @param Transport\TransportInterface $transport + * @return Mail + */ + public function setTransport(Transport\TransportInterface $transport) + { + $this->transport = $transport; + return $this; + } + + /** + * Places event line into array of lines to be used as message body. + * + * @param array $event Event data + */ + protected function doWrite(array $event) + { + // Track the number of entries per priority level. + if (!isset($this->numEntriesPerPriority[$event['priorityName']])) { + $this->numEntriesPerPriority[$event['priorityName']] = 1; + } else { + $this->numEntriesPerPriority[$event['priorityName']]++; + } + + // All plaintext events are to use the standard formatter. + $this->eventsToMail[] = $this->formatter->format($event); + } + + /** + * Allows caller to have the mail subject dynamically set to contain the + * entry counts per-priority level. + * + * Sets the text for use in the subject, with entry counts per-priority + * level appended to the end. Since a Zend\Mail\Message subject can only be set + * once, this method cannot be used if the Zend\Mail\Message object already has a + * subject set. + * + * @param string $subject Subject prepend text + * @return Mail + */ + public function setSubjectPrependText($subject) + { + $this->subjectPrependText = (string) $subject; + return $this; + } + + /** + * Sends mail to recipient(s) if log entries are present. Note that both + * plaintext and HTML portions of email are handled here. + */ + public function shutdown() + { + // If there are events to mail, use them as message body. Otherwise, + // there is no mail to be sent. + if (empty($this->eventsToMail)) { + return; + } + + if ($this->subjectPrependText !== null) { + // Tack on the summary of entries per-priority to the subject + // line and set it on the Zend\Mail object. + $numEntries = $this->getFormattedNumEntriesPerPriority(); + $this->mail->setSubject("{$this->subjectPrependText} ({$numEntries})"); + } + + // Always provide events to mail as plaintext. + $this->mail->setBody(implode(PHP_EOL, $this->eventsToMail)); + + // Finally, send the mail. If an exception occurs, convert it into a + // warning-level message so we can avoid an exception thrown without a + // stack frame. + try { + $this->transport->send($this->mail); + } catch (TransportException\ExceptionInterface $e) { + trigger_error( + "unable to send log entries via email; " . + "message = {$e->getMessage()}; " . + "code = {$e->getCode()}; " . + "exception class = " . get_class($e), + E_USER_WARNING); + } + } + + /** + * Gets a string of number of entries per-priority level that occurred, or + * an empty string if none occurred. + * + * @return string + */ + protected function getFormattedNumEntriesPerPriority() + { + $strings = array(); + + foreach ($this->numEntriesPerPriority as $priority => $numEntries) { + $strings[] = "{$priority}={$numEntries}"; + } + + return implode(', ', $strings); + } +} diff --git a/src/Writer/Mock.php b/src/Writer/Mock.php new file mode 100644 index 00000000..995c38a4 --- /dev/null +++ b/src/Writer/Mock.php @@ -0,0 +1,54 @@ +events[] = $event; + } + + /** + * Record shutdown + * + * @return void + */ + public function shutdown() + { + $this->shutdown = true; + } +} diff --git a/src/Writer/MongoDB.php b/src/Writer/MongoDB.php new file mode 100644 index 00000000..f9cae3dc --- /dev/null +++ b/src/Writer/MongoDB.php @@ -0,0 +1,119 @@ +mongoCollection = $mongo->selectCollection($database, $collection); + $this->saveOptions = $saveOptions; + } + + /** + * This writer does not support formatting. + * + * @param Zend\Log\Formatter\FormatterInterface $formatter + * @return void + * @throws Zend\Log\Exception\InvalidArgumentException + */ + public function setFormatter(FormatterInterface $formatter) + { + throw new InvalidArgumentException(get_class() . ' does not support formatting'); + } + + /** + * Write a message to the log. + * + * @param array $event Event data + * @return void + * @throws Zend\Log\Exception\RuntimeException + */ + protected function doWrite(array $event) + { + if (null === $this->mongoCollection) { + throw new RuntimeException('MongoCollection must be defined'); + } + + if (isset($event['timestamp']) && $event['timestamp'] instanceof DateTime) { + $event['timestamp'] = new MongoDate($event['timestamp']->getTimestamp()); + } + + $this->mongoCollection->save($event, $this->saveOptions); + } +} diff --git a/src/Writer/Null.php b/src/Writer/Null.php new file mode 100644 index 00000000..d6f53202 --- /dev/null +++ b/src/Writer/Null.php @@ -0,0 +1,28 @@ +stream = $streamOrUrl; + } else { + ErrorHandler::start(); + $this->stream = fopen($streamOrUrl, $mode, false); + $error = ErrorHandler::stop(); + if (!$this->stream) { + throw new Exception\RuntimeException(sprintf( + '"%s" cannot be opened with mode "%s"', + $streamOrUrl, + $mode + ), 0, $error); + } + } + + if (null !== $logSeparator) { + $this->setLogSeparator($logSeparator); + } + + $this->formatter = new SimpleFormatter(); + } + + /** + * Write a message to the log. + * + * @param array $event event data + * @return void + * @throws Exception\RuntimeException + */ + protected function doWrite(array $event) + { + $line = $this->formatter->format($event) . $this->logSeparator; + + ErrorHandler::start(E_WARNING); + $result = fwrite($this->stream, $line); + $error = ErrorHandler::stop(); + if (false === $result) { + throw new Exception\RuntimeException("Unable to write to stream", 0, $error); + } + } + + /** + * Set log separator string + * + * @param string $logSeparator + * @return Stream + */ + public function setLogSeparator($logSeparator) + { + $this->logSeparator = (string) $logSeparator; + return $this; + } + + /** + * Get log separator string + * + * @return string + */ + public function getLogSeparator() + { + return $this->logSeparator; + } + + /** + * Close the stream resource. + * + * @return void + */ + public function shutdown() + { + if (is_resource($this->stream)) { + fclose($this->stream); + } + } +} diff --git a/src/Writer/Syslog.php b/src/Writer/Syslog.php new file mode 100644 index 00000000..aebf4fdc --- /dev/null +++ b/src/Writer/Syslog.php @@ -0,0 +1,244 @@ + LOG_EMERG, + Logger::ALERT => LOG_ALERT, + Logger::CRIT => LOG_CRIT, + Logger::ERR => LOG_ERR, + Logger::WARN => LOG_WARNING, + Logger::NOTICE => LOG_NOTICE, + Logger::INFO => LOG_INFO, + Logger::DEBUG => LOG_DEBUG, + ); + + /** + * The default log priority - for unmapped custom priorities + * + * @var string + */ + protected $defaultPriority = LOG_NOTICE; + + /** + * Last application name set by a syslog-writer instance + * + * @var string + */ + protected static $lastApplication; + + /** + * Last facility name set by a syslog-writer instance + * + * @var string + */ + protected static $lastFacility; + + /** + * Application name used by this syslog-writer instance + * + * @var string + */ + protected $appName = 'Zend\Log'; + + /** + * Facility used by this syslog-writer instance + * + * @var int + */ + protected $facility = LOG_USER; + + /** + * Types of program available to logging of message + * + * @var array + */ + protected $validFacilities = array(); + + /** + * Constructor + * + * @param array $params Array of options; may include "application" and "facility" keys + * @return Syslog + */ + public function __construct(array $params = array()) + { + if (isset($params['application'])) { + $this->application = $params['application']; + } + + $runInitializeSyslog = true; + if (isset($params['facility'])) { + $this->setFacility($params['facility']); + $runInitializeSyslog = false; + } + + if ($runInitializeSyslog) { + $this->initializeSyslog(); + } + + $this->setFormatter(new SimpleFormatter('%message%')); + } + + /** + * Initialize values facilities + * + * @return void + */ + protected function initializeValidFacilities() + { + $constants = array( + 'LOG_AUTH', + 'LOG_AUTHPRIV', + 'LOG_CRON', + 'LOG_DAEMON', + 'LOG_KERN', + 'LOG_LOCAL0', + 'LOG_LOCAL1', + 'LOG_LOCAL2', + 'LOG_LOCAL3', + 'LOG_LOCAL4', + 'LOG_LOCAL5', + 'LOG_LOCAL6', + 'LOG_LOCAL7', + 'LOG_LPR', + 'LOG_MAIL', + 'LOG_NEWS', + 'LOG_SYSLOG', + 'LOG_USER', + 'LOG_UUCP' + ); + + foreach ($constants as $constant) { + if (defined($constant)) { + $this->validFacilities[] = constant($constant); + } + } + } + + /** + * Initialize syslog / set application name and facility + * + * @return void + */ + protected function initializeSyslog() + { + self::$lastApplication = $this->appName; + self::$lastFacility = $this->facility; + openlog($this->appName, LOG_PID, $this->facility); + } + + /** + * Set syslog facility + * + * @param int $facility Syslog facility + * @return Syslog + * @throws Exception\InvalidArgumentException for invalid log facility + */ + public function setFacility($facility) + { + if ($this->facility === $facility) { + return $this; + } + + if (!count($this->validFacilities)) { + $this->initializeValidFacilities(); + } + + if (!in_array($facility, $this->validFacilities)) { + throw new Exception\InvalidArgumentException( + 'Invalid log facility provided; please see http://php.net/openlog for a list of valid facility values' + ); + } + + if ('WIN' == strtoupper(substr(PHP_OS, 0, 3)) + && ($facility !== LOG_USER) + ) { + throw new Exception\InvalidArgumentException( + 'Only LOG_USER is a valid log facility on Windows' + ); + } + + $this->facility = $facility; + $this->initializeSyslog(); + return $this; + } + + /** + * Set application name + * + * @param string $appName Application name + * @return Syslog + */ + public function setApplicationName($appName) + { + if ($this->appName === $appName) { + return $this; + } + + $this->appName = $appName; + $this->initializeSyslog(); + return $this; + } + + /** + * Close syslog. + * + * @return void + */ + public function shutdown() + { + closelog(); + } + + /** + * Write a message to syslog. + * + * @param array $event event data + * @return void + */ + protected function doWrite(array $event) + { + if (array_key_exists($event['priority'], $this->priorities)) { + $priority = $this->priorities[$event['priority']]; + } else { + $priority = $this->defaultPriority; + } + + if ($this->appName !== self::$lastApplication + || $this->facility !== self::$lastFacility + ) { + $this->initializeSyslog(); + } + + $message = $this->formatter->format($event); + + syslog($priority, $message); + } +} diff --git a/src/Writer/WriterInterface.php b/src/Writer/WriterInterface.php new file mode 100644 index 00000000..9073775a --- /dev/null +++ b/src/Writer/WriterInterface.php @@ -0,0 +1,52 @@ +isEnabled = false; + } + if (function_exists('zend_monitor_custom_event')) { + $this->isZendServer = true; + } + } + + /** + * Is logging to this writer enabled? + * + * If the Zend Monitor extension is not enabled, this log writer will + * fail silently. You can query this method to determine if the log + * writer is enabled. + * + * @return boolean + */ + public function isEnabled() + { + return $this->isEnabled; + } + + /** + * Log a message to this writer. + * + * @param array $event log data event + * @return void + */ + public function write(array $event) + { + if (!$this->isEnabled()) { + return; + } + + parent::write($event); + } + + /** + * Write a message to the log. + * + * @param array $event log data event + * @return void + */ + protected function doWrite(array $event) + { + $priority = $event['priority']; + $message = $event['message']; + unset($event['priority'], $event['message']); + + if (!empty($event)) { + if ($this->isZendServer) { + // On Zend Server; third argument should be the event + zend_monitor_custom_event($priority, $message, $event); + } else { + // On Zend Platform; third argument is severity -- either + // 0 or 1 -- and fourth is optional (event) + // Severity is either 0 (normal) or 1 (severe); classifying + // notice, info, and debug as "normal", and all others as + // "severe" + monitor_custom_event($priority, $message, ($priority > 4) ? 0 : 1, $event); + } + } else { + monitor_custom_event($priority, $message); + } + } +} diff --git a/src/WriterPluginManager.php b/src/WriterPluginManager.php new file mode 100644 index 00000000..53558237 --- /dev/null +++ b/src/WriterPluginManager.php @@ -0,0 +1,67 @@ + 'Zend\Log\Writer\Db', + 'firephp' => 'Zend\Log\Writer\FirePhp', + 'mail' => 'Zend\Log\Writer\Mail', + 'mock' => 'Zend\Log\Writer\Mock', + 'null' => 'Zend\Log\Writer\Null', + 'stream' => 'Zend\Log\Writer\Stream', + 'syslog' => 'Zend\Log\Writer\Syslog', + 'zendmonitor' => 'Zend\Log\Writer\ZendMonitor', + ); + + /** + * Allow many writers of the same type + * + * @var bool + */ + protected $shareByDefault = false; + + /** + * Validate the plugin + * + * Checks that the writer loaded is an instance of Writer\WriterInterface. + * + * @param mixed $plugin + * @return void + * @throws Exception\InvalidArgumentException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Writer\WriterInterface) { + // we're okay + return; + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Plugin of type %s is invalid; must implement %s\Writer\WriterInterface', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/test/Filter/MockTest.php b/test/Filter/MockTest.php new file mode 100644 index 00000000..d198332b --- /dev/null +++ b/test/Filter/MockTest.php @@ -0,0 +1,33 @@ +assertSame(array(), $filter->events); + + $fields = array('foo' => 'bar'); + $this->assertTrue($filter->filter($fields)); + $this->assertSame(array($fields), $filter->events); + } +} diff --git a/test/Filter/PriorityTest.php b/test/Filter/PriorityTest.php new file mode 100644 index 00000000..905eb146 --- /dev/null +++ b/test/Filter/PriorityTest.php @@ -0,0 +1,49 @@ +assertTrue($filter->filter(array('priority' => 2))); + $this->assertTrue($filter->filter(array('priority' => 1))); + $this->assertFalse($filter->filter(array('priority' => 3))); + } + + public function testComparisonOperatorCanBeChanged() + { + // accept above priority 2 + $filter = new Priority(2, '>'); + + $this->assertTrue($filter->filter(array('priority' => 3))); + $this->assertFalse($filter->filter(array('priority' => 2))); + $this->assertFalse($filter->filter(array('priority' => 1))); + } + + public function testConstructorThrowsOnInvalidPriority() + { + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'must be an integer'); + new Priority('foo'); + } +} diff --git a/test/Filter/RegexTest.php b/test/Filter/RegexTest.php new file mode 100644 index 00000000..b757c761 --- /dev/null +++ b/test/Filter/RegexTest.php @@ -0,0 +1,37 @@ +setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'invalid reg'); + new Regex('invalid regexp'); + } + + public function testMessageFilter() + { + $filter = new Regex('/accept/'); + $this->assertTrue($filter->filter(array('message' => 'foo accept bar'))); + $this->assertFalse($filter->filter(array('message' => 'foo reject bar'))); + } +} diff --git a/test/Filter/SuppressFilterTest.php b/test/Filter/SuppressFilterTest.php new file mode 100644 index 00000000..c16062f0 --- /dev/null +++ b/test/Filter/SuppressFilterTest.php @@ -0,0 +1,57 @@ +filter = new SuppressFilter(); + } + + public function testSuppressIsInitiallyOff() + { + $this->assertTrue($this->filter->filter(array())); + } + + public function testSuppressOn() + { + $this->filter->suppress(true); + $this->assertFalse($this->filter->filter(array())); + $this->assertFalse($this->filter->filter(array())); + } + + public function testSuppressOff() + { + $this->filter->suppress(false); + $this->assertTrue($this->filter->filter(array())); + $this->assertTrue($this->filter->filter(array())); + } + + public function testSuppressCanBeReset() + { + $this->filter->suppress(true); + $this->assertFalse($this->filter->filter(array())); + $this->filter->suppress(false); + $this->assertTrue($this->filter->filter(array())); + $this->filter->suppress(true); + $this->assertFalse($this->filter->filter(array())); + } +} diff --git a/test/Filter/ValidatorTest.php b/test/Filter/ValidatorTest.php new file mode 100644 index 00000000..3e7a1181 --- /dev/null +++ b/test/Filter/ValidatorTest.php @@ -0,0 +1,45 @@ +assertTrue($filter->filter(array('message' => '123'))); + $this->assertFalse($filter->filter(array('message' => 'test'))); + $this->assertFalse($filter->filter(array('message' => 'test123'))); + $this->assertFalse($filter->filter(array('message' => '(%$'))); + } + + public function testValidatorChain() + { + $validatorChain = new ValidatorChain(); + $validatorChain->addValidator(new DigitsFilter()); + $validatorChain->addValidator(new Int()); + $filter = new Validator($validatorChain); + $this->assertTrue($filter->filter(array('message' => '123'))); + $this->assertFalse($filter->filter(array('message' => 'test'))); + } +} diff --git a/test/Formatter/BaseTest.php b/test/Formatter/BaseTest.php new file mode 100644 index 00000000..d5d5f8a5 --- /dev/null +++ b/test/Formatter/BaseTest.php @@ -0,0 +1,113 @@ +assertEquals(BaseFormatter::DEFAULT_DATETIME_FORMAT, $formatter->getDateTimeFormat()); + } + + /** + * @dataProvider provideDateTimeFormats + */ + public function testAllowsSpecifyingDateTimeFormatAsConstructorArgument($dateTimeFormat) + { + $formatter = new BaseFormatter($dateTimeFormat); + + $this->assertEquals($dateTimeFormat, $formatter->getDateTimeFormat()); + } + + /** + * @return array + */ + public function provideDateTimeFormats() + { + return array( + array('r'), + array('U'), + array(DateTime::RSS), + ); + } + + /** + * @dataProvider provideDateTimeFormats + */ + public function testSetDateTimeFormat($dateTimeFormat) + { + $formatter = new BaseFormatter(); + $formatter->setDateTimeFormat($dateTimeFormat); + + $this->assertEquals($dateTimeFormat, $formatter->getDateTimeFormat()); + } + + public function testFormatAllTypes() + { + $datetime = new DateTime(); + $object = new stdClass(); + $object->foo = 'bar'; + $formatter = new BaseFormatter(); + + $event = array( + 'timestamp' => $datetime, + 'priority' => 1, + 'message' => 'tottakai', + 'extra' => array( + 'float' => 0.2, + 'boolean' => false, + 'array_empty' => array(), + 'array' => range(0, 4), + 'traversable_empty' => new EmptyIterator(), + 'traversable' => new ArrayIterator(array('id', 42)), + 'null' => null, + 'object_empty' => new stdClass(), + 'object' => $object, + 'string object' => new StringObject(), + 'resource' => fopen('php://stdout', 'w'), + ), + ); + $outputExpected = array( + 'timestamp' => $datetime->format($formatter->getDateTimeFormat()), + 'priority' => 1, + 'message' => 'tottakai', + 'extra' => array( + 'boolean' => false, + 'float' => 0.2, + 'array_empty' => '[]', + 'array' => '[0,1,2,3,4]', + 'traversable_empty' => '[]', + 'traversable' => '["id",42]', + 'null' => null, + 'object_empty' => 'object(stdClass) {}', + 'object' => 'object(stdClass) {"foo":"bar"}', + 'string object' => 'Hello World', + 'resource' => 'resource(stream)', + ), + ); + + $this->assertEquals($outputExpected, $formatter->format($event)); + } +} diff --git a/test/Formatter/DbTest.php b/test/Formatter/DbTest.php new file mode 100644 index 00000000..b1de2ee2 --- /dev/null +++ b/test/Formatter/DbTest.php @@ -0,0 +1,73 @@ +assertEquals(DbFormatter::DEFAULT_DATETIME_FORMAT, $formatter->getDateTimeFormat()); + } + + /** + * @dataProvider provideDateTimeFormats + */ + public function testSetDateTimeFormat($dateTimeFormat) + { + $formatter = new DbFormatter(); + $formatter->setDateTimeFormat($dateTimeFormat); + + $this->assertEquals($dateTimeFormat, $formatter->getDateTimeFormat()); + } + + /** + * @return array + */ + public function provideDateTimeFormats() + { + return array( + array('r'), + array('U'), + array(DateTime::RSS), + ); + } + + /** + * @dataProvider provideDateTimeFormats + */ + public function testAllowsSpecifyingDateTimeFormatAsConstructorArgument($dateTimeFormat) + { + $formatter = new DbFormatter($dateTimeFormat); + + $this->assertEquals($dateTimeFormat, $formatter->getDateTimeFormat()); + } + + public function testFormatDateTimeInEvent() + { + $datetime = new DateTime(); + $event = array('timestamp' => $datetime); + $formatter = new DbFormatter(); + + $format = DbFormatter::DEFAULT_DATETIME_FORMAT; + $this->assertContains($datetime->format($format), $formatter->format($event)); + } +} diff --git a/test/Formatter/ErrorHandlerTest.php b/test/Formatter/ErrorHandlerTest.php new file mode 100644 index 00000000..829b91ac --- /dev/null +++ b/test/Formatter/ErrorHandlerTest.php @@ -0,0 +1,53 @@ + $date, + 'message' => 'test', + 'priority' => 1, + 'priorityName' => 'CRIT', + 'extra' => array ( + 'errno' => 1, + 'file' => 'test.php', + 'line' => 1 + ) + ); + $formatter = new ErrorHandler(); + $output = $formatter->format($event); + + $this->assertEquals($date->format('c') . ' CRIT (1) test (errno 1) in test.php on line 1', $output); + } + + public function testSetDateTimeFormat() + { + $formatter = new ErrorHandler(); + + $this->assertEquals('c', $formatter->getDateTimeFormat()); + $this->assertSame($formatter, $formatter->setDateTimeFormat('r')); + $this->assertEquals('r', $formatter->getDateTimeFormat()); + } +} diff --git a/test/Formatter/ExceptionHandlerTest.php b/test/Formatter/ExceptionHandlerTest.php new file mode 100644 index 00000000..20cae5bc --- /dev/null +++ b/test/Formatter/ExceptionHandlerTest.php @@ -0,0 +1,120 @@ + $date, + 'message' => 'test', + 'priority' => 1, + 'priorityName' => 'CRIT', + 'extra' => array( + 'file' => 'test.php', + 'line' => 1, + 'trace' => array( + array( + 'file' => 'test.php', + 'line' => 1, + 'function' => 'test', + 'class' => 'Test', + 'type' => '::', + 'args' => array(1) + ), + array( + 'file' => 'test.php', + 'line' => 2, + 'function' => 'test', + 'class' => 'Test', + 'type' => '::', + 'args' => array(1) + ) + ) + ) + ); + + // The formatter ends with unix style line endings so make sure we expect that + // output as well: + $expected = $date->format('c') . " CRIT (1) test in test.php on line 1\n"; + $expected .= "[Trace]\n"; + $expected .= "File : test.php\n"; + $expected .= "Line : 1\n"; + $expected .= "Func : test\n"; + $expected .= "Class : Test\n"; + $expected .= "Type : static\n"; + $expected .= "Args : Array\n"; + $expected .= "(\n"; + $expected .= " [0] => 1\n"; + $expected .= ")\n\n"; + $expected .= "File : test.php\n"; + $expected .= "Line : 2\n"; + $expected .= "Func : test\n"; + $expected .= "Class : Test\n"; + $expected .= "Type : static\n"; + $expected .= "Args : Array\n"; + $expected .= "(\n"; + $expected .= " [0] => 1\n"; + $expected .= ")\n\n"; + + $formatter = new ExceptionHandler(); + $output = $formatter->format($event); + + $this->assertEquals($expected, $output); + } + + /** + * @dataProvider provideDateTimeFormats + */ + public function testSetDateTimeFormat($dateTimeFormat) + { + $date = new DateTime(); + + $event = array( + 'timestamp' => $date, + 'message' => 'test', + 'priority' => 1, + 'priorityName' => 'CRIT', + 'extra' => array( + 'file' => 'test.php', + 'line' => 1, + ), + ); + + $expected = $date->format($dateTimeFormat) . ' CRIT (1) test in test.php on line 1'; + + $formatter = new ExceptionHandler(); + + $this->assertSame($formatter, $formatter->setDateTimeFormat($dateTimeFormat)); + $this->assertEquals($dateTimeFormat, $formatter->getDateTimeFormat()); + $this->assertEquals($expected, $formatter->format($event)); + } + + public function provideDateTimeFormats() + { + return array( + array('r'), + array('U'), + ); + } +} diff --git a/test/Formatter/FirePhpTest.php b/test/Formatter/FirePhpTest.php new file mode 100644 index 00000000..9fef4bf5 --- /dev/null +++ b/test/Formatter/FirePhpTest.php @@ -0,0 +1,54 @@ + 'foo' ); + + $f = new FirePhp(); + $line = $f->format($fields); + + $this->assertContains($fields['message'], $line); + } + + public function testSetDateTimeFormatDoesNothing() + { + $formatter = new FirePhp(); + + $this->assertEquals('', $formatter->getDateTimeFormat()); + $this->assertSame($formatter, $formatter->setDateTimeFormat('r')); + $this->assertEquals('', $formatter->getDateTimeFormat()); + } +} diff --git a/test/Formatter/SimpleTest.php b/test/Formatter/SimpleTest.php new file mode 100644 index 00000000..18fe0f62 --- /dev/null +++ b/test/Formatter/SimpleTest.php @@ -0,0 +1,111 @@ +setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'must be a string'); + new Simple(1); + } + + public function testDefaultFormat() + { + $date = new DateTime('2012-08-28T18:15:00Z'); + $fields = array( + 'timestamp' => $date, + 'message' => 'foo', + 'priority' => 42, + 'priorityName' => 'bar', + 'extra' => array() + ); + + $outputExpected = '2012-08-28T18:15:00+00:00 bar (42): foo'; + $formatter = new Simple(); + + $this->assertEquals($outputExpected, $formatter->format($fields)); + } + + /** + * @dataProvider provideDateTimeFormats + */ + public function testCustomDateTimeFormat($dateTimeFormat) + { + $date = new DateTime(); + $event = array('timestamp' => $date); + $formatter = new Simple('%timestamp%', $dateTimeFormat); + + $this->assertEquals($date->format($dateTimeFormat), $formatter->format($event)); + } + + /** + * @dataProvider provideDateTimeFormats + */ + public function testSetDateTimeFormat($dateTimeFormat) + { + $date = new DateTime(); + $event = array('timestamp' => $date); + $formatter = new Simple('%timestamp%'); + + $this->assertSame($formatter, $formatter->setDateTimeFormat($dateTimeFormat)); + $this->assertEquals($dateTimeFormat, $formatter->getDateTimeFormat()); + $this->assertEquals($date->format($dateTimeFormat), $formatter->format($event)); + } + + public function provideDateTimeFormats() + { + return array( + array('r'), + array('U'), + ); + } + + /** + * @group ZF-10427 + */ + public function testDefaultFormatShouldDisplayExtraInformations() + { + $message = 'custom message'; + $exception = new RuntimeException($message); + $event = array( + 'timestamp' => new DateTime(), + 'message' => 'Application error', + 'priority' => 2, + 'priorityName' => 'CRIT', + 'extra' => array($exception), + ); + + $formatter = new Simple(); + $output = $formatter->format($event); + + $this->assertContains($message, $output); + } + + public function testAllowsSpecifyingFormatAsConstructorArgument() + { + $format = '[%timestamp%] %message%'; + $formatter = new Simple($format); + $this->assertEquals($format, $formatter->format(array())); + } +} diff --git a/test/Formatter/XmlTest.php b/test/Formatter/XmlTest.php new file mode 100644 index 00000000..660aeaee --- /dev/null +++ b/test/Formatter/XmlTest.php @@ -0,0 +1,200 @@ +format(array('timestamp' => $date, 'message' => 'foo', 'priority' => 42)); + + $this->assertContains($date->format('c'), $line); + $this->assertContains('foo', $line); + $this->assertContains((string)42, $line); + } + + public function testConfiguringElementMapping() + { + $f = new XmlFormatter('log', array('foo' => 'bar')); + $line = $f->format(array('bar' => 'baz')); + $this->assertContains('baz', $line); + } + + /** + * @dataProvider provideDateTimeFormats + */ + public function testConfiguringDateTimeFormat($dateTimeFormat) + { + $date = new DateTime(); + $f = new XmlFormatter('log', null, 'UTF-8', $dateTimeFormat); + $this->assertContains($date->format($dateTimeFormat), $f->format(array('timestamp' => $date))); + } + + /** + * @dataProvider provideDateTimeFormats + */ + public function testSetDateTimeFormat($dateTimeFormat) + { + $date = new DateTime(); + $f = new XmlFormatter(); + $this->assertSame($f, $f->setDateTimeFormat($dateTimeFormat)); + $this->assertContains($dateTimeFormat, $f->getDateTimeFormat()); + $this->assertContains($date->format($dateTimeFormat), $f->format(array('timestamp' => $date))); + } + + public function provideDateTimeFormats() + { + return array( + array('r'), + array('U'), + ); + } + + public function testXmlDeclarationIsStripped() + { + $f = new XmlFormatter(); + $line = $f->format(array('message' => 'foo', 'priority' => 42)); + + $this->assertNotContains('<\?xml version=', $line); + } + + public function testXmlValidates() + { + $f = new XmlFormatter(); + $line = $f->format(array('message' => 'foo', 'priority' => 42)); + + $sxml = @simplexml_load_string($line); + $this->assertInstanceOf('SimpleXMLElement', $sxml, 'Formatted XML is invalid'); + } + + /** + * @group ZF-2062 + * @group ZF-4190 + */ + public function testHtmlSpecialCharsInMessageGetEscapedForValidXml() + { + $f = new XmlFormatter(); + $line = $f->format(array('message' => '&key1=value1&key2=value2', 'priority' => 42)); + + $this->assertContains("&", $line); + $this->assertTrue(substr_count($line, "&") == 2); + } + + /** + * @group ZF-2062 + * @group ZF-4190 + */ + public function testFixingBrokenCharsSoXmlIsValid() + { + $f = new XmlFormatter(); + $line = $f->format(array('message' => '&', 'priority' => 42)); + + $this->assertContains('&amp', $line); + } + + public function testConstructorWithArray() + { + $date = new DateTime(); + $options = array( + 'rootElement' => 'log', + 'elementMap' => array( + 'date' => 'timestamp', + 'word' => 'message', + 'priority' => 'priority' + ), + 'dateTimeFormat' => 'r', + ); + $event = array( + 'timestamp' => $date, + 'message' => 'tottakai', + 'priority' => 4 + ); + $expected = sprintf('%stottakai4', $date->format('r')); + + $formatter = new XmlFormatter($options); + $output = $formatter->format($event); + $this->assertContains($expected, $output); + $this->assertEquals('UTF-8', $formatter->getEncoding()); + } + + /** + * @group ZF-11161 + */ + public function testNonScalarValuesAreExcludedFromFormattedString() + { + $options = array( + 'rootElement' => 'log' + ); + $event = array( + 'message' => 'tottakai', + 'priority' => 4, + 'context' => array('test'=>'one'), + 'reference' => new XmlFormatter() + ); + $expected = 'tottakai4'; + + $formatter = new XmlFormatter($options); + $output = $formatter->format($event); + $this->assertContains($expected, $output); + } + + /** + * @group ZF-11161 + */ + public function testObjectsWithStringSerializationAreIncludedInFormattedString() + { + $options = array( + 'rootElement' => 'log' + ); + $event = array( + 'message' => 'tottakai', + 'priority' => 4, + 'context' => array('test'=>'one'), + 'reference' => new SerializableObject() + ); + $expected = 'tottakai4ZendTest\Log\TestAsset\SerializableObject'; + + $formatter = new XmlFormatter($options); + $output = $formatter->format($event); + $this->assertContains($expected, $output); + } + + /** + * @group ZF2-453 + */ + public function testFormatWillRemoveExtraEmptyArrayFromEvent() + { + $formatter = new XmlFormatter; + $d = new DateTime('2001-01-01T12:00:00-06:00'); + $event = array( + 'timestamp' => $d, + 'message' => 'test', + 'priority' => 1, + 'priorityName' => 'CRIT', + 'extra' => array() + ); + $expected = '2001-01-01T12:00:00-06:00test1CRIT'; + $expected .= PHP_EOL . PHP_EOL; + $this->assertEquals($expected, $formatter->format($event)); + } +} diff --git a/test/LoggerTest.php b/test/LoggerTest.php new file mode 100644 index 00000000..9343c885 --- /dev/null +++ b/test/LoggerTest.php @@ -0,0 +1,262 @@ +logger = new Logger; + } + + public function testUsesWriterPluginManagerByDefault() + { + $this->assertInstanceOf('Zend\Log\WriterPluginManager', $this->logger->getWriterPluginManager()); + } + + public function testPassingValidStringClassToSetPluginManager() + { + $this->logger->setWriterPluginManager('Zend\Log\WriterPluginManager'); + $this->assertInstanceOf('Zend\Log\WriterPluginManager', $this->logger->getWriterPluginManager()); + } + + public static function provideInvalidClasses() + { + return array( + array('stdClass'), + array(new \stdClass()), + ); + } + + /** + * @dataProvider provideInvalidClasses + */ + public function testPassingInvalidArgumentToSetPluginManagerRaisesException($plugins) + { + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException'); + $this->logger->setWriterPluginManager($plugins); + } + + public function testPassingShortNameToPluginReturnsWriterByThatName() + { + $writer = $this->logger->writerPlugin('mock'); + $this->assertInstanceOf('Zend\Log\Writer\Mock', $writer); + } + + public function testPassWriterAsString() + { + $this->logger->addWriter('mock'); + $writers = $this->logger->getWriters(); + $this->assertInstanceOf('Zend\Stdlib\SplPriorityQueue', $writers); + } + + /** + * @dataProvider provideInvalidClasses + */ + public function testPassingInvalidArgumentToAddWriterRaisesException($writer) + { + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'must implement'); + $this->logger->addWriter($writer); + } + + public function testEmptyWriter() + { + $this->setExpectedException('Zend\Log\Exception\RuntimeException', 'No log writer specified'); + $this->logger->log(Logger::INFO, 'test'); + } + + public function testSetWriters() + { + $writer1 = $this->logger->writerPlugin('mock'); + $writer2 = $this->logger->writerPlugin('null'); + $writers = new SplPriorityQueue(); + $writers->insert($writer1, 1); + $writers->insert($writer2, 2); + $this->logger->setWriters($writers); + + $writers = $this->logger->getWriters(); + $this->assertInstanceOf('Zend\Stdlib\SplPriorityQueue', $writers); + $writer = $writers->extract(); + $this->assertTrue($writer instanceof \Zend\Log\Writer\Null); + $writer = $writers->extract(); + $this->assertTrue($writer instanceof \Zend\Log\Writer\Mock); + } + + public function testAddWriterWithPriority() + { + $writer1 = $this->logger->writerPlugin('mock'); + $this->logger->addWriter($writer1,1); + $writer2 = $this->logger->writerPlugin('null'); + $this->logger->addWriter($writer2,2); + $writers = $this->logger->getWriters(); + + $this->assertInstanceOf('Zend\Stdlib\SplPriorityQueue', $writers); + $writer = $writers->extract(); + $this->assertTrue($writer instanceof \Zend\Log\Writer\Null); + $writer = $writers->extract(); + $this->assertTrue($writer instanceof \Zend\Log\Writer\Mock); + + } + + public function testAddWithSamePriority() + { + $writer1 = $this->logger->writerPlugin('mock'); + $this->logger->addWriter($writer1,1); + $writer2 = $this->logger->writerPlugin('null'); + $this->logger->addWriter($writer2,1); + $writers = $this->logger->getWriters(); + + $this->assertInstanceOf('Zend\Stdlib\SplPriorityQueue', $writers); + $writer = $writers->extract(); + $this->assertTrue($writer instanceof \Zend\Log\Writer\Mock); + $writer = $writers->extract(); + $this->assertTrue($writer instanceof \Zend\Log\Writer\Null); + } + + public function testLogging() + { + $writer = new MockWriter; + $this->logger->addWriter($writer); + $this->logger->log(Logger::INFO, 'tottakai'); + + $this->assertEquals(count($writer->events), 1); + $this->assertContains('tottakai', $writer->events[0]['message']); + } + + public function testLoggingArray() + { + $writer = new MockWriter; + $this->logger->addWriter($writer); + $this->logger->log(Logger::INFO, array('test')); + + $this->assertEquals(count($writer->events), 1); + $this->assertContains('test', $writer->events[0]['message']); + } + + public function testAddFilter() + { + $writer = new MockWriter; + $filter = new MockFilter; + $writer->addFilter($filter); + $this->logger->addWriter($writer); + $this->logger->log(Logger::INFO, array('test')); + + $this->assertEquals(count($filter->events), 1); + $this->assertContains('test', $filter->events[0]['message']); + } + + public function testAddFilterByName() + { + $writer = new MockWriter; + $writer->addFilter('mock'); + $this->logger->addWriter($writer); + $this->logger->log(Logger::INFO, array('test')); + + $this->assertEquals(count($writer->events), 1); + $this->assertContains('test', $writer->events[0]['message']); + } + + /** + * provideTestFilters + */ + public function provideTestFilters() + { + return array( + array('priority', array('priority' => Logger::INFO)), + array('regex', array( 'regex' => '/[0-9]+/' )), + array('validator', array('validator' => new DigitsFilter)), + ); + } + + /** + * @dataProvider provideTestFilters + */ + public function testAddFilterByNameWithParams($filter, $options) + { + $writer = new MockWriter; + $writer->addFilter($filter, $options); + $this->logger->addWriter($writer); + + $this->logger->log(Logger::INFO, '123'); + $this->assertEquals(count($writer->events), 1); + $this->assertContains('123', $writer->events[0]['message']); + } + + public static function provideAttributes() + { + return array( + array(array()), + array(array('user' => 'foo', 'ip' => '127.0.0.1')), + array(new \ArrayObject(array('id' => 42))), + ); + } + + /** + * @dataProvider provideAttributes + */ + public function testLoggingCustomAttributesForUserContext($extra) + { + $writer = new MockWriter; + $this->logger->addWriter($writer); + $this->logger->log(Logger::ERR, 'tottakai', $extra); + + $this->assertEquals(count($writer->events), 1); + $this->assertInternalType('array', $writer->events[0]['extra']); + $this->assertEquals(count($writer->events[0]['extra']), count($extra)); + } + + public static function provideInvalidArguments() + { + return array( + array(new \stdClass(), array('valid')), + array('valid', null), + array('valid', true), + array('valid', 10), + array('valid', 'invalid'), + array('valid', new \stdClass()), + ); + } + + /** + * @dataProvider provideInvalidArguments + */ + public function testPassingInvalidArgumentToLogRaisesException($message, $extra) + { + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException'); + $this->logger->log(Logger::ERR, $message, $extra); + } + + public function testRegisterErrorHandler() + { + $writer = new MockWriter; + $this->logger->addWriter($writer); + + $this->assertTrue(Logger::registerErrorHandler($this->logger)); + // check for single error handler instance + $this->assertFalse(Logger::registerErrorHandler($this->logger)); + // generate a warning + echo $test; + Logger::unregisterErrorHandler(); + $this->assertEquals($writer->events[0]['message'], 'Undefined variable: test'); + } +} diff --git a/test/TestAsset/ConcreteWriter.php b/test/TestAsset/ConcreteWriter.php new file mode 100644 index 00000000..bb4644da --- /dev/null +++ b/test/TestAsset/ConcreteWriter.php @@ -0,0 +1,20 @@ +facility; + } +} diff --git a/test/TestAsset/MockDbAdapter.php b/test/TestAsset/MockDbAdapter.php new file mode 100644 index 00000000..6f8eec29 --- /dev/null +++ b/test/TestAsset/MockDbAdapter.php @@ -0,0 +1,38 @@ +calls[$method][] = $params; + } + + public function __construct() + { + $this->platform = new MockDbPlatform; + $this->driver = new MockDbDriver; + + } + public function query($sql, $parametersOrQueryMode = DbAdapter::QUERY_MODE_PREPARE) + { + $this->calls[__FUNCTION__][] = $sql; + return $this; + } +} diff --git a/test/TestAsset/MockDbDriver.php b/test/TestAsset/MockDbDriver.php new file mode 100644 index 00000000..56c667f7 --- /dev/null +++ b/test/TestAsset/MockDbDriver.php @@ -0,0 +1,21 @@ +calls[$method][] = $params; + } + +} diff --git a/test/TestAsset/MockDbPlatform.php b/test/TestAsset/MockDbPlatform.php new file mode 100644 index 00000000..a0d9415c --- /dev/null +++ b/test/TestAsset/MockDbPlatform.php @@ -0,0 +1,21 @@ +calls[$method][] = $params; + } + +} diff --git a/test/TestAsset/MockFirePhp.php b/test/TestAsset/MockFirePhp.php new file mode 100644 index 00000000..41b4933f --- /dev/null +++ b/test/TestAsset/MockFirePhp.php @@ -0,0 +1,46 @@ +enabled = $enabled; + } + + public function getEnabled() + { + return $this->enabled; + } + + public function error($line) + { + $this->calls['error'][] = $line; + } + + public function warn($line) + { + $this->calls['warn'][] = $line; + } + + public function info($line) + { + $this->calls['info'][] = $line; + } + + public function trace($line) + { + $this->calls['trace'][] = $line; + } + + public function log($line) + { + $this->calls['log'][] = $line; + } +} diff --git a/test/TestAsset/SerializableObject.php b/test/TestAsset/SerializableObject.php new file mode 100644 index 00000000..da5e2b9a --- /dev/null +++ b/test/TestAsset/SerializableObject.php @@ -0,0 +1,19 @@ +_writer = new ConcreteWriter(); + } + + /** + * @group ZF-6085 + */ + public function testSetFormatter() + { + $this->_writer->setFormatter(new SimpleFormatter()); + $this->setExpectedException('PHPUnit_Framework_Error'); + $this->_writer->setFormatter(new \StdClass()); + } + + public function testAddFilter() + { + $this->_writer->addFilter(1); + $this->_writer->addFilter(new RegexFilter('/mess/')); + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException'); + $this->_writer->addFilter(new \StdClass()); + } + + public function testAddMockFilterByName() + { + $instance = $this->_writer->addFilter('mock'); + $this->assertTrue($instance instanceof ConcreteWriter); + } + + public function testAddRegexFilterWithParamsByName() + { + $instance = $this->_writer->addFilter('regex', array( 'regex' => '/mess/' )); + $this->assertTrue($instance instanceof ConcreteWriter); + } + + /** + * @group ZF-8953 + */ + public function testFluentInterface() + { + $instance = $this->_writer->addFilter(1) + ->setFormatter(new SimpleFormatter()); + + $this->assertTrue($instance instanceof ConcreteWriter); + } +} diff --git a/test/Writer/DbTest.php b/test/Writer/DbTest.php new file mode 100644 index 00000000..65d68230 --- /dev/null +++ b/test/Writer/DbTest.php @@ -0,0 +1,241 @@ +tableName = 'db-table-name'; + + $this->db = new MockDbAdapter(); + $this->writer = new DbWriter($this->db, $this->tableName); + } + + public function testNotPassingTableNameToConstructorThrowsException() + { + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'table name'); + $writer = new DbWriter($this->db); + } + + public function testNotPassingDbToConstructorThrowsException() + { + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'Adapter'); + $writer = new DbWriter(array()); + } + + public function testPassingTableNameAsArgIsOK() + { + $options = array( + 'db' => $this->db, + 'table' => $this->tableName, + ); + $writer = new DbWriter($options); + $this->assertInstanceOf('Zend\Log\Writer\Db', $writer); + $this->assertAttributeEquals($this->tableName, 'tableName', $writer); + } + + public function testWriteWithDefaults() + { + // log to the mock db adapter + $fields = array( + 'message' => 'foo', + 'priority' => 42 + ); + + $this->writer->write($fields); + + // insert should be called once... + $this->assertContains('query', array_keys($this->db->calls)); + $this->assertEquals(1, count($this->db->calls['query'])); + $this->assertContains('execute', array_keys($this->db->calls)); + $this->assertEquals(1, count($this->db->calls['execute'])); + $this->assertEquals(array($fields), $this->db->calls['execute'][0]); + } + + public function testWriteWithDefaultsUsingArray() + { + // log to the mock db adapter + $message = 'message-to-log'; + $priority = 2; + $events = array( + 'file' => 'test', + 'line' => 1 + ); + $this->writer->write(array( + 'message' => $message, + 'priority' => $priority, + 'events' => $events + )); + $this->assertContains('query', array_keys($this->db->calls)); + $this->assertEquals(1, count($this->db->calls['query'])); + + $binds = array( + 'message' => $message, + 'priority' => $priority, + 'events_line' => $events['line'], + 'events_file' => $events['file'] + ); + $this->assertEquals(array($binds), $this->db->calls['execute'][0]); + } + + public function testWriteWithDefaultsUsingArrayAndSeparator() + { + $this->writer = new DbWriter($this->db, $this->tableName, null, '-'); + + // log to the mock db adapter + $message = 'message-to-log'; + $priority = 2; + $events = array( + 'file' => 'test', + 'line' => 1 + ); + $this->writer->write(array( + 'message' => $message, + 'priority' => $priority, + 'events' => $events + )); + $this->assertContains('query', array_keys($this->db->calls)); + $this->assertEquals(1, count($this->db->calls['query'])); + + $binds = array( + 'message' => $message, + 'priority' => $priority, + 'events-line' => $events['line'], + 'events-file' => $events['file'] + ); + $this->assertEquals(array($binds), $this->db->calls['execute'][0]); + } + + public function testWriteUsesOptionalCustomColumnNames() + { + $this->writer = new DbWriter($this->db, $this->tableName, array( + 'message' => 'new-message-field' , + 'priority' => 'new-priority-field' + )); + + // log to the mock db adapter + $message = 'message-to-log'; + $priority = 2; + $this->writer->write(array( + 'message' => $message, + 'priority' => $priority + )); + + // insert should be called once... + $this->assertContains('query', array_keys($this->db->calls)); + $this->assertEquals(1, count($this->db->calls['query'])); + + // ...with the correct table and binds for the database + $binds = array( + 'new-message-field' => $message, + 'new-priority-field' => $priority + ); + $this->assertEquals(array($binds), $this->db->calls['execute'][0]); + } + + public function testWriteUsesParamsWithArray() + { + $this->writer = new DbWriter($this->db, $this->tableName, array( + 'message' => 'new-message-field' , + 'priority' => 'new-priority-field', + 'events' => array( + 'line' => 'new-line', + 'file' => 'new-file' + ) + )); + + // log to the mock db adapter + $message = 'message-to-log'; + $priority = 2; + $events = array( + 'file' => 'test', + 'line' => 1 + ); + $this->writer->write(array( + 'message' => $message, + 'priority' => $priority, + 'events' => $events + )); + $this->assertContains('query', array_keys($this->db->calls)); + $this->assertEquals(1, count($this->db->calls['query'])); + // ...with the correct table and binds for the database + $binds = array( + 'new-message-field' => $message, + 'new-priority-field' => $priority, + 'new-line' => $events['line'], + 'new-file' => $events['file'] + ); + $this->assertEquals(array($binds), $this->db->calls['execute'][0]); + } + + public function testShutdownRemovesReferenceToDatabaseInstance() + { + $this->writer->write(array('message' => 'this should not fail')); + $this->writer->shutdown(); + + $this->setExpectedException('Zend\Log\Exception\RuntimeException', 'Database adapter is null'); + $this->writer->write(array('message' => 'this should fail')); + } + + /** + * @group ZF-10089 + */ + public function testThrowStrictSetFormatter() + { + $this->setExpectedException('PHPUnit_Framework_Error'); + $this->writer->setFormatter(new \StdClass()); + } + + public function testWriteDateTimeAsTimestamp() + { + $date = new DateTime(); + $event = array('timestamp'=> $date); + $this->writer->write($event); + + $this->assertContains('query', array_keys($this->db->calls)); + $this->assertEquals(1, count($this->db->calls['query'])); + + $this->assertEquals(array(array( + 'timestamp' => $date->format(FormatterInterface::DEFAULT_DATETIME_FORMAT) + )), $this->db->calls['execute'][0]); + } + + public function testWriteDateTimeAsExtraValue() + { + $date = new DateTime(); + $event = array( + 'extra'=> array( + 'request_time' => $date + ) + ); + $this->writer->write($event); + + $this->assertContains('query', array_keys($this->db->calls)); + $this->assertEquals(1, count($this->db->calls['query'])); + + $this->assertEquals(array(array( + 'extra_request_time' => $date->format(FormatterInterface::DEFAULT_DATETIME_FORMAT) + )), $this->db->calls['execute'][0]); + } +} diff --git a/test/Writer/FirePhpTest.php b/test/Writer/FirePhpTest.php new file mode 100644 index 00000000..6225f058 --- /dev/null +++ b/test/Writer/FirePhpTest.php @@ -0,0 +1,91 @@ +firephp = new MockFirePhp(); + + } + /** + * Test get FirePhp + */ + public function testGetFirePhp() + { + $writer = new FirePhp($this->firephp); + $this->assertTrue($writer->getFirePhp() instanceof FirePhpInterface); + } + /** + * Test set firephp + */ + public function testSetFirePhp() + { + $writer = new FirePhp($this->firephp); + $firephp2 = new MockFirePhp(); + + $writer->setFirePhp($firephp2); + $this->assertTrue($writer->getFirePhp() instanceof FirePhpInterface); + $this->assertEquals($firephp2, $writer->getFirePhp()); + } + /** + * Test write + */ + public function testWrite() + { + $writer = new FirePhp($this->firephp); + $writer->write(array( + 'message' => 'my msg', + 'priority' => Logger::DEBUG + )); + $this->assertEquals('my msg', $this->firephp->calls['trace'][0]); + } + /** + * Test write with FirePhp disabled + */ + public function testWriteDisabled() + { + $firephp = new MockFirePhp(false); + $writer = new FirePhp($firephp); + $writer->write(array( + 'message' => 'my msg', + 'priority' => Logger::DEBUG + )); + $this->assertTrue(empty($this->firephp->calls)); + } +} diff --git a/test/Writer/MailTest.php b/test/Writer/MailTest.php new file mode 100644 index 00000000..cc01fcb6 --- /dev/null +++ b/test/Writer/MailTest.php @@ -0,0 +1,87 @@ + __DIR__, + 'callback' => function (Transport\File $transport) { + return MailTest::FILENAME; + }, + )); + $transport->setOptions($options); + + $this->writer = new MailWriter($message, $transport); + $this->log = new Logger(); + $this->log->addWriter($this->writer); + } + + protected function tearDown() + { + @unlink(__DIR__. '/' . self::FILENAME); + } + + /** + * Tests normal logging, but with multiple messages for a level. + * + * @return void + */ + public function testNormalLoggingMultiplePerLevel() + { + $this->log->info('an info message'); + $this->log->info('a second info message'); + unset($this->log); + + $contents = file_get_contents(__DIR__ . '/' . self::FILENAME); + $this->assertContains('an info message', $contents); + $this->assertContains('a second info message', $contents); + } + + public function testSetSubjectPrependText() + { + $this->writer->setSubjectPrependText('test'); + + $this->log->info('an info message'); + $this->log->info('a second info message'); + unset($this->log); + + $contents = file_get_contents(__DIR__ . '/' . self::FILENAME); + $this->assertContains('an info message', $contents); + $this->assertContains('Subject: test', $contents); + } +} diff --git a/test/Writer/MockTest.php b/test/Writer/MockTest.php new file mode 100644 index 00000000..f8a0f778 --- /dev/null +++ b/test/Writer/MockTest.php @@ -0,0 +1,33 @@ +assertSame(array(), $writer->events); + + $fields = array('foo' => 'bar'); + $writer->write($fields); + $this->assertSame(array($fields), $writer->events); + } +} diff --git a/test/Writer/MongoDBTest.php b/test/Writer/MongoDBTest.php new file mode 100644 index 00000000..1d2e3416 --- /dev/null +++ b/test/Writer/MongoDBTest.php @@ -0,0 +1,102 @@ +markTestSkipped('The mongo PHP extension is not available'); + } + + $this->database = 'zf2_test'; + $this->collection = 'logs'; + + $this->mongo = $this->getMockBuilder('Mongo') + ->disableOriginalConstructor() + ->setMethods(array('selectCollection')) + ->getMock(); + + $this->mongoCollection = $this->getMockBuilder('MongoCollection') + ->disableOriginalConstructor() + ->setMethods(array('save')) + ->getMock(); + + $this->mongo->expects($this->any()) + ->method('selectCollection') + ->with($this->database, $this->collection) + ->will($this->returnValue($this->mongoCollection)); + } + + /** + * @expectedException Zend\Log\Exception\InvalidArgumentException + */ + public function testFormattingIsNotSupported() + { + $writer = new MongoDBWriter($this->mongo, $this->database, $this->collection); + + $writer->setFormatter($this->getMock('Zend\Log\Formatter\FormatterInterface')); + } + + public function testWriteWithDefaultSaveOptions() + { + $event = array('message'=> 'foo', 'priority' => 42); + + $this->mongoCollection->expects($this->once()) + ->method('save') + ->with($event, array()); + + $writer = new MongoDBWriter($this->mongo, $this->database, $this->collection); + + $writer->write($event); + } + + public function testWriteWithCustomSaveOptions() + { + $event = array('message' => 'foo', 'priority' => 42); + $saveOptions = array('safe' => false, 'fsync' => false, 'timeout' => 100); + + $this->mongoCollection->expects($this->once()) + ->method('save') + ->with($event, $saveOptions); + + $writer = new MongoDBWriter($this->mongo, $this->database, $this->collection, $saveOptions); + + $writer->write($event); + } + + public function testWriteConvertsDateTimeToMongoDate() + { + $date = new DateTime(); + $event = array('timestamp'=> $date); + + $this->mongoCollection->expects($this->once()) + ->method('save') + ->with($this->contains(new MongoDate($date->getTimestamp()), false)); + + $writer = new MongoDBWriter($this->mongo, $this->database, $this->collection); + + $writer->write($event); + } +} diff --git a/test/Writer/NullTest.php b/test/Writer/NullTest.php new file mode 100644 index 00000000..07dfaebd --- /dev/null +++ b/test/Writer/NullTest.php @@ -0,0 +1,29 @@ +write(array('message' => 'foo', 'priority' => 42)); + } +} diff --git a/test/Writer/StreamTest.php b/test/Writer/StreamTest.php new file mode 100644 index 00000000..098d5e28 --- /dev/null +++ b/test/Writer/StreamTest.php @@ -0,0 +1,150 @@ +fail(); + } catch (\Exception $e) { + $this->assertInstanceOf('Zend\Log\Exception\InvalidArgumentException', $e); + $this->assertRegExp('/not a stream/i', $e->getMessage()); + } + xml_parser_free($resource); + } + + public function testConstructorWithValidStream() + { + $stream = fopen('php://memory', 'w+'); + new StreamWriter($stream); + } + + public function testConstructorWithValidUrl() + { + new StreamWriter('php://memory'); + } + + public function testConstructorThrowsWhenModeSpecifiedForExistingStream() + { + $stream = fopen('php://memory', 'w+'); + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'existing stream'); + new StreamWriter($stream, 'w+'); + } + + public function testConstructorThrowsWhenStreamCannotBeOpened() + { + $this->setExpectedException('Zend\Log\Exception\RuntimeException', 'cannot be opened'); + new StreamWriter(''); + } + + public function testWrite() + { + $stream = fopen('php://memory', 'w+'); + $fields = array('message' => 'message-to-log'); + + $writer = new StreamWriter($stream); + $writer->write($fields); + + rewind($stream); + $contents = stream_get_contents($stream); + fclose($stream); + + $this->assertContains($fields['message'], $contents); + } + + public function testWriteThrowsWhenStreamWriteFails() + { + $stream = fopen('php://memory', 'w+'); + $writer = new StreamWriter($stream); + fclose($stream); + + $this->setExpectedException('Zend\Log\Exception\RuntimeException', 'Unable to write'); + $writer->write(array('message' => 'foo')); + } + + public function testShutdownClosesStreamResource() + { + $writer = new StreamWriter('php://memory', 'w+'); + $writer->write(array('message' => 'this write should succeed')); + + $writer->shutdown(); + + $this->setExpectedException('Zend\Log\Exception\RuntimeException', 'Unable to write'); + $writer->write(array('message' => 'this write should fail')); + } + + public function testSettingNewFormatter() + { + $stream = fopen('php://memory', 'w+'); + $writer = new StreamWriter($stream); + $expected = 'foo'; + + $formatter = new SimpleFormatter($expected); + $writer->setFormatter($formatter); + + $writer->write(array('bar'=>'baz')); + rewind($stream); + $contents = stream_get_contents($stream); + fclose($stream); + + $this->assertContains($expected, $contents); + } + + public function testAllowSpecifyingLogSeparator() + { + $stream = fopen('php://memory', 'w+'); + $writer = new StreamWriter($stream); + $writer->setLogSeparator('::'); + + $fields = array('message' => 'message1'); + $writer->write($fields); + $fields['message'] = 'message2'; + $writer->write($fields); + + rewind($stream); + $contents = stream_get_contents($stream); + fclose($stream); + + $this->assertRegexp('/message1.*?::.*?message2/', $contents); + $this->assertNotContains(PHP_EOL, $contents); + } + + public function testAllowsSpecifyingLogSeparatorAsConstructorArgument() + { + $writer = new StreamWriter('php://memory', 'w+', '::'); + $this->assertEquals('::', $writer->getLogSeparator()); + } + + public function testAllowsSpecifyingLogSeparatorWithinArrayPassedToConstructor() + { + $options = array( + 'stream' => 'php://memory', + 'mode' => 'w+', + 'log_separator' => '::', + ); + $writer = new StreamWriter($options); + $this->assertEquals('::', $writer->getLogSeparator()); + } +} diff --git a/test/Writer/SyslogTest.php b/test/Writer/SyslogTest.php new file mode 100644 index 00000000..a57a0afe --- /dev/null +++ b/test/Writer/SyslogTest.php @@ -0,0 +1,96 @@ + 'foo', + 'priority' => LOG_NOTICE + ); + $writer = new SyslogWriter(); + $writer->write($fields); + } + + /** + * @group ZF-7603 + */ + public function testThrowExceptionValueNotPresentInFacilities() + { + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'Invalid log facility provided'); + $writer = new SyslogWriter(); + $writer->setFacility(LOG_USER * 1000); + } + + /** + * @group ZF-7603 + */ + public function testThrowExceptionIfFacilityInvalidInWindows() + { + if ('WIN' != strtoupper(substr(PHP_OS, 0, 3))) { + $this->markTestSkipped('Run only in windows'); + } + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'Only LOG_USER is a valid'); + $writer = new SyslogWriter(); + $writer->setFacility(LOG_AUTH); + } + + /** + * @group ZF-8953 + */ + public function testFluentInterface() + { + $writer = new SyslogWriter(); + $instance = $writer->setFacility(LOG_USER) + ->setApplicationName('my_app'); + + $this->assertTrue($instance instanceof SyslogWriter); + } + + /** + * @group ZF-10769 + */ + public function testPastFacilityViaConstructor() + { + $writer = new CustomSyslogWriter(array('facility' => LOG_USER)); + $this->assertEquals(LOG_USER, $writer->getFacility()); + } + + /** + * @group ZF-8382 + */ + public function testWriteWithFormatter() + { + $event = array( + 'message' => 'tottakai', + 'priority' => Logger::ERR + ); + + $writer = new SyslogWriter(); + $formatter = new SimpleFormatter('%message% (this is a test)'); + $writer->setFormatter($formatter); + + $writer->write($event); + } +} diff --git a/test/Writer/ZendMonitorTest.php b/test/Writer/ZendMonitorTest.php new file mode 100644 index 00000000..a1ce8616 --- /dev/null +++ b/test/Writer/ZendMonitorTest.php @@ -0,0 +1,40 @@ +write(array( + 'message' => 'my mess', + 'priority' => 1 + )); + } + + public function testIsEnabled() + { + $writer = new ZendMonitor(); + $this->assertInternalType('boolean', $writer->isEnabled()); + } +} diff --git a/test/WriterPluginManagerTest.php b/test/WriterPluginManagerTest.php new file mode 100644 index 00000000..6ad21b06 --- /dev/null +++ b/test/WriterPluginManagerTest.php @@ -0,0 +1,44 @@ +plugins = new WriterPluginManager(); + } + + public function testRegisteringInvalidWriterRaisesException() + { + $this->setExpectedException('Zend\Log\Exception\InvalidArgumentException', 'must implement'); + $this->plugins->setService('test', $this); + } + + public function testInvokableClassFirephp() + { + $firephp = $this->plugins->get('firephp'); + $this->assertInstanceOf('Zend\Log\Writer\Firephp', $firephp); + } +} diff --git a/test/_files/layout.phtml b/test/_files/layout.phtml new file mode 100644 index 00000000..b1ebac07 --- /dev/null +++ b/test/_files/layout.phtml @@ -0,0 +1 @@ +layout()->events; \ No newline at end of file diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 00000000..a9b7cb72 --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,34 @@ +