10000 [FrameworkBundle] Add a minimalist default PSR-3 logger · symfony/symfony@416e882 · GitHub
[go: up one dir, main page]

Skip to content

Commit 416e882

Browse files
committed
[FrameworkBundle] Add a minimalist default PSR-3 logger
1 parent 0a9d46d commit 416e882

File tree

5 files changed

+309
-0
lines changed

5 files changed

+309
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand;
2323
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
2424
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
25+
use Symfony\Bundle\FrameworkBundle\Logger\Logger;
2526
use Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader;
2627
use Symfony\Component\Cache\Adapter\AdapterInterface;
2728
use Symfony\Component\Cache\Adapter\ArrayAdapter;
@@ -303,6 +304,10 @@ public function load(array $configs, ContainerBuilder $container)
303304
$loader->load('web_link.xml');
304305
}
305306

307+
$loggerDefinition = $container->register(LoggerInterface::class, Logger::class);
308+
$loggerDefinition->setPublic(false);
309+
$container->setAlias('logger', LoggerInterface::class);
310+
306311
$this->addAnnotatedClassesToCompile(array(
307312
'**\\Controller\\',
308313
'**\\Entity\\',
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Logger;
13+
14+
use Psr\Log\AbstractLogger;
15+
use Psr\Log\InvalidArgumentException;
16+
use Psr\Log\LogLevel;
17+
18+
/**
19+
* Minimalist PSR-3 logger designed to write in stdout, stderr or any other stream.
20+
*
21+
* @author Kévin Dunglas <dunglas@gmail.com>
22+
*
23+
* @see http://www.php-fig.org/psr/psr-3/
24+
*/
25+
final class Logger extends AbstractLogger
26+
{
27+
/**
28+
* @var array
29+
*/
30+
private static $defaultLevelToStream = array(
31+
LogLevel::EMERGENCY => 'php://stderr',
32+
LogLevel::ALERT => 'php://stderr',
33+
LogLevel::CRITICAL => 'php://stderr',
34+
LogLevel::ERROR => 'php://stderr',
35+
LogLevel::WARNING => 'php://stdout',
36+
LogLevel::NOTICE => 'php://stdout',
37+
LogLevel::INFO => 'php://stdout',
38+
LogLevel::DEBUG => false,
39+
);
40+
41+
private $levelToStream;
42+
private $formatPattern;
43+
44+
/**
45+
* @var bool
46+
*/
47+
private $errored = false;
48+
49+
/**
50+
* @param array $levelToStream
51+
* @param string $formatPattern
52+
*/
53+
public function __construct(array $levelToStream = array(), $formatPattern = '[%s] %s')
54+
{
55+
$this->levelToStream = $levelToStream + self::$defaultLevelToStream;
56+
$this->formatPattern = $formatPattern;
57+
}
58+
59+
/**
60+
* {@inheritdoc}
61+
*/
62+
public function log($level, $message, array $context = array())
63+
{
64+
if (!isset($this->levelToStream[$level])) {
65+
throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level));
66+
}
67+
68+
if (!$this->errored) {
69+
$this->errored = in_array($level, array(LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR));
70+
}
71+
72+
if ($this->levelToStream[$level]) {
73+
file_put_contents($this->levelToStream[$level], sprintf($this->formatPattern, $level, $this->interpolate($message, $context)).PHP_EOL, FILE_APPEND);
74+
}
75+
}
76+
77+
/**
78+
* Returns true when any messages have been logged at error levels.
79+
*
80+
* @return bool
81+
*/
82+
public function hasErrored()
83+
{
84+
return $this->errored;
85+
}
86+
87+
/**
88+
* Interpolates context values into the message placeholders.
89+
*
90+
* @author PHP Framework Interoperability Group
91+
*
92+
* @param string $message
93+
* @param array $context
94+
*
95+
* @return string
96+
*/
97+
private function interpolate($message, array $context)
98+
{
99+
// build a replacement array with braces around the context keys
100+
$replace = array();
101+
foreach ($context as $key => $val) {
102+
if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) {
103+
$replace[sprintf('{%s}', $key)] = $val;
104+
}
105+
}
106+
107+
// interpolate replacement values into the message and return
108+
return strtr($message, $replace);
109+
}
110+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Tests\Logger;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Psr\Log\LoggerInterface;
16+
use Psr\Log\LogLevel;
17+
use Symfony\Bundle\FrameworkBundle\Logger\Logger;
18+
19+
/**
20+
* @author Kévin Dunglas <dunglas@gmail.com>
21+
* @author Jordi Boggiano <j.boggiano@seld.be>
22+
*/
23+
class LoggerTest extends TestCase
24+
{
25+
/**
26+
* @var LoggerInterface
27+
*/
28+
private $logger;
29+
30+
/**
31+
* @var string
32+
*/
33+
private $tmpFile;
34+
35+
public function setUp()
36+
{
37+
$this->tmpFile = sys_get_temp_dir().'/log';
38+
$this->logger = new Logger(array(
39+
LogLevel::EMERGENCY => $this->tmpFile,
40+
LogLevel::ALERT => $this->tmpFile,
41+
LogLevel::CRITICAL => $this->tmpFile,
42+
LogLevel::ERROR => $this->tmpFile,
43+
LogLevel::WARNING => $this->tmpFile,
44+
LogLevel::NOTICE => $this->tmpFile,
45+
LogLevel::INFO => $this->tmpFile,
1241 46+
LogLevel::DEBUG => $this->tmpFile,
47+
), '%s %s');
48+
file_put_contents($this->tmpFile, '');
49+
}
50+
51+
/**
52+
* Return the log messages in order.
53+
*
54+
* @return string[]
55+
*/
56+
public function getLogs()
57+
{
58+
return file($this->tmpFile, FILE_IGNORE_NEW_LINES);
59+
}
60+
61+
public function testHasErrored()
62+
{
63+
$logger = $this->logger;
64+
$this->assertFalse($logger->hasErrored());
65+
66+
$logger->warning('foo');
67+
$this->assertFalse($logger->hasErrored());
68+
69+
$logger->error('bar');
70+
$this->assertTrue($logger->hasErrored());
71+
}
72+
73+
public function testImplements()
74+
{
75+
$this->assertInstanceOf(LoggerInterface::class, $this->logger);
76+
}
77+
78+
/**
79+
* @dataProvider provideLevelsAndMessages
80+
*/
81+
public function testLogsAtAllLevels($level, $message)
82+
{
83+
$this->logger->{$level}($message, array('user' => 'Bob'));
84+
$this->logger->log($level, $message, array('user' => 'Bob'));
85+
86+
$expected = array(
87+
"$level message of level $level with context: Bob",
88+
"$level message of level $level with context: Bob",
89+
);
90+
$this->assertEquals($expected, $this->getLogs());
91+
}
92+
93+
public function provideLevelsAndMessages()
94+
{
95+
return array(
96+
LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'),
97+
LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'),
98+
LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'),
99+
LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'),
100+
LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'),
101+
LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'),
102+
LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'),
103+
LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'),
104+
);
105+
}
106+
107+
public function testLogLevelDisabled()
108+
{
109+
$this->logger = new Logger(array(LogLevel::DEBUG => false));
110+
111+
$this->logger->debug('test', array('user' => 'Bob'));
112+
$this->logger->log(LogLevel::DEBUG, 'test', array('user' => 'Bob'));
113+
114+
// Will always be true, but asserts than an exception isn't thrown
115+
$this->assertEquals(array(), $this->getLogs());
116+
}
117+
118+
/**
119+
* @expectedException \Psr\Log\InvalidArgumentException
120+
*/
121+
public function testThrowsOnInvalidLevel()
122+
{
123+
$logger = $this->logger;
124+
$logger->log('invalid level', 'Foo');
125+
}
126+
127+
public function testContextReplacement()
128+
{
129+
$logger = $this->logger;
130+
$logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar'));
131+
132+
$expected = array('info {Message {nothing} Bob Bar a}');
133+
$this->assertEquals($expected, $this->getLogs());
134+
}
135+
136+
public function testObjectCastToString()
137+
{
138+
if (method_exists($this, 'createPartialMock')) {
139+
$dummy = $this->createPartialMock(DummyTest::class, array('__toString'));
140+
} else {
141+
$dummy = $this->getMock(DummyTest::class, array('__toString'));
142+
}
143+
$dummy->expects($this->once())
144+
->method('__toString')
145+
->will($this->returnValue('DUMMY'));
146+
147+
$this->logger->warning($dummy);
148+
149+
$expected = array('warning DUMMY');
150+
$this->assertEquals($expected, $this->getLogs());
151+
}
152+
153+
public function testContextCanContainAnything()
154+
{
155+
$context = array(
156+
'bool' => true,
157+
'null' => null,
158+
'string' => 'Foo',
159+
'int' => 0,
160+
'float' => 0.5,
161+
'nested' => array('with object' => new DummyTest()),
162+
'object' => new \DateTime(),
163+
'resource' => fopen('php://memory', 'r'),
164+
);
165+
166+
$this->logger->warning('Crazy context data', $context);
167+
168+
$expected = array('warning Crazy context data');
169+
$this->assertEquals($expected, $this->getLogs());
170+
}
171+
172+
public function testContextExceptionKeyCanBeExceptionOrOtherValues()
173+
{
174+
$logger = $this->logger;
175+
$logger->warning('Random message', array('exception' => 'oops'));
176+
$logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail')));
177+
178+
$expected = array(
179+
'warning Random message',
180+
'critical Uncaught Exception!',
181+
);
182+
$this->assertEquals($expected, $this->getLogs());
183+
}
184+
}
185+
186+
class DummyTest
187+
{
188+
public function __toString()
189+
{
190+
}
191+
}

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"require": {
1919
"php": "^5.5.9|>=7.0.8",
2020
"ext-xml": "*",
21+
"psr/log": "~1.0",
2122
"symfony/cache": "~3.4|~4.0",
2223
"symfony/class-loader": "~3.2",
2324
"symfony/dependency-injection": "~3.4|~4.0",

src/Symfony/Component/Console/Logger/ConsoleLogger.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ public function log($level, $message, array $context = array())
101101

102102
/**
103103
* Returns true when any messages have been logged at error levels.
104+
*
105+
* @return bool
104106
*/
105107
public function hasErrored()
106108
{

0 commit comments

Comments
 (0)
0