Upstream Seldaek/monolog state of the touched files before 1191965241
This commit is contained in:
commit
2216d4fc24
2 files changed
+956
No files matched your search
@@ -0,0 +1,606 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Monolog package.
|
||||
*
|
||||
* (c) Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Monolog\Formatter;
|
||||
|
||||
use Monolog\Level;
|
||||
use Monolog\LogRecord;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
use Monolog\Test\MonologTestCase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
/**
|
||||
* @covers Monolog\Formatter\RedactingFormatter
|
||||
*/
|
||||
class RedactingFormatterTest extends MonologTestCase
|
||||
{
|
||||
public function testRedactsSensitiveKeysInContextAndExtra()
|
||||
{
|
||||
$captured = null;
|
||||
$formatter = new RedactingFormatter($this->capturingFormatter($captured));
|
||||
|
||||
$formatter->format($this->getRecord(
|
||||
context: ['user' => 'rishi', 'password' => 'super-secret', 'nested' => ['api_key' => 'abc123']],
|
||||
extra: ['token' => 't-456', 'irrelevant' => 'keep'],
|
||||
));
|
||||
|
||||
$this->assertSame('rishi', $captured->context['user']);
|
||||
$this->assertSame('[REDACTED]', $captured->context['password']);
|
||||
$this->assertSame('[REDACTED]', $captured->context['nested']['api_key']);
|
||||
$this->assertSame('[REDACTED]', $captured->extra['token']);
|
||||
$this->assertSame('keep', $captured->extra['irrelevant']);
|
||||
}
|
||||
|
||||
public function testKeyMatchingIsCaseInsensitive()
|
||||
{
|
||||
$captured = null;
|
||||
$formatter = new RedactingFormatter($this->capturingFormatter($captured));
|
||||
|
||||
$formatter->format($this->getRecord(context: ['Password' => 'x', 'AUTHORIZATION' => 'Bearer y']));
|
||||
|
||||
$this->assertSame('[REDACTED]', $captured->context['Password']);
|
||||
$this->assertSame('[REDACTED]', $captured->context['AUTHORIZATION']);
|
||||
}
|
||||
|
||||
public function testRedactsPatternsInFinalOutput()
|
||||
{
|
||||
$formatter = new RedactingFormatter(
|
||||
new LineFormatter('%message% %context%', 'Y-m-d'),
|
||||
sensitiveKeys: [],
|
||||
patterns: ['{Bearer\s+[A-Za-z0-9._-]+}'],
|
||||
);
|
||||
|
||||
$output = $formatter->format($this->getRecord(
|
||||
message: 'Auth header Bearer abc.def-ghi',
|
||||
context: ['header' => 'Bearer zzz999'],
|
||||
));
|
||||
|
||||
$this->assertStringContainsString('Auth header [REDACTED]', $output);
|
||||
$this->assertStringNotContainsString('Bearer', $output);
|
||||
}
|
||||
|
||||
public function testTokenPatternConstantRedactsLongTokens()
|
||||
{
|
||||
$formatter = new RedactingFormatter(
|
||||
new LineFormatter('%message%', 'Y-m-d'),
|
||||
sensitiveKeys: [],
|
||||
patterns: [RedactingFormatter::TOKEN_PATTERN],
|
||||
);
|
||||
|
||||
$output = $formatter->format($this->getRecord(message: 'token is test_0123456789abcdefghijklmnopqrstuvwxyz here'));
|
||||
|
||||
$this->assertStringContainsString('token is [REDACTED] here', $output);
|
||||
}
|
||||
|
||||
public function testSweepsSecretsHiddenInsideNormalizedObjects()
|
||||
{
|
||||
// The secret lives inside an exception that only becomes a string once
|
||||
// the wrapped JsonFormatter normalizes the record. Redacting the record
|
||||
// alone would miss it; running the patterns over the output catches it.
|
||||
$formatter = new RedactingFormatter(
|
||||
new JsonFormatter(),
|
||||
sensitiveKeys: [],
|
||||
patterns: ['{Bearer\s+[A-Za-z0-9._-]+}'],
|
||||
);
|
||||
|
||||
$output = $formatter->format($this->getRecord(
|
||||
context: ['exception' => new \RuntimeException('failed using Bearer abc.def-ghi')],
|
||||
));
|
||||
|
||||
$this->assertIsString($output);
|
||||
$this->assertStringNotContainsString('Bearer abc.def-ghi', $output);
|
||||
$this->assertStringContainsString('[REDACTED]', $output);
|
||||
}
|
||||
|
||||
public function testSweepsArrayReturningFormatters()
|
||||
{
|
||||
$formatter = new RedactingFormatter(
|
||||
new NormalizerFormatter('Y-m-d'),
|
||||
sensitiveKeys: [],
|
||||
patterns: ['{Bearer\s+[A-Za-z0-9._-]+}'],
|
||||
);
|
||||
|
||||
$output = $formatter->format($this->getRecord(message: 'Bearer abc.def-ghi'));
|
||||
|
||||
$this->assertIsArray($output);
|
||||
$this->assertSame('[REDACTED]', $output['message']);
|
||||
}
|
||||
|
||||
public function testFormatBatchRedactsEachRecord()
|
||||
{
|
||||
$formatter = new RedactingFormatter(
|
||||
new LineFormatter('%message% %context%', 'Y-m-d'),
|
||||
patterns: ['{Bearer\s+[A-Za-z0-9._-]+}'],
|
||||
);
|
||||
|
||||
// LineFormatter::formatBatch joins records into a single string
|
||||
$output = $formatter->formatBatch([
|
||||
$this->getRecord(message: 'one Bearer aaa.bbb-ccc', context: ['password' => 'p1']),
|
||||
$this->getRecord(message: 'two', context: ['token' => 't2']),
|
||||
]);
|
||||
|
||||
$this->assertIsString($output);
|
||||
$this->assertStringNotContainsString('Bearer', $output);
|
||||
$this->assertStringNotContainsString('p1', $output);
|
||||
$this->assertStringNotContainsString('t2', $output);
|
||||
$this->assertSame(3, substr_count($output, '[REDACTED]')); // password key, token key, and Bearer pattern
|
||||
}
|
||||
|
||||
public function testFormatBatchWithArrayReturningFormatter()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new NormalizerFormatter('Y-m-d'));
|
||||
|
||||
$output = $formatter->formatBatch([
|
||||
$this->getRecord(context: ['password' => 'p1']),
|
||||
$this->getRecord(context: ['token' => 't2']),
|
||||
]);
|
||||
|
||||
$this->assertIsArray($output);
|
||||
$this->assertSame('[REDACTED]', $output[0]['context']['password']);
|
||||
$this->assertSame('[REDACTED]', $output[1]['context']['token']);
|
||||
}
|
||||
|
||||
public function testThrowsOnInvalidPattern()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
new RedactingFormatter(new LineFormatter(), patterns: ['{invalid']);
|
||||
}
|
||||
|
||||
public function testNoPatternsLeavesOutputUntouched()
|
||||
{
|
||||
$inner = new LineFormatter('%message%', 'Y-m-d');
|
||||
$record = $this->getRecord(message: 'a long token test_0123456789abcdefghijklmnopqrstuvwxyz');
|
||||
|
||||
$formatter = new RedactingFormatter($inner, sensitiveKeys: []);
|
||||
|
||||
$this->assertSame($inner->format($record), $formatter->format($record));
|
||||
}
|
||||
|
||||
public function testCustomMask()
|
||||
{
|
||||
$captured = null;
|
||||
$formatter = new RedactingFormatter($this->capturingFormatter($captured), mask: '***');
|
||||
|
||||
$formatter->format($this->getRecord(context: ['password' => 'x']));
|
||||
|
||||
$this->assertSame('***', $captured->context['password']);
|
||||
}
|
||||
|
||||
public function testRedactsSensitiveParameterValues()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%message% %context%', 'Y-m-d'));
|
||||
|
||||
$output = $formatter->format($this->getRecord(context: ['user' => new SensitiveParamHolder()]));
|
||||
|
||||
$this->assertStringNotContainsString('s3cret-webhook', $output);
|
||||
$this->assertStringContainsString('[REDACTED]', $output);
|
||||
// the non-sensitive parameter is left alone
|
||||
$this->assertStringContainsString('user-1', $output);
|
||||
}
|
||||
|
||||
public function testRedactsSensitiveParameterValuesWithJsonFormatter()
|
||||
{
|
||||
// JsonFormatter hands objects straight to json_encode, so nothing normalizes them
|
||||
$formatter = new RedactingFormatter(new JsonFormatter());
|
||||
|
||||
$output = $formatter->format($this->getRecord(context: ['user' => new SensitiveParamHolder()]));
|
||||
|
||||
$this->assertIsString($output);
|
||||
$this->assertStringNotContainsString('s3cret-webhook', $output);
|
||||
$this->assertStringContainsString('"mySecretUrl":"[REDACTED]"', $output);
|
||||
}
|
||||
|
||||
public function testRedactsNonPublicSensitiveParameterLeakedByToString()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%context%', 'Y-m-d'));
|
||||
|
||||
$output = $formatter->format($this->getRecord(context: ['conn' => new SensitiveDsnHolder()]));
|
||||
|
||||
$this->assertStringNotContainsString('hunter2', $output);
|
||||
$this->assertStringContainsString('Connection([REDACTED])', $output);
|
||||
}
|
||||
|
||||
public function testRedactsNonPromotedSensitiveParameterMatchedByPropertyName()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%context%', 'Y-m-d'));
|
||||
|
||||
$output = $formatter->format($this->getRecord(context: ['sub' => new SensitiveNonPromotedHolder()]));
|
||||
|
||||
$this->assertStringNotContainsString('tok-abcdefghij', $output);
|
||||
$this->assertStringContainsString('[REDACTED]', $output);
|
||||
}
|
||||
|
||||
public function testRedactsSensitiveParameterOnNestedObjectAndInsideArrays()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%context%', 'Y-m-d'));
|
||||
|
||||
$output = $formatter->format($this->getRecord(context: ['list' => [['nested' => new SensitiveNestingHolder()]]]));
|
||||
|
||||
$this->assertStringNotContainsString('s3cret-webhook', $output);
|
||||
$this->assertStringContainsString('[REDACTED]', $output);
|
||||
}
|
||||
|
||||
public function testRedactsSecretAlreadyInterpolatedIntoTheMessage()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%message% %context%', 'Y-m-d'));
|
||||
$processor = new PsrLogMessageProcessor();
|
||||
|
||||
$output = $formatter->format($processor($this->getRecord(
|
||||
message: 'calling api with {token}',
|
||||
context: ['token' => 'tok-supersecret-value'],
|
||||
)));
|
||||
|
||||
$this->assertStringNotContainsString('tok-supersecret-value', $output);
|
||||
$this->assertStringContainsString('calling api with [REDACTED]', $output);
|
||||
}
|
||||
|
||||
public function testRedactsSecretEmbeddedInAnExceptionMessage()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new JsonFormatter());
|
||||
|
||||
$output = $formatter->format($this->getRecord(context: [
|
||||
'user' => new SensitiveParamHolder(),
|
||||
'exception' => new \RuntimeException('could not reach https://example.org/s3cret-webhook'),
|
||||
]));
|
||||
|
||||
$this->assertIsString($output);
|
||||
$this->assertStringNotContainsString('s3cret-webhook', $output);
|
||||
$this->assertStringContainsString('could not reach [REDACTED]', $output);
|
||||
}
|
||||
|
||||
public function testRedactsSecretsNeedingJsonEscaping()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new JsonFormatter(), sensitiveKeys: ['password']);
|
||||
|
||||
$output = $formatter->format($this->getRecord(
|
||||
message: 'login failed for pa"ss\\word-123',
|
||||
context: ['password' => 'pa"ss\\word-123'],
|
||||
));
|
||||
|
||||
$this->assertIsString($output);
|
||||
$this->assertStringNotContainsString('word-123', $output);
|
||||
$this->assertSame(2, substr_count($output, '[REDACTED]')); // masked key + swept message
|
||||
}
|
||||
|
||||
public function testRedactsSensitiveParameterValueObjects()
|
||||
{
|
||||
if (!class_exists(\SensitiveParameterValue::class)) {
|
||||
$this->markTestSkipped('SensitiveParameterValue requires PHP 8.2');
|
||||
}
|
||||
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%message% %context%', 'Y-m-d'));
|
||||
|
||||
$output = $formatter->format($this->getRecord(
|
||||
message: 'token was tok-supersecret-value',
|
||||
context: ['arg' => new \SensitiveParameterValue('tok-supersecret-value')],
|
||||
));
|
||||
|
||||
$this->assertStringNotContainsString('tok-supersecret-value', $output);
|
||||
}
|
||||
|
||||
public function testShortSecretsAreNotSwept()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%message% %context%', 'Y-m-d'));
|
||||
|
||||
$output = $formatter->format($this->getRecord(
|
||||
message: 'user pass logged in',
|
||||
context: ['password' => 'pass'],
|
||||
));
|
||||
|
||||
// the key is still masked, but "pass" is too short to be removed from the message
|
||||
$this->assertStringContainsString('user pass logged in', $output);
|
||||
$this->assertStringContainsString('[REDACTED]', $output);
|
||||
}
|
||||
|
||||
public function testMinSecretLengthIsConfigurable()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%message%', 'Y-m-d'), minSecretLength: 4);
|
||||
|
||||
$output = $formatter->format($this->getRecord(message: 'user pass logged in', context: ['password' => 'pass']));
|
||||
|
||||
$this->assertStringContainsString('user [REDACTED] logged in', $output);
|
||||
}
|
||||
|
||||
public function testSensitiveParameterSupportCanBeDisabled()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%message% %context%', 'Y-m-d'), redactSensitiveParameters: false);
|
||||
|
||||
$output = $formatter->format($this->getRecord(
|
||||
message: 'calling https://example.org/s3cret-webhook',
|
||||
context: ['user' => new SensitiveParamHolder(), 'token' => 'tok-supersecret-value'],
|
||||
));
|
||||
|
||||
$this->assertStringContainsString('calling https://example.org/s3cret-webhook', $output);
|
||||
// sensitive keys are still swept from the output, only the objects are left alone
|
||||
$this->assertStringNotContainsString('tok-supersecret-value', $output);
|
||||
}
|
||||
|
||||
#[DataProvider('provideEdgeCaseObjects')]
|
||||
public function testHandlesObjectsWithoutUsableConstructors(object $object)
|
||||
{
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%context%', 'Y-m-d'));
|
||||
|
||||
$output = $formatter->format($this->getRecord(context: ['edge' => $object]));
|
||||
|
||||
$this->assertIsString($output);
|
||||
}
|
||||
|
||||
public static function provideEdgeCaseObjects(): array
|
||||
{
|
||||
$cyclic = new SensitiveCyclicHolder();
|
||||
$cyclic->next = $cyclic;
|
||||
|
||||
return [
|
||||
'no constructor' => [new \stdClass()],
|
||||
'enum' => [SensitiveTestEnum::One],
|
||||
'anonymous class' => [new class ('anon-secret-value') {
|
||||
public function __construct(#[\SensitiveParameter] public string $secret)
|
||||
{
|
||||
}
|
||||
}],
|
||||
'uninitialized property' => [(new \ReflectionClass(SensitiveNonPromotedHolder::class))->newInstanceWithoutConstructor()],
|
||||
'cyclic graph' => [$cyclic],
|
||||
];
|
||||
}
|
||||
|
||||
public function testCyclicGraphSecretsAreStillFound()
|
||||
{
|
||||
$cyclic = new SensitiveCyclicHolder();
|
||||
$cyclic->next = $cyclic;
|
||||
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%message%', 'Y-m-d'));
|
||||
|
||||
$output = $formatter->format($this->getRecord(message: 'leaked cyclic-secret-value', context: ['obj' => $cyclic]));
|
||||
|
||||
$this->assertStringContainsString('leaked [REDACTED]', $output);
|
||||
}
|
||||
|
||||
public function testRedactsSensitiveParameterInheritedFromAParentClass()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%context%', 'Y-m-d'));
|
||||
|
||||
$output = $formatter->format($this->getRecord(context: ['creds' => new ChildCredentials()]));
|
||||
|
||||
$this->assertStringNotContainsString('tok-inherited-secret', $output);
|
||||
$this->assertStringContainsString('token=[REDACTED]', $output);
|
||||
}
|
||||
|
||||
public function testRedactsStringableSecretsAtSensitiveKeys()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%message% %context%', 'Y-m-d'));
|
||||
|
||||
$output = $formatter->format($this->getRecord(
|
||||
message: 'authenticating with stringable-secret-value',
|
||||
context: ['token' => new StringableSecret()],
|
||||
));
|
||||
|
||||
$this->assertStringNotContainsString('stringable-secret-value', $output);
|
||||
}
|
||||
|
||||
public function testRedactsStringableSensitiveParameters()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%context%', 'Y-m-d'));
|
||||
|
||||
$output = $formatter->format($this->getRecord(context: ['holder' => new StringableSecretHolder()]));
|
||||
|
||||
$this->assertStringNotContainsString('stringable-secret-value', $output);
|
||||
$this->assertStringContainsString('auth=[REDACTED]', $output);
|
||||
}
|
||||
|
||||
public function testAThrowingToStringDoesNotBreakFormatting()
|
||||
{
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%message% %context%', 'Y-m-d'));
|
||||
|
||||
$output = $formatter->format($this->getRecord(
|
||||
message: 'still logged',
|
||||
context: ['token' => new ThrowingStringable()],
|
||||
));
|
||||
|
||||
$this->assertStringContainsString('still logged', $output);
|
||||
}
|
||||
|
||||
public function testARecordIsDroppedRatherThanLeakedWhenAPatternCannotBeApplied()
|
||||
{
|
||||
// a /u pattern over invalid UTF-8 makes preg_replace() return null
|
||||
$formatter = new RedactingFormatter(
|
||||
new LineFormatter('%message%', 'Y-m-d'),
|
||||
sensitiveKeys: [],
|
||||
patterns: ['{tok-[a-z0-9]+}u'],
|
||||
);
|
||||
|
||||
$output = $formatter->format($this->getRecord(message: 'secret tok-abc123 in '.\chr(0xFF).' here'));
|
||||
|
||||
$this->assertStringNotContainsString('tok-abc123', $output);
|
||||
$this->assertStringContainsString('RedactingFormatter dropped this record', $output);
|
||||
// neither the pattern nor the mask is echoed back, both may embed secret material
|
||||
$this->assertStringNotContainsString('tok-[a-z0-9]+', $output);
|
||||
}
|
||||
|
||||
public function testSelfReferencingContextArraysDoNotRecurseForever()
|
||||
{
|
||||
$context = ['password' => 'super-secret-value'];
|
||||
$context['self'] = &$context;
|
||||
|
||||
$formatter = new RedactingFormatter(new LineFormatter('%context%', 'Y-m-d'));
|
||||
|
||||
$output = $formatter->format($this->getRecord(context: $context));
|
||||
|
||||
$this->assertIsString($output);
|
||||
$this->assertStringNotContainsString('super-secret-value', $output);
|
||||
}
|
||||
|
||||
public function testSelfReferencingFormatterOutputDoesNotRecurseForever()
|
||||
{
|
||||
$inner = new class implements FormatterInterface {
|
||||
public function format(LogRecord $record)
|
||||
{
|
||||
$formatted = ['leaked' => 'tok-supersecret-value'];
|
||||
$formatted['self'] = &$formatted;
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
public function formatBatch(array $records)
|
||||
{
|
||||
return array_map(fn (LogRecord $record) => $this->format($record), $records);
|
||||
}
|
||||
};
|
||||
|
||||
$output = (new RedactingFormatter($inner))->format($this->getRecord(context: ['token' => 'tok-supersecret-value']));
|
||||
|
||||
$this->assertIsArray($output);
|
||||
$this->assertSame('[REDACTED]', $output['leaked']);
|
||||
}
|
||||
|
||||
public function testGetWrappedFormatter()
|
||||
{
|
||||
$inner = new LineFormatter();
|
||||
|
||||
$this->assertSame($inner, (new RedactingFormatter($inner))->getWrappedFormatter());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatter that records the (already redacted) LogRecord it receives
|
||||
* into $captured, so tests can assert on the structured record passed downstream.
|
||||
*/
|
||||
private function capturingFormatter(?LogRecord &$captured): FormatterInterface
|
||||
{
|
||||
return new class($captured) implements FormatterInterface {
|
||||
public function __construct(private ?LogRecord &$captured)
|
||||
{
|
||||
}
|
||||
|
||||
public function format(LogRecord $record)
|
||||
{
|
||||
$this->captured = $record;
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function formatBatch(array $records)
|
||||
{
|
||||
foreach ($records as $record) {
|
||||
$this->captured = $record;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SensitiveParamHolder
|
||||
{
|
||||
public function __construct(
|
||||
public string $id = 'user-1',
|
||||
#[\SensitiveParameter]
|
||||
public string $mySecretUrl = 'https://example.org/s3cret-webhook',
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
class SensitiveDsnHolder
|
||||
{
|
||||
public function __construct(
|
||||
#[\SensitiveParameter]
|
||||
private string $dsn = 'mysql://user:hunter2@localhost/db',
|
||||
) {
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'Connection('.$this->dsn.')';
|
||||
}
|
||||
}
|
||||
|
||||
class SensitiveNonPromotedHolder
|
||||
{
|
||||
public string $token;
|
||||
|
||||
public function __construct(#[\SensitiveParameter] string $token = 'tok-abcdefghij')
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
}
|
||||
|
||||
class SensitiveNestingHolder
|
||||
{
|
||||
public function __construct(public SensitiveParamHolder $inner = new SensitiveParamHolder())
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class SensitiveCyclicHolder
|
||||
{
|
||||
public ?self $next = null;
|
||||
|
||||
public function __construct(#[\SensitiveParameter] public string $secret = 'cyclic-secret-value')
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
enum SensitiveTestEnum: string
|
||||
{
|
||||
case One = 'one';
|
||||
}
|
||||
|
||||
class ParentCredentials
|
||||
{
|
||||
public function __construct(
|
||||
#[\SensitiveParameter]
|
||||
private string $token = 'tok-inherited-secret',
|
||||
) {
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return "token={$this->token}";
|
||||
}
|
||||
}
|
||||
|
||||
class ChildCredentials extends ParentCredentials
|
||||
{
|
||||
}
|
||||
|
||||
class StringableSecret
|
||||
{
|
||||
public function __construct(private string $value = 'stringable-secret-value')
|
||||
{
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
|
||||
class StringableSecretHolder
|
||||
{
|
||||
public function __construct(
|
||||
#[\SensitiveParameter]
|
||||
private StringableSecret $token = new StringableSecret(),
|
||||
) {
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'auth='.$this->token;
|
||||
}
|
||||
}
|
||||
|
||||
class ThrowingStringable
|
||||
{
|
||||
public function __toString(): string
|
||||
{
|
||||
throw new \RuntimeException('nope');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user