<?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: KOMOREBi</title>
    <description>The latest articles on DEV Community by KOMOREBi (@kmrb_works).</description>
    <link>https://dev.to/kmrb_works</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%2F4234693%2Fc541a747-256e-483e-9743-57c30ee798c7.png</url>
      <title>DEV Community: KOMOREBi</title>
      <link>https://dev.to/kmrb_works</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kmrb_works"/>
    <language>en</language>
    <item>
      <title>php -l passed. Every page still died.</title>
      <dc:creator>KOMOREBi</dc:creator>
      <pubDate>Mon, 09 Nov 2026 08:22:33 +0000</pubDate>
      <link>https://dev.to/kmrb_works/php-l-passed-every-page-still-died-5g90</link>
      <guid>https://dev.to/kmrb_works/php-l-passed-every-page-still-died-5g90</guid>
      <description>&lt;p&gt;This post was generated by an AI agent from the maintenance records of the person running this account. The incidents and numbers are real; the wording is not human-written.&lt;/p&gt;

&lt;p&gt;I look after old PHP sites in Japan. This autumn I worked on getting three of them onto PHP 8.x, for three different reasons: a critical security release, a server whose PHP changed underneath it (we still don't know who changed it), and a planned jump from 5.5.&lt;/p&gt;

&lt;p&gt;Two of them went live on 8.x. The first never did: its launch was called off, so what I say about it comes from the new server before release.&lt;/p&gt;

&lt;p&gt;The obvious checks passed more often than they should have. Here is what actually broke, with the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;php -l&lt;/code&gt; is not a migration test
&lt;/h2&gt;

&lt;p&gt;One site was going from 7.4 to 8.4. Its code still had PHP 4 habits in it. On the new server, every page that ran PHP stopped with this (class name changed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Non-static method Database::connect() cannot be called statically
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lint never mentions it. Here is a minimal version, which I re-ran on PHP 8.4.26 while writing this:&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Db&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;connect&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="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nc"&gt;Db&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;connect&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;php -l&lt;/code&gt; says &lt;code&gt;No syntax errors detected&lt;/code&gt;. Running it is a fatal error.&lt;/p&gt;

&lt;p&gt;We added &lt;code&gt;static&lt;/code&gt; to 221 methods. Check two things before you do that: the method must not touch &lt;code&gt;$this&lt;/code&gt; (none of ours did), and if a parent method is static, every override has to be static too, or you get a different fatal.&lt;/p&gt;

&lt;p&gt;Old-style constructors are worse, because nothing errors at all:&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Base&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;Base&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;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"set"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Item&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Base&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;$i&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;Item&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// NULL on PHP 8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A method named after its class stopped being a constructor in PHP 8. It is now an ordinary method that nobody calls, so you get an object that was never set up and no message about it. We renamed 45 of these to &lt;code&gt;__construct&lt;/code&gt;, along with the &lt;code&gt;parent::Base(&lt;/code&gt; calls.&lt;/p&gt;

&lt;p&gt;After those fixes, lint over all 648 files failed on exactly one, a file nothing loads, because of &lt;code&gt;$str{0}&lt;/code&gt;. That is a fair picture of what lint is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  44 pages out of 266
&lt;/h2&gt;

&lt;p&gt;On the second site, PHP went from 7.4 to 8.x with no record of anyone on our side changing it. The client reported that some detail pages were missing images and content.&lt;/p&gt;

&lt;p&gt;An empty XML element was being turned into an empty array, and the code passed it to &lt;code&gt;nl2br()&lt;/code&gt;. On 7.4 that is a warning and the page carries on. On 8.x it is a &lt;code&gt;TypeError&lt;/code&gt; and output stops right there.&lt;/p&gt;

&lt;p&gt;It only happened for records with one particular combination of fields: 44 of 266. A spot check of a few URLs could easily have missed it, since that's about 1 page in 6.&lt;/p&gt;

&lt;p&gt;We had the API responses saved for all 266 records, so we replayed them through PHP 8.1 and 8.4 in Docker with the network off. Before the fix there were 44 &lt;code&gt;TypeError&lt;/code&gt;s, same as production. After it there were 0, and the other 222 pages came out identical apart from a &lt;code&gt;?v=&lt;/code&gt; timestamp.&lt;/p&gt;

&lt;p&gt;The fix was to normalise empty arrays to empty strings right after loading, leaving out the six fields that are nested on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diff the output instead of clicking around
&lt;/h2&gt;

&lt;p&gt;The third site went from PHP 5.5 to 8.4. Before switching production, we ran both versions against the same data and compared 135 pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;121 identical&lt;/li&gt;
&lt;li&gt;4 differing only in a date line&lt;/li&gt;
&lt;li&gt;10 different, each with a reason we could write down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reading 10 diffs beats clicking through 135 pages and hoping you notice something.&lt;/p&gt;

&lt;p&gt;We also rehearsed the rollback on a test domain. Going back to 5.5 took 3 seconds and going forward to 8.4 took 2. It is a much calmer production switch when you already know those numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller things
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;mysqli throws now.&lt;/strong&gt; Since PHP 8.1 the default is exceptions. Old code that checks for &lt;code&gt;false&lt;/code&gt; needs &lt;code&gt;mysqli_report(MYSQLI_REPORT_OFF)&lt;/code&gt; until you rewrite it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep errors off the page for real.&lt;/strong&gt; Old code loves &lt;code&gt;ini_set('display_errors', 1)&lt;/code&gt;. In an FPM pool, &lt;code&gt;php_admin_flag[display_errors] = off&lt;/code&gt; cannot be overridden from code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect noise.&lt;/strong&gt; One pass over the first site's pages wrote thousands of Deprecated and Warning lines, 3,978 of them from passing &lt;code&gt;null&lt;/code&gt; to &lt;code&gt;preg_match()&lt;/code&gt;. Look at fatals first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the timezone.&lt;/strong&gt; We found this one reviewing the config, not from a failure: if the new config doesn't set &lt;code&gt;date.timezone&lt;/code&gt;, dates run in UTC.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;This is what I'd run through next time, with commands to find each of these. Run them from the project root.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before you touch anything
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Write down the current PHP version and how it is installed (distro package, panel, source).&lt;/li&gt;
&lt;li&gt;Find out who or what can change the PHP version. One of our sites changed without us.&lt;/li&gt;
&lt;li&gt;Save real input data you can replay (API responses, a database copy).&lt;/li&gt;
&lt;li&gt;Pick the URLs to compare, including rarely used record types.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What &lt;code&gt;php -l&lt;/code&gt; does catch
&lt;/h3&gt;

&lt;p&gt;Syntax such as &lt;code&gt;$str{0}&lt;/code&gt; fails here. Lint every file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.php'&lt;/span&gt; &lt;span class="nt"&gt;-print0&lt;/span&gt; | xargs &lt;span class="nt"&gt;-0&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt; php &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'No syntax errors'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What &lt;code&gt;php -l&lt;/code&gt; does not catch
&lt;/h3&gt;

&lt;p&gt;Functions removed in PHP 8:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s1"&gt;'\b(each|create_function|get_magic_quotes_(gpc|runtime))\s*\('&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.php'&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Old-style constructors, which silently stop running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.php'&lt;/span&gt; &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'
  FNR == 1 { c = "" }
  match($0, /class +[A-Za-z0-9_]+/) { c = substr($0, RSTART + 6, RLENGTH - 6); gsub(/ /, "", c) }
  c &amp;amp;&amp;amp; match($0, /function +&amp;amp;?[A-Za-z0-9_]+ *\(/) {
    f = substr($0, RSTART, RLENGTH); gsub(/function +&amp;amp;?| *\(/, "", f)
    if (tolower(f) == tolower(c)) print FILENAME ":" FNR ": " $0
  }'&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; +
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parent constructors called by class name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s1"&gt;'parent::[A-Z][A-Za-z0-9_]*\s*\('&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.php'&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; __construct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Static calls to methods that are not declared static. This lists the calls, and you check each target. Include templates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s1"&gt;'\b[A-Z][A-Za-z0-9_]*::[A-Za-z_][A-Za-z0-9_]*\s*\('&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.php'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.tpl'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.html'&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-vE&lt;/span&gt; &lt;span class="s1"&gt;'parent::|self::|static::'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Smarty &lt;code&gt;{php}&lt;/code&gt; tags, which newer Smarty does not allow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s1"&gt;'{php}'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.tpl'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.html'&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also check the installed version of old mail libraries such as PHPMailer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test with real data
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Replay every saved record through the new version, not just a few URLs.&lt;/li&gt;
&lt;li&gt;Run the old and new versions on the same data and diff the HTML. Ignore cache-busting values such as &lt;code&gt;?v=&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Give every remaining difference a written reason.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Switch
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Rehearse the rollback on a test domain and time it.&lt;/li&gt;
&lt;li&gt;After the switch, look at fatals first, then work through Warnings and Deprecated.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'PHP (Fatal|Parse) error|Uncaught'&lt;/span&gt; error.log | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three sites isn't a study, and your numbers will differ. If you've moved something old to PHP 8 and hit something that isn't on the list, I'd like to hear about it.&lt;/p&gt;

&lt;p&gt;Follow &lt;a class="mentioned-user" href="https://dev.to/kmrb_works"&gt;@kmrb_works&lt;/a&gt; here on DEV for more field notes from real PHP maintenance work. Also on &lt;a href="https://x.com/kmrb_works" rel="noopener noreferrer"&gt;X&lt;/a&gt; and &lt;a href="https://note.com/quirky_hosta1908" rel="noopener noreferrer"&gt;note&lt;/a&gt;, mostly in Japanese.&lt;/p&gt;

</description>
      <category>php</category>
      <category>legacy</category>
      <category>webdev</category>
      <category>migration</category>
    </item>
  </channel>
</rss>
