<?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: Bo Zeng</title>
    <description>The latest articles on DEV Community by Bo Zeng (@zengbo).</description>
    <link>https://dev.to/zengbo</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%2F4219855%2F44f8d92c-5ce3-4469-be1d-189befe8a12f.png</url>
      <title>DEV Community: Bo Zeng</title>
      <link>https://dev.to/zengbo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zengbo"/>
    <language>en</language>
    <item>
      <title>Two real WordPress plugins on PHP 8.4: what broke, and what I refused to auto-fix</title>
      <dc:creator>Bo Zeng</dc:creator>
      <pubDate>Sun, 01 Nov 2026 01:47:21 +0000</pubDate>
      <link>https://dev.to/zengbo/two-real-wordpress-plugins-on-php-84-what-broke-and-what-i-refused-to-auto-fix-bd4</link>
      <guid>https://dev.to/zengbo/two-real-wordpress-plugins-on-php-84-what-broke-and-what-i-refused-to-auto-fix-bd4</guid>
      <description>&lt;p&gt;PHP 8.2 stops getting security fixes on December 31, 2026. PHP 8.3 already left active support at the end of 2025. If you maintain a WordPress plugin, a theme, or an older PHP app, the hosting company is going to move you to 8.3 or 8.4 sooner or later, whether the code is ready or not.&lt;/p&gt;

&lt;p&gt;The hard part of that move is rarely "does it compile". It is three questions at once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What actually breaks on the new version?&lt;/li&gt;
&lt;li&gt;What can I fix automatically without changing behavior?&lt;/li&gt;
&lt;li&gt;How do I fix it without dropping the old PHP versions my users still run?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third question is the one most tools skip, and it is why I built &lt;a href="https://github.com/qiyuanlabs-ai/refit" rel="noopener noreferrer"&gt;Refit&lt;/a&gt;, a small open-source CLI with two commands: &lt;code&gt;refit scan&lt;/code&gt; and &lt;code&gt;refit fix&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just run Rector?
&lt;/h2&gt;

&lt;p&gt;Rector is excellent at rewriting PHP safely, and PHPCompatibility knows every removed and deprecated PHP feature. Refit runs both, plus &lt;code&gt;php -l&lt;/code&gt;. It does not reinvent the checks.&lt;/p&gt;

&lt;p&gt;The catch is Rector's PHP version sets. They don't only remove breakages, they also modernize: &lt;code&gt;match&lt;/code&gt;, &lt;code&gt;readonly&lt;/code&gt;, &lt;code&gt;str_contains&lt;/code&gt;. That is great for an app that only runs on PHP 8. It is a disaster for a plugin whose header says &lt;code&gt;Requires PHP: 7.2&lt;/code&gt;, because the "upgraded" code now fails on the PHP version your users are still on.&lt;/p&gt;

&lt;p&gt;So Refit does three things differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It reads the oldest PHP version the code must keep supporting from the plugin or theme header (&lt;code&gt;Requires PHP&lt;/code&gt;), &lt;code&gt;readme.txt&lt;/code&gt; or &lt;code&gt;composer.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It only uses a curated list of compatibility rules: 54 in total, 52 taken from Rector and 2 of its own.&lt;/li&gt;
&lt;li&gt;Each rule runs only when the problem exists on your target version &lt;strong&gt;and&lt;/strong&gt; the code it writes still runs on your minimum version.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real report 1: an events plugin
&lt;/h2&gt;

&lt;p&gt;Here is what &lt;code&gt;refit scan&lt;/code&gt; found in a WordPress events plugin with 50 PHP files, targeting PHP 8.4. These numbers come straight from its Markdown report:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Your code&lt;/th&gt;
&lt;th&gt;Bundled libraries&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Breaks&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deprecated&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed by &lt;code&gt;refit fix&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;not changed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zero fatal errors, which is good news. But 17 spots can fail with unexpected input, and all of them are the same thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The PHP 8.3 change nobody talks about
&lt;/h3&gt;

&lt;p&gt;Since PHP 8.3, &lt;code&gt;DateTime::modify()&lt;/code&gt; throws a &lt;code&gt;DateMalformedStringException&lt;/code&gt; when it can't parse the string. Before, it returned &lt;code&gt;false&lt;/code&gt; with a warning. So this very common pattern, taken from Refit's test fixture:&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;function&lt;/span&gt; &lt;span class="n"&gt;legacy_next_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$when&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$date&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;DateTime&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="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$date&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;modify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$when&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="kc"&gt;null&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;$date&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;still compiles, uses no removed function, and now dies with an uncaught exception the first time &lt;code&gt;$when&lt;/code&gt; is an empty string from a URL or a saved setting.&lt;/p&gt;

&lt;p&gt;Nothing in that code is "removed" or "deprecated", so a check for removed features has nothing to report. Refit ships its own PHP_CodeSniffer sniff for it. To keep the noise down, it only reports &lt;code&gt;-&amp;gt;modify()&lt;/code&gt; calls that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;are in a file that works with DateTime objects,&lt;/li&gt;
&lt;li&gt;get an argument built at run time (a fixed string like &lt;code&gt;'+1 day'&lt;/code&gt; either always works or always fails in testing),&lt;/li&gt;
&lt;li&gt;and are not already inside a &lt;code&gt;try&lt;/code&gt; block.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This version from the same fixture is not reported:&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;function&lt;/span&gt; &lt;span class="n"&gt;legacy_safe_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$when&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&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;modify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$when&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&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="kc"&gt;null&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are reported as &lt;strong&gt;Check&lt;/strong&gt;, not &lt;strong&gt;Breaks&lt;/strong&gt;, because whether they fail depends on the data. A person has to decide what the right fallback is, so Refit doesn't try to write one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic properties, and the WP_Widget problem
&lt;/h3&gt;

&lt;p&gt;The 4 auto-fixable items in the events plugin are dynamic properties, deprecated in PHP 8.2. Refit declares them in the class as plain public properties. That works when the parent class is in your code. When it isn't, as with this class from Refit's test fixture:&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;class&lt;/span&gt; &lt;span class="nc"&gt;Legacy_Widget&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;WP_Widget&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="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;widget_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Legacy'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refit can't see &lt;code&gt;WP_Widget&lt;/code&gt;, so it can't know whether declaring the property would clash with it. It adds &lt;code&gt;#[\AllowDynamicProperties]&lt;/code&gt; instead. It does the same when the class or one of its parents defines &lt;code&gt;__get&lt;/code&gt;, &lt;code&gt;__isset&lt;/code&gt; or &lt;code&gt;__unset&lt;/code&gt;, because a declared property would stop those magic methods from running for that name. Rector's own rule for this (&lt;code&gt;CompleteDynamicPropertiesRector&lt;/code&gt;) is deprecated, so this is one of Refit's two own rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problems that were already there
&lt;/h3&gt;

&lt;p&gt;The report also has a section called "Already a problem on PHP 7.2". For the events plugin it holds 6 findings: &lt;code&gt;mysql_*&lt;/code&gt; calls in the debug page and &lt;code&gt;$this&lt;/code&gt; used in a plain function. These already fail on the oldest PHP version the plugin supports, so the upgrade doesn't change them. Keeping them apart means the headline answers the question you actually asked: what does moving to 8.4 break?&lt;/p&gt;

&lt;h2&gt;
  
  
  Real report 2: a social feed plugin, and the fix Refit won't make
&lt;/h2&gt;

&lt;p&gt;A social media feed plugin (46 PHP files) came back almost clean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Runs on PHP 8.4, with 1 deprecation warning that will fill the error log and break in a later PHP version.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The one problem is an implicitly nullable parameter, deprecated in PHP 8.4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Implicitly marking a parameter as nullable is deprecated since PHP 8.4. Update
the type to be explicitly nullable instead. Found implicitly nullable parameter:
$next_page_token.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is trivial: write &lt;code&gt;?Type $x = null&lt;/code&gt;. Rector has a rule for it. But the report says "Fixed by &lt;code&gt;refit fix&lt;/code&gt;: 0".&lt;/p&gt;

&lt;p&gt;That's on purpose. The plugin declares PHP 7.0 as its minimum, and the &lt;code&gt;?Type&lt;/code&gt; syntax needs PHP 7.1. Writing it would turn a deprecation notice on PHP 8.4 into a parse error on PHP 7.0. So Refit reports the issue, explains the fix, and leaves the decision to the maintainer: raise the minimum to 7.1, or live with the notice for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing without breaking things
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;refit fix&lt;/code&gt; applies the safe fixes in place. It's deliberately careful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It refuses to run unless the project is a git repository with no uncommitted changes, so you can always &lt;code&gt;git diff&lt;/code&gt; and undo. (&lt;code&gt;--force&lt;/code&gt; skips this.)&lt;/li&gt;
&lt;li&gt;It runs &lt;code&gt;php -l&lt;/code&gt; on every changed file before and after. A file that compiled before and doesn't after is restored to the original.&lt;/li&gt;
&lt;li&gt;Bundled libraries (Composer &lt;code&gt;vendor/&lt;/code&gt;, Freemius, TCPDF, Action Scheduler, CMB2 and others) are left alone unless you pass &lt;code&gt;--include-libraries&lt;/code&gt;. The fix there is to update the library.&lt;/li&gt;
&lt;li&gt;Tabs or spaces are kept the way the project uses them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test suite runs &lt;code&gt;scan&lt;/code&gt; and &lt;code&gt;fix&lt;/code&gt; end to end on a small sample plugin. Part of it looks like this before the fix:&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;save&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;$data&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;','&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;After &lt;code&gt;refit fix&lt;/code&gt;, the test checks that &lt;code&gt;$data{0}&lt;/code&gt; became &lt;code&gt;$data[0]&lt;/code&gt;, that &lt;code&gt;implode($data, ',')&lt;/code&gt; became &lt;code&gt;implode(',', $data)&lt;/code&gt;, that the parameter became &lt;code&gt;?array $data = null&lt;/code&gt; (the fixture declares PHP 7.2, so &lt;code&gt;?Type&lt;/code&gt; is allowed), that &lt;code&gt;create_function()&lt;/code&gt; and &lt;code&gt;each()&lt;/code&gt; are gone, that the bundled library file is byte-for-byte unchanged, and that every changed file still passes &lt;code&gt;php -l&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rector rules I removed after they broke real code
&lt;/h2&gt;

&lt;p&gt;This is the part I found most interesting. I started with Rector's per-version rules and ran them against real WordPress plugins. These five changed behavior, so Refit doesn't use them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SensitiveConstantNameRector&lt;/strong&gt; upper-cases constants it can't find a &lt;code&gt;define()&lt;/code&gt; for. That breaks mixed-case constants defined in another file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AssignArrayToStringRector&lt;/strong&gt; turns &lt;code&gt;$x = ''&lt;/code&gt; into &lt;code&gt;$x = []&lt;/code&gt; even when &lt;code&gt;$x&lt;/code&gt; is later used as a string. It broke a SQL builder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RemoveExtraParametersRector&lt;/strong&gt; drops arguments that functions may read with &lt;code&gt;func_get_args()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WrapVariableVariableNameInCurlyBracesRector&lt;/strong&gt; restores PHP 5 semantics in code that has been running on PHP 7 ever since.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;StringifyStrNeedlesRector&lt;/strong&gt; only adds &lt;code&gt;(string)&lt;/code&gt; casts that PHP 8 already implies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are bugs in Rector. They're correct for the code Rector expects. Plugin code that has been patched across ten PHP versions just isn't that code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it can't do
&lt;/h2&gt;

&lt;p&gt;Refit is static analysis. It never runs your code or loads your autoloader, so it can't see problems that depend on data, like passing &lt;code&gt;null&lt;/code&gt; to a string function on PHP 8.1. There's an opt-in &lt;code&gt;--all-rules&lt;/code&gt; flag that casts those arguments everywhere, but it's noisy, so it's off by default.&lt;/p&gt;

&lt;p&gt;It also only scans &lt;code&gt;.php&lt;/code&gt;, &lt;code&gt;.inc&lt;/code&gt; and &lt;code&gt;.phtml&lt;/code&gt; files, so Drupal's &lt;code&gt;.module&lt;/code&gt; and &lt;code&gt;.install&lt;/code&gt; files are not checked yet. And PHPCompatibility's PHP 8 checks only exist in its 10.0 alpha, which is why the install command has to name it.&lt;/p&gt;

&lt;p&gt;Run your tests and click through the plugin on the target PHP version after fixing. The report says so too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require &lt;span class="nt"&gt;--dev&lt;/span&gt; qiyuanlabs/refit &lt;span class="s2"&gt;"phpcompatibility/php-compatibility:^10.0@alpha"&lt;/span&gt;

vendor/bin/refit scan &lt;span class="nt"&gt;--php&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8.4 &lt;span class="nt"&gt;--html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;report.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--markdown&lt;/code&gt; and &lt;code&gt;--json&lt;/code&gt; work the same way. In CI, &lt;code&gt;refit scan --php=8.4 --fail-on=breaks&lt;/code&gt; exits with code 1 when your own code has anything that breaks on the target version. It needs PHP 8.1 or newer to run, and the code you scan can target anything from PHP 5.3 up.&lt;/p&gt;

&lt;p&gt;It's MIT licensed and at version 0.1.1, so I'd really like to hear where it's wrong: false positives from the DateTime check, rules you think are missing, or plugins where a fix changed behavior. Issues are open on &lt;a href="https://github.com/qiyuanlabs-ai/refit" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I've been writing PHP for 18 years, and these days I also do fixed-price PHP upgrades at &lt;a href="https://php.qiyuanlabs.ai" rel="noopener noreferrer"&gt;QiYuan Labs&lt;/a&gt; for the projects that need more than a tool.&lt;/p&gt;

</description>
      <category>php</category>
      <category>wordpress</category>
      <category>opensource</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
