<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mike Georgeff</title>
    <description>The latest articles on DEV Community by Mike Georgeff (@georgeff).</description>
    <link>https://dev.to/georgeff</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3965697%2F8201ca64-8b55-4d61-8104-865808572585.jpeg</url>
      <title>DEV Community: Mike Georgeff</title>
      <link>https://dev.to/georgeff</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/georgeff"/>
    <language>en</language>
    <item>
      <title>Context is Curated, Not Captured</title>
      <dc:creator>Mike Georgeff</dc:creator>
      <pubDate>Mon, 22 Jun 2026 17:17:39 +0000</pubDate>
      <link>https://dev.to/georgeff/context-is-curated-not-captured-5fof</link>
      <guid>https://dev.to/georgeff/context-is-curated-not-captured-5fof</guid>
      <description>&lt;h3&gt;
  
  
  The Problem Part 1 Didn't Solve
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://dev.to/georgeff/your-php-logs-are-lying-to-you-4g72"&gt;Part 1&lt;/a&gt; gave you consistent log line structure: every entry has a consistent envelope. But the context is still a free-for-all.&lt;/p&gt;

&lt;p&gt;Two common anti-patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logging raw exception messages: &lt;code&gt;$logger-&amp;gt;error($e-&amp;gt;getMessage())&lt;/code&gt;, a string with no structure, no machine-readable context&lt;/li&gt;
&lt;li&gt;Dumping arbitrary data into the context: raw third-party API responses, full request payloads, deeply nested objects thrown into the context array&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second anti-pattern has real operational consequences. Log platforms have entry size limits; oversized context causes truncation or log entries being split and needing recomposition in the platform. Inconsistent field names across the codebase (&lt;code&gt;userId&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;uid&lt;/code&gt;) destroy the aggregation the platform was bought to provide. The problem isn't the logger; the logger is doing its job. The problem is that the context is treated as a dumping ground rather than a contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context is Curated, Not Captured
&lt;/h3&gt;

&lt;p&gt;The discipline: define what fields belong on a log entry for a given exception ahead of time, not at the call site. The call site should never decide what context gets logged; the decision belongs at the domain model level.&lt;/p&gt;

&lt;p&gt;This is the difference between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Captured - call site decides, schema varies everywhere&lt;/span&gt;
&lt;span class="nv"&gt;$logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'response'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$apiResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Curated - schema defined on the exception, the reporter just delivers it&lt;/span&gt;
&lt;span class="nv"&gt;$logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;structuredData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;structuredData&lt;/code&gt; is a property on the domain exception; it returns the complete structural representation of the exception: error code, severity, retryable flag, occurred_at, context, metadata. The reporter doesn't decide what gets logged; the exception does.&lt;/p&gt;

&lt;p&gt;The reporter's job is minimal. Translate the throwable, then call the logger with the three values the domain exception already knows about itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;translator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;structuredData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consistency is the goal. Every &lt;code&gt;PaymentFailedException&lt;/code&gt; that gets logged looks identical in your logging platform, regardless of where in the codebase it was thrown.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Domain Exception Model
&lt;/h3&gt;

&lt;p&gt;A domain exception is an exception that carries its own structured context. It knows severity, its error code, whether the operation is retryable, and the specific context field that describes it.&lt;/p&gt;

&lt;p&gt;Key properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;severity&lt;/code&gt;: not inferred by the logger, declared by the exception itself. &lt;code&gt;severity&lt;/code&gt; is a backed enum (&lt;code&gt;critical&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, &lt;code&gt;info&lt;/code&gt;, &lt;code&gt;debug&lt;/code&gt;). &lt;code&gt;$e-&amp;gt;severity-&amp;gt;value&lt;/code&gt; gives the string the logger expects. Type safety means no invalid severity values, no typos making it into production.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context&lt;/code&gt;: curated key/value data defined at construction, not at the log call.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retryable&lt;/code&gt;: operational metadata that belongs on the log entry.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;occurredAt&lt;/code&gt;: RFC 3339 timestamp captured at throw time, not log time.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getErrorCode()&lt;/code&gt;: structured error code (e.g. &lt;code&gt;DB_0001&lt;/code&gt;, &lt;code&gt;PAYMENT_0033&lt;/code&gt;) that is independently queryable in your log platform.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;structuredData&lt;/code&gt;: the complete structured representation of the exception, ready for the logger. Uses a property hook (PHP 8.4+), computed once, then cached.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Asymmetric property visibility &lt;code&gt;public protected(set)&lt;/code&gt; (PHP 8.4+). Properties are publicly readable but only settable within the class and its subclasses. Immutable from the outside; prevents mutation after construction. &lt;code&gt;getErrorCode()&lt;/code&gt; is abstract; every concrete exception must declare its own code. This is the contract that enforces the structured error code pattern across the codebase.&lt;/p&gt;

&lt;p&gt;As a fallback for exceptions that have not been modeled yet, an &lt;code&gt;UnknownException&lt;/code&gt; is defined that captures the original class, code, file and line. &lt;code&gt;UNKNOWN_0000&lt;/code&gt; as the error code is a signal in your log platform that something needs modeling.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Translation Pipeline
&lt;/h3&gt;

&lt;p&gt;Not every exception in your application will be a &lt;code&gt;DomainException&lt;/code&gt;. Third-party libraries throw their own exceptions, PHP itself throws runtime errors. The translator's job is to accept any &lt;code&gt;Throwable&lt;/code&gt; and return a &lt;code&gt;DomainException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;TranslationHandler&lt;/code&gt; contract has three methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;TranslationHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;DomainException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;matches()&lt;/code&gt;: does the handler know how to translate this exception?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;handle()&lt;/code&gt;: translate the &lt;code&gt;Throwable&lt;/code&gt; into a &lt;code&gt;DomainException&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;priority()&lt;/code&gt;: handlers are sorted once during construction, not at call time. Higher priority wins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The translator's full constructor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;TranslationHandler&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$handlers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$handlers&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nb"&gt;usort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sorted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;handlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sorted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same principle as the boot/run boundary from the &lt;a href="https://dev.to/georgeff/two-phases-two-apis-425j"&gt;kernel&lt;/a&gt;. Expensive work (sorting) happens once at construction; the hot path (&lt;code&gt;translate()&lt;/code&gt;) just iterates an already-sorted list.&lt;/p&gt;

&lt;p&gt;The translator walks the already sorted handler list and finds the first match.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;array_find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;array_find&lt;/code&gt; (PHP 8.4+) is the right tool. Find the first match and stop. No manual loops, no breaks; the intent is immediately readable.&lt;/p&gt;

&lt;p&gt;The full pipeline: every &lt;code&gt;Throwable&lt;/code&gt; that enters &lt;code&gt;translate()&lt;/code&gt; follows exactly one path:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: already a domain exception, return as is&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;DomainException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No translation needed, no information lost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: find the first matching handler&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;array_find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: no handler matched, wrap in an &lt;code&gt;UnknownException&lt;/code&gt; and return&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnknownException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is swallowed. &lt;code&gt;UNKNOWN_0000&lt;/code&gt; in the platform is a signal that this exception type needs a handler written for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: handler matched, translate and return&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$handler&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler produces a &lt;code&gt;DomainException&lt;/code&gt; with curated context.&lt;/p&gt;

&lt;p&gt;The pipeline is extensible. Add a handler for any third-party exception type and it gets curated context without touching the call site.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Enricher Pattern
&lt;/h3&gt;

&lt;p&gt;The enricher sits above the &lt;code&gt;DomainException&lt;/code&gt;. It adds context fields to every log entry regardless of what is being logged. The &lt;code&gt;ContextEnricher&lt;/code&gt; contract defines a single method: &lt;code&gt;enrich(array $context): array&lt;/code&gt;. Enrichers are composed into the logger via the decorator pattern described in &lt;a href="https://dev.to/georgeff/your-php-logs-are-lying-to-you-4g72"&gt;Part 1&lt;/a&gt;, wired at boot, invisible to the call site. The addition operator preserves caller-set values: &lt;code&gt;$context + ['field' =&amp;gt; $value]&lt;/code&gt;. An enricher never overwrites context that the &lt;code&gt;DomainException&lt;/code&gt; already defined.&lt;/p&gt;

&lt;p&gt;A concrete example: &lt;code&gt;CorrelationIdEnricher&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;enrich&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'correlation_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;correlationId&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every log entry now carries a &lt;code&gt;correlation_id&lt;/code&gt; field, added once at the composition root, without a single call site change. The enricher is registered by the &lt;code&gt;StructuredLoggingModule&lt;/code&gt; and auto-resolved by the kernel tag registry; the developer adds the module explicitly, and the enricher wires itself through the tag system with no further call site work. What &lt;code&gt;correlation_id&lt;/code&gt; is, where it comes from, and how it connects log entries across service boundaries will be explained in Part 3.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Have Now and What's Still Missing
&lt;/h3&gt;

&lt;p&gt;Every log entry has a consistent envelope (&lt;a href="https://dev.to/georgeff/your-php-logs-are-lying-to-you-4g72"&gt;Part 1&lt;/a&gt;), every exception produces consistent curated structured context, and every log entry carries a &lt;code&gt;correlation_id&lt;/code&gt; field. Your log platform can now aggregate by error code, filter by severity, track retryable vs non-retryable failures, and report on error rates by type. But, &lt;code&gt;correlation_id&lt;/code&gt; is only useful if it's the same value across all log entries for a given request, and across all services that handle that request. A &lt;code&gt;correlation_id&lt;/code&gt; that resets per service tells you nothing about the flow of the request through a distributed system.&lt;/p&gt;

&lt;p&gt;Part 3 of this series will define what correlation IDs are, why they matter, and how they flow across service boundaries.&lt;/p&gt;




&lt;p&gt;If you want to see a full concrete implementation of everything covered in this article, &lt;code&gt;meritum/structured-logging&lt;/code&gt; is a domain exception model, translation pipeline, and enricher system built on these exact principles.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/MeritumIO/structured-logging" rel="noopener noreferrer"&gt;github.com/MeritumIO/structured-logging&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://packagist.org/packages/meritum/structured-logging" rel="noopener noreferrer"&gt;packagist.org/packages/meritum/structured-logging&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Natural Aristoi - &lt;a href="https://dev.to/georgeff/natural-aristoi-h6j"&gt;dev.to/georgeff/natural-aristoi-h6j&lt;/a&gt;&lt;br&gt;
Two Phases, Two APIs - &lt;a href="https://dev.to/georgeff/two-phases-two-apis-425j"&gt;dev.to/georgeff/two-phases-two-apis-425j&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>softwareengineering</category>
      <category>php</category>
      <category>php8</category>
    </item>
    <item>
      <title>Your PHP Logs are Lying to You</title>
      <dc:creator>Mike Georgeff</dc:creator>
      <pubDate>Tue, 16 Jun 2026 14:42:04 +0000</pubDate>
      <link>https://dev.to/georgeff/your-php-logs-are-lying-to-you-4g72</link>
      <guid>https://dev.to/georgeff/your-php-logs-are-lying-to-you-4g72</guid>
      <description>&lt;h3&gt;
  
  
  The Evolution: From Log Tails to Indexed Search
&lt;/h3&gt;

&lt;p&gt;Most teams start with a stream-based log platform, such as Papertrail, Loggly, or legacy syslog viewers. These platforms are fast to set up, and work well early on, but ultimately are just live tail with grep. Unstructured logs work fine here because a human is reading and searching them manually. As the application and team grow, you graduate to an indexed search platform, such as Elastic/OpenSearch, Datadog Logs, or Loki with LogQL. This is where unstructured logs break down because these platforms expect to index fields, not parse free text. You can still ship plain text to Elastic, but you've thrown away everything that makes it powerful: field filtering, aggregations, dashboards, and alerting on specific values. If you're thinking "we write to a log file, not a stream", that's fine, and many of these stream platforms are reading a file anyway. The live tail is literally &lt;code&gt;tail -f&lt;/code&gt;. The problem was never the destination. The problem is what the file contains. The platform didn't change your logs, it exposed what your logs already were: unstructured text. Structured logs enable trend analysis: error rate over time, errors by application version, change failure rate after a deployment. These are questions grep can't answer. "Did our error rate increase after v2.3.0 shipped?" requires &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;level&lt;/code&gt;, and &lt;code&gt;timestamp&lt;/code&gt; as discrete, indexed fields you can aggregate on, not substrings buried in formatted strings. These are the kinds of questions that matter at an engineering org level. Change failure rate is a DORA metric. Teams measuring engineering effectiveness need their logs to support it, and that's only possible with consistent structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stdout Decouples Your Application from the Log Destination
&lt;/h3&gt;

&lt;p&gt;Writing to stdout is an intentional architectural boundary, not a lack of a "real" logging setup. Your application's only concern is emitting a well-formed log entry to the stream. Where it goes after that is not the application's problem. In a Kubernetes environment, an operator (Fluentd, Fluent Bit, Logstash, Vector) captures the stdout stream from each container and routes it. Swapping log platforms is an operator configuration change. Zero application code is touched, and there is zero redeployment of your software. A Kubernetes operator can fan logs out to multiple destinations simultaneously: Elastic for search indexing, S3 for long-term archival, PagerDuty for critical-level entries. Your application emits once, and the operator handles the rest. This fan-out is something a tightly coupled or file-based logger cannot do cleanly. The application would have to be aware of every destination. The 12-factor principle formalizes this: treat logs as event streams, let the execution environment handle routing and storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Structured Logging Actually Is
&lt;/h3&gt;

&lt;p&gt;Each log entry is a self-describing data structure, not a formatted string. Use NDJSON (Newline Delimited JSON), one JSON object per line, independently parseable by the logging platform. The platform indexes each field, not the full string. This is the difference between "find me logs containing the word payment" and "find me all error-level logs where amount &amp;gt; 100 in the last 15 minutes". The platform can only be as powerful as the structure you give it.&lt;/p&gt;

&lt;p&gt;Example comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Unstructured
[2026-06-09 14:32:01] ERROR: Payment failed for user 4321

# Structured
{"timestamp":"2026-06-15T10:28:01.123+00:00","level":"error","severity":"error","message":"Payment failed","context":{"user_id":4321,"app_version":"1.3.0"}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What belongs on every log line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;timestamp&lt;/code&gt;: RFC 3339 extended, UTC (e.g. &lt;code&gt;2026-06-10T14:32:01.123+00:00&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;level&lt;/code&gt;: original PSR-3 level name, all 8 values are preserved&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;severity&lt;/code&gt;: reduced 5-value set (&lt;code&gt;debug&lt;/code&gt;, &lt;code&gt;info&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;critical&lt;/code&gt;) aligned with structured log sinks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;message&lt;/code&gt;: human-readable summary, kept short&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context&lt;/code&gt;: structured key/value object, always present even when no context is supplied&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why &lt;code&gt;level&lt;/code&gt; and &lt;code&gt;severity&lt;/code&gt;? PSR-3 defines 8 levels, but most sinks don't natively understand all of them. &lt;code&gt;severity&lt;/code&gt; gives the platform a normalized field it can reliably aggregate on without knowing PSR-3's model. In short, &lt;code&gt;level&lt;/code&gt; preserves PSR-3 fidelity, &lt;code&gt;severity&lt;/code&gt; serves the platform. No information is lost and the sink gets what it needs. The practical side effect is the &lt;code&gt;severity&lt;/code&gt; field ends the "when do I use &lt;code&gt;emergency&lt;/code&gt; vs &lt;code&gt;alert&lt;/code&gt; vs &lt;code&gt;critical&lt;/code&gt;" debate. Those distinctions came from syslog and don't map cleanly to application logging. Your alerting dashboard runs on &lt;code&gt;severity&lt;/code&gt;. Operationally, all three mean the same thing. The nuance lives in &lt;code&gt;level&lt;/code&gt; for whoever needs it.&lt;/p&gt;

&lt;p&gt;Consistency matters more than completeness. Every line must have the same shape. A &lt;code&gt;version&lt;/code&gt; field that appears on 60% of log lines is useless for change failure rate reporting.&lt;/p&gt;

&lt;h3&gt;
  
  
  PSR-3 and Why It Matters
&lt;/h3&gt;

&lt;p&gt;PSR-3 is the PHP standard logging contract defining 8 severity methods and &lt;code&gt;log()&lt;/code&gt;. Context, the second argument defined by the interface, is where the structure lives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Payment failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4321&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'app_version'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'1.3.0'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;app_version&lt;/code&gt; will appear on every "Payment failed" log, providing the logging platform consistent data to index.&lt;/p&gt;

&lt;p&gt;Coding to PSR-3 means your application is decoupled from the concrete logger implementation. The decorator pattern is a natural extension of PSR-3 decoupling. Because &lt;code&gt;LoggerInterface&lt;/code&gt; is an interface, you can wrap any logger that implements the same contract. A context-enriching logger accepts a &lt;code&gt;LoggerInterface&lt;/code&gt;, enriches the context, then delegates to the inner logger. The call site never changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContextEnrichingLogger&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;LoggerInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * @var ContextEnricher[]
     */&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$enrichers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;LoggerInterface&lt;/span&gt; &lt;span class="nv"&gt;$inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;ContextEnricher&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$enrichers&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;enrichers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$enrichers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$enrichedContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;enrichContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$enrichedContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Remaining LoggerInterface methods&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where consistent context attributes get added once, at the composition root, rather than at every log call site. Wire the decorator at &lt;a href="https://dev.to/georgeff/two-phases-two-apis-425j"&gt;boot time&lt;/a&gt; and every log call in the application gets the enriched context automatically. The enrichers can globally capture values such as &lt;code&gt;app_version&lt;/code&gt;, &lt;code&gt;environment&lt;/code&gt;, and &lt;code&gt;correlation_id&lt;/code&gt;. This provides a central location to build out log context, eliminating repetition at every log call. What the enricher pipeline doesn't solve is converting a thrown exception into structured context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing a Minimal Structured Logger
&lt;/h3&gt;

&lt;p&gt;When writing a minimal logger, implement the &lt;code&gt;LoggerInterface&lt;/code&gt; as a first-class citizen of the implementation, not an afterthought. The &lt;a href="https://packagist.org/packages/psr/log" rel="noopener noreferrer"&gt;&lt;code&gt;psr/log&lt;/code&gt;&lt;/a&gt; package also ships with a trait defining the 8 standard methods, allowing the implementation to focus on &lt;code&gt;log()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Psr\Log\LoggerTrait&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Psr\Log\LoggerInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Logger&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;LoggerInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;LoggerTrait&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write the output to stdout.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
 * @var resource
 */&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;mixed&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;STDOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;is_resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\InvalidArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Logger output must be a resource'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Format each entry as a JSON object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;\Stringable&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$severityMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'emergency'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'critical'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'alert'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'critical'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'critical'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'critical'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'error'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'error'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'warning'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'warning'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'notice'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'info'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'info'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'info'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'debug'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'debug'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="nv"&gt;$logEntry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'timestamp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\DateTimeImmutable&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\DateTimeInterface&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RFC3339_EXTENDED&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'level'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'severity'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$severityMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$level&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'message'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'context'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="nv"&gt;$json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$logEntry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;JSON_UNESCAPED_SLASHES&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;JSON_UNESCAPED_UNICODE&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;JSON_THROW_ON_ERROR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nb"&gt;fwrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$json&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep it simple. No file rotation, no handlers, no formatters. The logger's only job is to serialize a log entry and write it to the stream.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Have Now and What's Still Missing
&lt;/h3&gt;

&lt;p&gt;You have clean, machine-readable, streamable logs that log aggregators can ingest and index without configuration. Once your logs are structured, how do you enrich them with the proper context, especially when the event being logged is an exception? And that's before we even get to the thousands of JSON lines with no way to know which lines belong to the same request.&lt;/p&gt;

&lt;p&gt;Part 2 of this series will cover the domain exception model and translation pipeline that turns exceptions into structured log data.&lt;/p&gt;




&lt;p&gt;If you want to see a full concrete implementation of everything covered in this article, &lt;code&gt;meritum/logger&lt;/code&gt; is a minimal PSR-3 structured logger built on these exact principles: RFC 3339 timestamps, NDJSON to stdout, severity normalization for structured log sinks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/MeritumIO/logger" rel="noopener noreferrer"&gt;github.com/MeritumIO/logger&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://packagist.org/packages/meritum/logger" rel="noopener noreferrer"&gt;packagist.org/packages/meritum/logger&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>softwareengineering</category>
      <category>php</category>
      <category>php8</category>
    </item>
    <item>
      <title>Two Phases, Two APIs</title>
      <dc:creator>Mike Georgeff</dc:creator>
      <pubDate>Tue, 09 Jun 2026 03:01:24 +0000</pubDate>
      <link>https://dev.to/georgeff/two-phases-two-apis-425j</link>
      <guid>https://dev.to/georgeff/two-phases-two-apis-425j</guid>
      <description>&lt;p&gt;Most applications have no enforced lifecycle boundaries. Services get resolved at arbitrary times, configs mutate mid-request, init logic bleeds into request handling. The result is an application that is difficult to reason about because its state is never truly settled.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Two Phases
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Boot&lt;/strong&gt;: The period before the application handles any work. The sole purpose of the boot phase is construction, registering service definitions, merging configuration, wiring dependencies, and running initialization logic. When boot ends, the service layer is immutable, nothing new can be registered, nothing can be reconfigured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run&lt;/strong&gt;: The application transforms inputs into outputs using the fully constructed service layer. No reconfiguration, no new definitions, no structural mutations, only pure transformation.&lt;/p&gt;

&lt;p&gt;The hard boundary between boot and run is not a convention, it is a constraint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the Boundary Matters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Predictability&lt;/strong&gt;: if the service layer is immutable after boot you always know what you are working with during a request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testability&lt;/strong&gt;: an immutable service graph is easy to inspect and reproduce in tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debuggability&lt;/strong&gt;: boot-time errors fail loud and early, they do not surface as mysterious runtime behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: a service layer that cannot be mutated at runtime prevents the injection of rogue providers or configuration overrides, shrinking the attack surface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Boot is boot, run is run, this should be enforced, not suggested.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Two APIs
&lt;/h3&gt;

&lt;p&gt;The package &lt;a href="https://github.com/MikeGeorgeff/kernel" rel="noopener noreferrer"&gt;&lt;code&gt;georgeff/kernel&lt;/code&gt;&lt;/a&gt; enforces this at the type level.&lt;/p&gt;

&lt;p&gt;When correctly composed, kernel definitions are registered before boot is called, and the fully initialized container is available after.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$kernel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Kernel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$kernel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MyClass&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MyClass&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nv"&gt;$kernel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$kernel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContainer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$container&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MyClass&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;addDefinition&lt;/code&gt; is called after boot, a &lt;code&gt;KernelException&lt;/code&gt; will be thrown because the service layer is immutable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$kernel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Kernel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$kernel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$kernel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MyClass&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MyClass&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the container is accessed before boot, a &lt;code&gt;KernelException&lt;/code&gt; will be thrown because the container has not yet been initialized.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$kernel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Kernel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$kernel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContainer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$kernel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run begins the moment &lt;code&gt;boot()&lt;/code&gt; returns, after the kernel is a resolved, immutable service graph. The handoff from construction to transformation is complete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Modules as Boot-Time Citizens
&lt;/h3&gt;

&lt;p&gt;In the &lt;code&gt;georgeff/kernel&lt;/code&gt; package, modules enforce the phase separation by design. &lt;code&gt;ModuleInterface::register()&lt;/code&gt; is a boot-time method, whose purpose is to register service definitions, tags, and decorators. &lt;code&gt;BootableModuleInterface::boot()&lt;/code&gt; is called at the tail end of the boot cycle, providing access to the built container for initialization work. New definitions cannot be registered at this stage since the service graph is already sealed. Neither has any role in run, making it structurally impossible to blur the line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phases as Architecture
&lt;/h3&gt;

&lt;p&gt;The boot/run distinction is not a pattern unique to this kernel, it is a general principle that most applications implement accidentally and inconsistently. Making it explicit, enforcing it with hard constraints, and designing your application around it produces software that is honest about its own state.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related&lt;/strong&gt;: &lt;a href="https://dev.to/georgeff/natural-aristoi-h6j"&gt;Natural Aristoi&lt;/a&gt; — the philosophy behind the design decisions in this package.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>softwareengineering</category>
      <category>programming</category>
    </item>
    <item>
      <title>Natural Aristoi</title>
      <dc:creator>Mike Georgeff</dc:creator>
      <pubDate>Wed, 03 Jun 2026 05:08:40 +0000</pubDate>
      <link>https://dev.to/georgeff/natural-aristoi-h6j</link>
      <guid>https://dev.to/georgeff/natural-aristoi-h6j</guid>
      <description>&lt;p&gt;Frameworks should earn their place in your application through demonstrated merit -- not convention, not network effect, not being the path of least resistance.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;Natural Aristoi is a concept championed by Thomas Jefferson describing a governing class defined by virtue and talent, not birth or privilege. Jefferson believed in small government, states' rights over federal, power closest to the people it governs -- local, visible, accountable. Distrust of centralized authority that accumulates complexity, imposes its will and becomes impossible to remove once entrenched.&lt;/p&gt;

&lt;p&gt;Small codebase. Components over monolith. Architecture closest to the problem it solves -- visible, intentional, accountable in its behavior. Distrust of the monolithic framework that grows, mandates, and makes decisions on your behalf whether you asked it or not.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem with Full Frameworks
&lt;/h3&gt;

&lt;p&gt;Frameworks force an all-or-nothing decision. Opinions are baked into every layer. This leads to abstractions that you did not ask for that hide what is actually happening. When your needs diverge, you're fighting the framework instead of engineering the solution.&lt;/p&gt;

&lt;p&gt;Ultimately engineering comes in second, the framework's conventions come first.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Composability Alternative
&lt;/h3&gt;

&lt;p&gt;Compose only what your application needs -- nothing more, nothing less. Each component earns its place by doing precisely one thing. Shared standards serve as the contract layer -- components speak to each other through interfaces, not implementations. In PHP, PSR standards serve this exact role -- defining interfaces that components agree on without mandating how any of them are implemented. The result is small, purposeful, replaceable pieces that assemble into something cohesive.&lt;/p&gt;

&lt;p&gt;The engineer decides the architecture, not the framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Define hard boundaries&lt;/strong&gt; -- boot is boot, run is run, this should be enforced, not suggested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Immutability as a constraint&lt;/strong&gt; -- the service layer freezes at boot, making predictability a guarantee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit over implicit&lt;/strong&gt; -- dependencies are declared, not located; registration is separated from resolution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pure transformation&lt;/strong&gt; -- the system transforms inputs, it does not reconfigure itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restraint as a signal&lt;/strong&gt; -- 165 lines for a DI container, because that's all it needs to be.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why this Matters
&lt;/h3&gt;

&lt;p&gt;Applications built this way are predictable, testable, and honest about their own state. Implementation changes do not cause a snowball effect, they are centralized and isolated. Good software earns its place, flawed software is easily removed.&lt;/p&gt;

&lt;p&gt;Engineers who understand their stack make better decisions.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>softwareengineering</category>
      <category>philosophy</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
