Skip to content
42 changes: 42 additions & 0 deletions src/ContextProcessor/ArrayProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace RoadRunner\PsrLogger\ContextProcessor;

/**
* Processor for arrays and nested arrays.
*
* Recursively processes array elements to handle complex nested structures.
*
* @implements ContextProcessorInterface<array<array-key, mixed>, array<array-key, mixed>>
*/
class ArrayProcessor implements ContextProcessorInterface
{
public function canProcess(mixed $value): bool
{
return \is_array($value);
}

/**
* @param array<array-key, mixed> $value
* @param callable(mixed): mixed $recursiveProcessor
* @return array<array-key, mixed>
*/
public function process(mixed $value, callable $recursiveProcessor): mixed
{
/** @var array<array-key, mixed> $processed */
$processed = [];

/**
* @var array-key $key
* @var mixed $item
*/
foreach ($value as $key => $item) {
/** @psalm-suppress MixedAssignment - Intentionally processing mixed types */
$processed[$key] = $recursiveProcessor($item);
}

return $processed;
}
}
31 changes: 31 additions & 0 deletions src/ContextProcessor/ContextProcessorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace RoadRunner\PsrLogger\ContextProcessor;

/**
* Interface for context data processors.
*
* Each processor handles a specific type of data and converts it to a
* format suitable for structured logging.
*
* @template TValue The input value type
* @template TProcessed The processed output type
*/
interface ContextProcessorInterface
{
/**
* Check if this processor can handle the given value.
*/
public function canProcess(mixed $value): bool;

/**
* Process the value and return a serializable representation.
*
* @param TValue $value The value to process
* @param callable(mixed): mixed $recursiveProcessor Function to process nested values recursively
* @return TProcessed Processed value suitable for logging
*/
public function process(mixed $value, callable $recursiveProcessor): mixed;
}
104 changes: 104 additions & 0 deletions src/ContextProcessor/ContextProcessorManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

declare(strict_types=1);

namespace RoadRunner\PsrLogger\ContextProcessor;

/**
* Manager for context data processors.
*
* Coordinates multiple processors to handle different data types for structured logging.
* Processors are executed in registration order, with the first matching processor handling the value.
*/
class ContextProcessorManager
{
/** @var array<ContextProcessorInterface> */
private array $processors = [];

public function __construct()
{
$this->registerDefaultProcessors();
}

/**
* Register a processor.
* Processors are checked in the order they are added.
*/
public function addProcessor(ContextProcessorInterface $processor): void
{
$this->processors[] = $processor;
}

/**
* Process context data recursively.
*
* @template TKey of array-key
* @template TValue
* @param array<TKey, TValue> $context
* @return array<string, mixed>
*/
public function processContext(array $context): array
{
if (empty($context)) {
return [];
}

/** @var array<string, mixed> $processed */
$processed = [];

/**
* @var TKey $key
* @var TValue $value
*/
foreach ($context as $key => $value) {
$stringKey = (string) $key;
/** @psalm-suppress MixedAssignment - Intentionally processing mixed types */
$processed[$stringKey] = $this->processValue($value);
}

return $processed;
}

/**
* Process a single value using the appropriate processor.
*/
public function processValue(mixed $value): mixed
{
foreach ($this->processors as $processor) {
if ($processor->canProcess($value)) {
return $processor->process($value, [$this, 'processValue']);
}
}

// This should never happen due to FallbackProcessor, but just in case
return \gettype($value);
}

/**
* Register the default set of processors in the correct order.
* Order matters: more specific processors should be registered first.
*/
private function registerDefaultProcessors(): void
{
// Null values first (most specific)
$this->addProcessor(new NullProcessor());

// Scalar values (very common, but after null)
$this->addProcessor(new ScalarProcessor());

// Specific object types (before generic object processor)
$this->addProcessor(new DateTimeProcessor());
$this->addProcessor(new ThrowableProcessor());
$this->addProcessor(new StringableProcessor());

// Collections and resources
$this->addProcessor(new ArrayProcessor());
$this->addProcessor(new ResourceProcessor());

// Generic object processor (before fallback)
$this->addProcessor(new ObjectProcessor());

// Fallback processor (last resort)
$this->addProcessor(new FallbackProcessor());
}
}
31 changes: 31 additions & 0 deletions src/ContextProcessor/DateTimeProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace RoadRunner\PsrLogger\ContextProcessor;

/**
* Processor for DateTime objects.
*
* Converts DateTime and DateTimeImmutable objects to ISO 8601 format
* for consistent structured logging.
*
* @implements ContextProcessorInterface<\DateTimeInterface, string>
*/
class DateTimeProcessor implements ContextProcessorInterface
{
public function canProcess(mixed $value): bool
{
return $value instanceof \DateTimeInterface;
}

/**
* @param \DateTimeInterface $value
* @param callable(mixed): mixed $recursiveProcessor
* @return string
*/
public function process(mixed $value, callable $recursiveProcessor): mixed
{
return $value->format(\DateTimeInterface::ATOM);
}
}
31 changes: 31 additions & 0 deletions src/ContextProcessor/FallbackProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace RoadRunner\PsrLogger\ContextProcessor;

/**
* Fallback processor for unknown types.
*
* Returns the type name for any value that couldn't be processed
* by more specific processors.
*
* @implements ContextProcessorInterface<mixed, string>
*/
class FallbackProcessor implements ContextProcessorInterface
{
public function canProcess(mixed $value): bool
{
// This processor can handle anything as a last resort
return true;
}

/**
* @param callable(mixed): mixed $recursiveProcessor
* @return string
*/
public function process(mixed $value, callable $recursiveProcessor): mixed
{
return \gettype($value);
}
}
32 changes: 32 additions & 0 deletions src/ContextProcessor/NullProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace RoadRunner\PsrLogger\ContextProcessor;

/**
* Processor for null values.
*
* Handles null values explicitly, passing them through as-is
* since null is already suitable for structured logging.
*
* @implements ContextProcessorInterface<null, null>
*/
class NullProcessor implements ContextProcessorInterface
{
public function canProcess(mixed $value): bool
{
return $value === null;
}

/**
* @param null $value
* @param callable(mixed): mixed $recursiveProcessor
* @return null
*/
public function process(mixed $value, callable $recursiveProcessor): mixed
{
// Null values are already suitable for logging
return null;
}
}
49 changes: 49 additions & 0 deletions src/ContextProcessor/ObjectProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace RoadRunner\PsrLogger\ContextProcessor;

/**
* Processor for generic objects.
*
* Attempts to convert objects to array representation using public properties,
* or falls back to class name if no public properties are available.
*
* @implements ContextProcessorInterface<object, array<string, mixed>|string>
*/
class ObjectProcessor implements ContextProcessorInterface
{
public function canProcess(mixed $value): bool
{
return \is_object($value);
}

/**
* @param object $value
* @param callable(mixed): mixed $recursiveProcessor
* @return array<string, mixed>|string
*/
public function process(mixed $value, callable $recursiveProcessor): mixed
{
// Try to convert to array (for objects with public properties)
$objectVars = \get_object_vars($value);

if (!empty($objectVars)) {
/** @var array<string, mixed> $processed */
$processed = [];
/**
* @var string $property
* @var mixed $propertyValue
*/
foreach ($objectVars as $property => $propertyValue) {
/** @psalm-suppress MixedAssignment - Intentionally processing mixed types */
$processed[$property] = $recursiveProcessor($propertyValue);
}
return $processed;
}

// Fallback to class name if no public properties
return \get_class($value);
}
}
30 changes: 30 additions & 0 deletions src/ContextProcessor/ResourceProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace RoadRunner\PsrLogger\ContextProcessor;

/**
* Processor for resource types.
*
* Converts resources to string representation indicating the resource type.
*
* @implements ContextProcessorInterface<resource, string>
*/
class ResourceProcessor implements ContextProcessorInterface
{
public function canProcess(mixed $value): bool
{
return \is_resource($value);
}

/**
* @param resource $value
* @param callable(mixed): mixed $recursiveProcessor
* @return string
*/
public function process(mixed $value, callable $recursiveProcessor): mixed
{
return \get_resource_type($value) . ' resource';
}
}
33 changes: 33 additions & 0 deletions src/ContextProcessor/ScalarProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace RoadRunner\PsrLogger\ContextProcessor;

/**
* Processor for scalar values (string, int, float, bool).
*
* These values are passed through as-is since they are already
* suitable for structured logging. Note: null values are handled
* by the dedicated NullProcessor.
*
* @implements ContextProcessorInterface<scalar, scalar>
*/
class ScalarProcessor implements ContextProcessorInterface
{
public function canProcess(mixed $value): bool
{
return \is_scalar($value);
}

/**
* @param scalar $value
* @param callable(mixed): mixed $recursiveProcessor
* @return scalar
*/
public function process(mixed $value, callable $recursiveProcessor): mixed
{
// Scalar values are already suitable for logging
return $value;
}
}
Loading
Loading