<?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: Benyamin Khalife</title>
    <description>The latest articles on DEV Community by Benyamin Khalife (@benkhalife).</description>
    <link>https://dev.to/benkhalife</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%2F3141137%2Fc20e0dbe-bc02-4da9-b419-2d7e81716203.jpg</url>
      <title>DEV Community: Benyamin Khalife</title>
      <link>https://dev.to/benkhalife</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/benkhalife"/>
    <language>en</language>
    <item>
      <title>Make Your Buttons Dodge the Cursor with a Wrium Plugin</title>
      <dc:creator>Benyamin Khalife</dc:creator>
      <pubDate>Sat, 03 Oct 2026 11:57:53 +0000</pubDate>
      <link>https://dev.to/benkhalife/make-your-buttons-dodge-the-cursor-with-a-wrium-plugin-2fhc</link>
      <guid>https://dev.to/benkhalife/make-your-buttons-dodge-the-cursor-with-a-wrium-plugin-2fhc</guid>
      <description>&lt;p&gt;Most buttons just sit there and wait to be clicked. This one doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrium Evasive Button&lt;/strong&gt; is a small plugin for &lt;a href="https://wrium.dev" rel="noopener noreferrer"&gt;Wrium&lt;/a&gt; that makes a button (or any interactive element) smoothly dodge the cursor when you get close. It's silly on purpose, but it turns out to be surprisingly useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why would you want this?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smart form validation:&lt;/strong&gt; the submit button stays out of reach until the required fields are valid&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gamified forms and playful login screens&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive confirmations&lt;/strong&gt; where the "wrong" choice is hard to click&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Smooth, physics-style gliding along walls, with no teleporting or sticking at edges&lt;/li&gt;
&lt;li&gt;Movement limited to the parent, the viewport, or any element you choose&lt;/li&gt;
&lt;li&gt;Limits via &lt;code&gt;maxRadius&lt;/code&gt;, &lt;code&gt;duration&lt;/code&gt;, and &lt;code&gt;maxAttempts&lt;/code&gt;, so the button eventually gives up and settles&lt;/li&gt;
&lt;li&gt;Reactive toggling: &lt;code&gt;v-evade="{ active: !isFormValid }"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;State classes and attributes for styling: &lt;code&gt;.is-evading&lt;/code&gt;, &lt;code&gt;.is-settled&lt;/code&gt;, &lt;code&gt;data-state&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@evade&lt;/code&gt;, &lt;code&gt;@settle&lt;/code&gt;, and &lt;code&gt;@giveup&lt;/code&gt; events&lt;/li&gt;
&lt;li&gt;Respects &lt;code&gt;prefers-reduced-motion&lt;/code&gt; and keeps keyboard navigation working (&lt;code&gt;Tab&lt;/code&gt; + &lt;code&gt;Enter&lt;/code&gt;/&lt;code&gt;Space&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Zero dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @wrium/evasive-button
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@wrium/wrium&lt;/code&gt; is a peer dependency, so install it too if you haven't already.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;v-evade&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Try to Click Me!&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@wrium/wrium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;EvasiveButtonPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@wrium/evasive-button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({}))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;EvasiveButtonPlugin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The button now runs away from your mouse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical version: form validation
&lt;/h2&gt;

&lt;p&gt;Bind &lt;code&gt;active&lt;/code&gt; to your form state. While the form is invalid the button dodges, and once it's valid the button glides back home, ready to be clicked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"button-arena"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
        &lt;span class="na"&gt;v-evade=&lt;/span&gt;&lt;span class="s"&gt;"{ active: !isFormValid, bounds: '#button-arena', maxRadius: 150 }"&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        {{ isFormValid ? 'Log In' : 'Fill fields first!' }}
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also style it based on its state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.login-btn.is-evading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e11d48&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;h2&gt;
  
  
  A note on UX and accessibility
&lt;/h2&gt;

&lt;p&gt;An evasive button is a fun UX trick, so use it with care. The plugin helps by respecting reduced-motion preferences, letting keyboard users submit normally, and giving up after a duration or number of attempts if you set those options. I'd still keep it for playful or low-stakes moments rather than critical flows.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/wrium/evasive-button" rel="noopener noreferrer"&gt;github.com/wrium/evasive-button&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Wrium: &lt;a href="https://wrium.dev" rel="noopener noreferrer"&gt;wrium.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's MIT licensed, and the repo includes unit tests (Vitest) and end-to-end tests (Playwright). Issues and PRs are welcome. I'd love to see the creative ways you use it!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>ux</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Real-Time Password Strength Checking for Wrium</title>
      <dc:creator>Benyamin Khalife</dc:creator>
      <pubDate>Fri, 02 Oct 2026 10:57:02 +0000</pubDate>
      <link>https://dev.to/benkhalife/real-time-password-strength-checking-for-wrium-1m37</link>
      <guid>https://dev.to/benkhalife/real-time-password-strength-checking-for-wrium-1m37</guid>
      <description>&lt;p&gt;We've all seen it: a signup form that happily accepts &lt;code&gt;password123&lt;/code&gt; and only complains after the server says no. Or worse, it doesn't complain at all.&lt;/p&gt;

&lt;p&gt;I wanted a tiny, drop-in way to give users feedback &lt;em&gt;while they type&lt;/em&gt;, so I built &lt;strong&gt;Wrium Password Strength&lt;/strong&gt;, a plugin for &lt;a href="https://wrium.dev" rel="noopener noreferrer"&gt;Wrium&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;It adds a single directive, &lt;code&gt;v-password-strength&lt;/code&gt;, that checks a password on every input and tells you whether it's good enough. Under the hood it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Blocks exact or substring matches against a common-password list&lt;/li&gt;
&lt;li&gt;Flags sequential runs (&lt;code&gt;abc&lt;/code&gt;, &lt;code&gt;123&lt;/code&gt;) and repeated characters (&lt;code&gt;aaaa&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Scores character variety: lowercase, uppercase, digits, symbols&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plugin itself has zero dependencies. It only needs Wrium as a peer dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @wrium/password-strength
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@wrium/wrium&lt;/code&gt; is a peer dependency, so install it too if you haven't already.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Register the plugin and set your rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@wrium/wrium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PasswordStrengthPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@wrium/password-strength&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;strength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PasswordStrengthPlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;good&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use the directive in your template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt; &lt;span class="na"&gt;v-password-strength=&lt;/span&gt;&lt;span class="s"&gt;"strength"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Strength: {{ strength?.label }}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;:disabled=&lt;/span&gt;&lt;span class="s"&gt;"!strength?.valid"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Submit&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On every keystroke the directive writes &lt;code&gt;{ label, valid, reasons }&lt;/code&gt; into your &lt;code&gt;ref&lt;/code&gt;, so you can show a label, list hints, or disable the submit button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not using the directive?
&lt;/h2&gt;

&lt;p&gt;The same logic is exported as a pure function with no DOM and no reactivity, handy for validating before a request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;assessPassword&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@wrium/password-strength&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;assessPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;correcthorsebatterystaple&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; { label: 'fair', valid: true, reasons: ['Mix letters, numbers, and symbols'] }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  An honest note
&lt;/h2&gt;

&lt;p&gt;The built-in blocklist is short. It catches the obvious offenders, but it's not a breach-database lookup. For high-stakes apps, pair it with a server-side check such as the Have I Been Pwned API. Think of this plugin as a fast, friendly first line of feedback, not your only line of defense.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/wrium/password-strength" rel="noopener noreferrer"&gt;github.com/wrium/password-strength&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Wrium: &lt;a href="https://wrium.dev" rel="noopener noreferrer"&gt;wrium.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's MIT licensed. Feedback, issues, and PRs are very welcome. If you try it, I'd love to hear how it goes!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Wrium: A Tiny Reactive JavaScript Library With No Build Step</title>
      <dc:creator>Benyamin Khalife</dc:creator>
      <pubDate>Wed, 30 Sep 2026 11:56:43 +0000</pubDate>
      <link>https://dev.to/benkhalife/wrium-a-tiny-reactive-javascript-library-with-no-build-step-4ko9</link>
      <guid>https://dev.to/benkhalife/wrium-a-tiny-reactive-javascript-library-with-no-build-step-4ko9</guid>
      <description>&lt;p&gt;Have you ever wanted a button that updates some text on a page, and ended up setting up a bundler, a compiler, and a folder full of config files?&lt;/p&gt;

&lt;p&gt;I felt that way too. So I built &lt;strong&gt;Wrium&lt;/strong&gt;, a small JavaScript library for making reactive user interfaces. You write normal HTML, add a few special attributes, and the page updates itself when your data changes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://wrium.dev" rel="noopener noreferrer"&gt;wrium.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/wrium/wrium" rel="noopener noreferrer"&gt;github.com/wrium/wrium&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;code&gt;@wrium/wrium&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is "reactive" anyway?
&lt;/h2&gt;

&lt;p&gt;"Reactive" means one simple thing: &lt;strong&gt;when your data changes, the screen updates automatically.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without reactivity, you write code like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#count&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You have to change the data &lt;em&gt;and&lt;/em&gt; update the page by hand. With a reactive library, you only change the data. The library updates the page for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet Wrium
&lt;/h2&gt;

&lt;p&gt;Wrium is inspired by Vue. If you know Vue's &lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;computed&lt;/code&gt;, &lt;code&gt;v-if&lt;/code&gt;, or &lt;code&gt;v-for&lt;/code&gt;, you will feel at home. But Wrium is much smaller and simpler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero dependencies&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No build step.&lt;/strong&gt; Add a script tag and go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;About 10.9 KB&lt;/strong&gt; for the whole core (minified)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript declarations&lt;/strong&gt; included&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MIT license&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not trying to replace big frameworks. It is for the times when a big framework is too much.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your first app in 20 lines
&lt;/h2&gt;

&lt;p&gt;Here is a working counter. Save it as an HTML file and open it in a browser (use a local server, because ES modules need one).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;{{ title }}&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Current count: {{ count }}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"increment"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"decrement"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;-&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@wrium/wrium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Counter App&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;increment&lt;/span&gt; &lt;span class="o"&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="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decrement&lt;/span&gt; &lt;span class="o"&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="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;--&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="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decrement&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ref(0)&lt;/code&gt; creates a reactive box that holds the number 0.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{{ count }}&lt;/code&gt; in the HTML shows the value of that box.&lt;/li&gt;
&lt;li&gt;When you click a button, &lt;code&gt;count.value&lt;/code&gt; changes and the text on the page updates by itself.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There was no compiler and no bundler. It is just HTML and JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four building blocks
&lt;/h2&gt;

&lt;p&gt;Wrium's reactivity is built on four functions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ref&lt;/code&gt;&lt;/strong&gt; holds a single value, such as a number, a string, or even an object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;John&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Jane&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the page updates&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;reactive&lt;/code&gt;&lt;/strong&gt; makes a whole object or array reactive, including nested properties.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reactive&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;John&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;todos&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Jane&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// reactive&lt;/span&gt;
&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Learn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// also reactive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;computed&lt;/code&gt;&lt;/strong&gt; creates a value that is calculated from other values. It only recalculates when something it depends on changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;John&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Doe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;&lt;code&gt;watchEffect&lt;/code&gt;&lt;/strong&gt; runs a function right away, then runs it again whenever the reactive values it uses change. You never have to list dependencies by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;watchEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Count is:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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;h2&gt;
  
  
  Templates: HTML that reacts
&lt;/h2&gt;

&lt;p&gt;Wrium reads your HTML directly and understands the directives you already know from Vue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Show or hide --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"score &amp;gt;= 90"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Excellent!&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;v-else&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Keep trying!&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Repeat --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"item in items"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"item.id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ item.name }}&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Two-way binding for inputs --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Events, with modifiers --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;submit.prevent=&lt;/span&gt;&lt;span class="s"&gt;"save"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;keyup.enter=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Bind attributes and classes --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;:disabled=&lt;/span&gt;&lt;span class="s"&gt;"isLoading"&lt;/span&gt; &lt;span class="na"&gt;:class=&lt;/span&gt;&lt;span class="s"&gt;"{ active: isActive }"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Save&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supported directives include &lt;code&gt;v-if&lt;/code&gt;, &lt;code&gt;v-else-if&lt;/code&gt;, &lt;code&gt;v-else&lt;/code&gt;, &lt;code&gt;v-for&lt;/code&gt;, &lt;code&gt;v-model&lt;/code&gt;, &lt;code&gt;v-show&lt;/code&gt;, &lt;code&gt;v-text&lt;/code&gt;, &lt;code&gt;v-bind&lt;/code&gt; (&lt;code&gt;:&lt;/code&gt;), and &lt;code&gt;v-on&lt;/code&gt; (&lt;code&gt;@&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example: a Todo list
&lt;/h2&gt;

&lt;p&gt;Every framework needs a Todo list example, so here is Wrium's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"newTodo"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;keyup.enter=&lt;/span&gt;&lt;span class="s"&gt;"addTodo"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Add todo"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"addTodo"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"todo in todos"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"todo.id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;:class=&lt;/span&gt;&lt;span class="s"&gt;"{ done: todo.done }"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ todo.text }}&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"removeTodo(todo.id)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;×&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;{{ remaining }} of {{ todos.length }} remaining&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reactive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;computed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@wrium/wrium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newTodo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;todos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reactive&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addTodo&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="nx"&gt;newTodo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newTodo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;newTodo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&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;function&lt;/span&gt; &lt;span class="nf"&gt;removeTodo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;newTodo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;addTodo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;removeTodo&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;State, list rendering, a computed counter, and event handling, all in one small file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Components
&lt;/h2&gt;

&lt;p&gt;You can reuse markup with &lt;code&gt;app.component()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;todo-item&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;li :class="{ done }"&amp;gt;{{ text }}&amp;lt;/li&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;todo-item&lt;/span&gt; &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"t in todos"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"t.id"&lt;/span&gt; &lt;span class="na"&gt;:text=&lt;/span&gt;&lt;span class="s"&gt;"t.text"&lt;/span&gt; &lt;span class="na"&gt;:done=&lt;/span&gt;&lt;span class="s"&gt;"t.done"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/todo-item&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every attribute on the tag becomes a prop, and props stay in sync with the parent when you bind them with &lt;code&gt;:&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugins: add your own directives
&lt;/h2&gt;

&lt;p&gt;Wrium's built-in directives are registered the same way plugins register theirs. Built-ins get no special treatment. Here is a tiny plugin that adds a &lt;code&gt;v-focus&lt;/code&gt; directive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FocusPlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;directive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;focus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&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="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({})).&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FocusPlugin&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some features ship as &lt;strong&gt;optional plugins&lt;/strong&gt; instead of core. For example, &lt;code&gt;v-html&lt;/code&gt; is opt-in because inserting raw HTML can cause XSS problems if you use untrusted content. Wrium makes you choose it on purpose. There are also plugins for password strength checking and for draggable elements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;Wrium is young, and I would rather tell you now than surprise you later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Directives, components, and hooks are global, not scoped per app instance.&lt;/li&gt;
&lt;li&gt;There are no slots and no dedicated &lt;code&gt;emit&lt;/code&gt;. A function passed as a prop does that job.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reactive()&lt;/code&gt; does not handle &lt;code&gt;Map&lt;/code&gt; and &lt;code&gt;Set&lt;/code&gt; specially yet.&lt;/li&gt;
&lt;li&gt;It needs &lt;code&gt;Proxy&lt;/code&gt; support, so there is no Internet Explorer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When should you use Wrium?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A good fit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small interactive widgets on an existing page&lt;/li&gt;
&lt;li&gt;Prototypes and demos&lt;/li&gt;
&lt;li&gt;Learning how reactivity works, since the library is small enough to read&lt;/li&gt;
&lt;li&gt;Projects where you really do not want a build step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Probably not the best fit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large apps that need routing, state management, SSR, and a big ecosystem. Use Vue, React, or Svelte there.&lt;/li&gt;
&lt;/ul&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;npm &lt;span class="nb"&gt;install&lt;/span&gt; @wrium/wrium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or read the docs at &lt;a href="https://wrium.dev/docs/introduction" rel="noopener noreferrer"&gt;wrium.dev&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Wrium started as an experiment called Zog.js, and it just reached v1.0.0. If you try it, I would love to hear your feedback. Open an issue, send a PR, or drop a star on &lt;a href="https://github.com/wrium/wrium" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. And tell me in the comments: what is the smallest thing you would build with it?&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>beginners</category>
    </item>
    <item>
      <title>What Is Vibe Coding? The Real Pros and Cons</title>
      <dc:creator>Benyamin Khalife</dc:creator>
      <pubDate>Thu, 17 Sep 2026 13:08:08 +0000</pubDate>
      <link>https://dev.to/benkhalife/what-is-vibe-coding-the-real-pros-and-cons-36fi</link>
      <guid>https://dev.to/benkhalife/what-is-vibe-coding-the-real-pros-and-cons-36fi</guid>
      <description>&lt;p&gt;Thanks to the rise of AI coding tools, &lt;strong&gt;vibe coding&lt;/strong&gt; has become one of the most talked-about ideas in software development. The basic idea is simple: describe what you want in natural language, let an AI generate the code, and iterate based on the result.&lt;/p&gt;

&lt;p&gt;That can feel almost magical. You can build a website, a prototype, or even a small application without writing every line of code yourself.&lt;/p&gt;

&lt;p&gt;But there is an important distinction: &lt;strong&gt;using AI to write code is not automatically vibe coding&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The term was introduced by Andrej Karpathy in February 2025 to describe a much more hands-off approach, where the developer relies heavily on AI-generated code, often without reading or fully understanding what was produced.&lt;/p&gt;

&lt;p&gt;That distinction matters because AI-assisted development can be extremely useful. The real question is what happens when you rely on the same approach for a product that has real users, real money, and real consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Vibe Coding Actually Works
&lt;/h2&gt;

&lt;p&gt;Vibe coding isn't inherently bad. In the right context, it can be extremely effective.&lt;/p&gt;

&lt;p&gt;It works particularly well for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building a quick prototype to test an idea&lt;/li&gt;
&lt;li&gt;Creating a proof of concept before committing serious resources&lt;/li&gt;
&lt;li&gt;Small personal tools with no sensitive data and few users&lt;/li&gt;
&lt;li&gt;Experimenting with unfamiliar technologies&lt;/li&gt;
&lt;li&gt;Learning basic programming concepts and software development workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For these use cases, speed is often more important than long-term maintainability.&lt;/p&gt;

&lt;p&gt;If a weekend project breaks six months later, that may not matter.&lt;/p&gt;

&lt;p&gt;The problem begins when the same approach is used to build the actual product a business depends on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Becomes Risky for Startups and Business Sites
&lt;/h2&gt;

&lt;p&gt;A production application has requirements that go far beyond "does the page work?"&lt;/p&gt;

&lt;p&gt;It needs proper authentication and authorization, secure handling of secrets, database access controls, input validation, error handling, logging, testing, backups, monitoring, and a codebase that someone can maintain six months from now.&lt;/p&gt;

&lt;p&gt;AI coding tools can generate functional implementations very quickly, but &lt;strong&gt;functional code is not the same thing as secure or maintainable software&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An AI-generated application may work perfectly during a demo while still containing problems that only appear with real users: authorization mistakes, exposed credentials, unsafe assumptions, missing edge cases, weak database rules, or fragile integrations.&lt;/p&gt;

&lt;p&gt;The bigger issue is often not the AI itself. It's the lack of technical understanding behind the person deploying the result.&lt;/p&gt;

&lt;p&gt;When the person building the product doesn't understand the architecture, debugging becomes difficult. Fixing one problem can also introduce another because nobody has a clear mental model of how the system actually works.&lt;/p&gt;

&lt;p&gt;That creates a dangerous maintenance cycle: the application grows, the code becomes harder to understand, and every new change becomes more dependent on the AI that created it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Real-World Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Enrichlead
&lt;/h3&gt;

&lt;p&gt;One widely discussed example involved &lt;strong&gt;Enrichlead&lt;/strong&gt;, a SaaS product built by its founder using Cursor with no handwritten code.&lt;/p&gt;

&lt;p&gt;Shortly after launch, the founder reported that the application was being abused: API usage was being consumed, users were bypassing the subscription system, and unwanted data was being created in the database. He also explained that he wasn't technical, which made diagnosing the problems much harder.&lt;/p&gt;

&lt;p&gt;The important lesson isn't that AI "failed."&lt;/p&gt;

&lt;p&gt;The system did what it was asked to do: it produced something that worked.&lt;/p&gt;

&lt;p&gt;The missing part was the engineering judgment required to answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is authentication enforced on the server?&lt;/li&gt;
&lt;li&gt;Can a user modify client-side values to unlock paid functionality?&lt;/li&gt;
&lt;li&gt;Where are API keys stored?&lt;/li&gt;
&lt;li&gt;What happens when someone deliberately abuses the application?&lt;/li&gt;
&lt;li&gt;What protections exist around database writes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are software engineering and security questions, not just coding questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A CMS That Was Never Actually Connected
&lt;/h3&gt;

&lt;p&gt;A quieter example comes from developer &lt;strong&gt;Dominik Gronkiewicz&lt;/strong&gt;, who described taking over a project built by a previous team.&lt;/p&gt;

&lt;p&gt;The website looked finished: clean design, smooth animations, and all the expected pages.&lt;/p&gt;

&lt;p&gt;But under the hood, a Sanity CMS subscription had been paid for months without actually being connected to the website. Headlines, team biographies, and other content were hardcoded directly inside JSX components, leaving the client unable to edit even a single sentence without a developer.&lt;/p&gt;

&lt;p&gt;This is a different kind of failure.&lt;/p&gt;

&lt;p&gt;Nothing was necessarily "broken" from a visual perspective. The site worked.&lt;/p&gt;

&lt;p&gt;But the architecture did not match the business requirement.&lt;/p&gt;

&lt;p&gt;A CMS exists so non-developers can manage content. Paying for a CMS and then hardcoding all the content defeats its purpose.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of problem that can be missed when the goal is simply to make the application look finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Maintenance Problem Is Often Bigger Than the Initial Build
&lt;/h2&gt;

&lt;p&gt;One of the biggest advantages of vibe coding is also one of its biggest weaknesses: &lt;strong&gt;you can create a lot of software before you fully understand what you created&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is fine for experimentation.&lt;/p&gt;

&lt;p&gt;It becomes expensive when the application needs continuous development.&lt;/p&gt;

&lt;p&gt;Eventually, someone has to answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why is this request slow?&lt;/li&gt;
&lt;li&gt;Why can this user access that data?&lt;/li&gt;
&lt;li&gt;Why did this deployment break another feature?&lt;/li&gt;
&lt;li&gt;Which package is responsible for this behavior?&lt;/li&gt;
&lt;li&gt;Where is this secret being used?&lt;/li&gt;
&lt;li&gt;How should this database change be migrated safely?&lt;/li&gt;
&lt;li&gt;What happens when traffic increases tenfold?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, the question is no longer "Can AI build this?"&lt;/p&gt;

&lt;p&gt;The question becomes "Does someone understand the system well enough to operate and evolve it?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Experienced Developers Still Matter
&lt;/h2&gt;

&lt;p&gt;A professional developer does much more than write syntax.&lt;/p&gt;

&lt;p&gt;They make architectural decisions, think about security, understand trade-offs, design for maintainability, test failure scenarios, review dependencies, debug production problems, and plan for the future.&lt;/p&gt;

&lt;p&gt;That doesn't mean experienced developers should avoid AI.&lt;/p&gt;

&lt;p&gt;Quite the opposite.&lt;/p&gt;

&lt;p&gt;AI can be a powerful development tool when it is used as an accelerator rather than as a replacement for engineering judgment.&lt;/p&gt;

&lt;p&gt;There is a major difference between:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"AI wrote the code, so I trust it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"AI wrote most of the code, and I understand, test, review, and take responsibility for what ships."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second approach is much closer to modern &lt;strong&gt;AI-assisted software development&lt;/strong&gt; than pure vibe coding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Vibe coding is excellent for exploring ideas, building prototypes, and creating small projects quickly.&lt;/p&gt;

&lt;p&gt;For production systems, the risks increase significantly when nobody involved fully understands the generated code and architecture.&lt;/p&gt;

&lt;p&gt;The real issue isn't whether AI wrote the code.&lt;/p&gt;

&lt;p&gt;The real issue is &lt;strong&gt;who is responsible for understanding the system, securing it, maintaining it, and fixing it when something goes wrong&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a startup or business website, AI can absolutely be part of that process. But it shouldn't be the only engineer in the room.&lt;/p&gt;

&lt;p&gt;At Webrium, we use modern development tools, including AI, to speed up development while keeping architecture, security, maintainability, and human review at the center of the process.&lt;/p&gt;

&lt;p&gt;If you're building something real and want a second pair of experienced eyes on it, feel free to reach out — &lt;strong&gt;&lt;a href="mailto:benjamindev365@gmail.com"&gt;benjamindev365@gmail.com&lt;/a&gt;&lt;/strong&gt;. (I'm also the developer behind &lt;a href="https://webrium.dev" rel="noopener noreferrer"&gt;webrium.dev&lt;/a&gt;, if you want to see what I build.)&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>startup</category>
      <category>programming</category>
    </item>
    <item>
      <title>xZeroProtect: A Free WordPress Firewall That Also Tells You Who Your Real Visitors Are</title>
      <dc:creator>Benyamin Khalife</dc:creator>
      <pubDate>Mon, 13 Jul 2026 08:07:39 +0000</pubDate>
      <link>https://dev.to/benkhalife/xzeroprotect-a-free-wordpress-firewall-that-also-tells-you-who-your-real-visitors-are-3lp0</link>
      <guid>https://dev.to/benkhalife/xzeroprotect-a-free-wordpress-firewall-that-also-tells-you-who-your-real-visitors-are-3lp0</guid>
      <description>&lt;p&gt;If you run a WordPress site, your access logs are probably full of noise: scanners poking at &lt;code&gt;wp-login.php&lt;/code&gt;, bots requesting &lt;code&gt;.env&lt;/code&gt; files that don't exist, and crawlers with fake user-agents pretending to be Googlebot. Most security plugins block some of this, but very few use that same detection layer to give you &lt;em&gt;cleaner analytics&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That's the idea behind &lt;strong&gt;xZeroProtect&lt;/strong&gt;, a free, open-source WordPress plugin I just released.&lt;/p&gt;

&lt;p&gt;🔗 Plugin: &lt;a href="https://wordpress.org/plugins/xzeroprotect/" rel="noopener noreferrer"&gt;https://wordpress.org/plugins/xzeroprotect/&lt;/a&gt;&lt;br&gt;
🔗 Firewall library: &lt;a href="https://github.com/webrium/xzeroprotect" rel="noopener noreferrer"&gt;https://github.com/webrium/xzeroprotect&lt;/a&gt;&lt;br&gt;
🔗 Plugin source: &lt;a href="https://github.com/webrium/xzeroprotect-wp" rel="noopener noreferrer"&gt;https://github.com/webrium/xzeroprotect-wp&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;I started by building a standalone, dependency-free PHP firewall library (&lt;code&gt;xzeroprotect&lt;/code&gt;) — no Redis, no external services, everything file-based. Once it could reliably tell a real visitor from a bot, it made sense to go one step further: use that same signal to power visitor analytics that are &lt;em&gt;already clean&lt;/em&gt;, instead of bolting a stats plugin on top of unfiltered traffic.&lt;/p&gt;

&lt;p&gt;So the WordPress plugin does two jobs with one pass:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Firewall&lt;/strong&gt; — blocks the request if it looks malicious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics&lt;/strong&gt; — if the request passes every check, it gets recorded as a real visit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nothing that looks like a bot ever makes it into your stats.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually blocks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SQL injection, XSS, path traversal, LFI/RFI, and command injection patterns in GET/POST/cookies&lt;/li&gt;
&lt;li&gt;Known scanner and exploit-tool user-agents (&lt;code&gt;sqlmap&lt;/code&gt;, &lt;code&gt;nikto&lt;/code&gt;, &lt;code&gt;nmap&lt;/code&gt;, &lt;code&gt;masscan&lt;/code&gt;, and more)&lt;/li&gt;
&lt;li&gt;Requests to sensitive paths (&lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.git&lt;/code&gt;, &lt;code&gt;phpmyadmin&lt;/code&gt;, &lt;code&gt;wp-config&lt;/code&gt;, common webshell names, etc.)&lt;/li&gt;
&lt;li&gt;Repeat offenders, via a sliding-window rate limiter with automatic (and escalating) IP bans&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One detail I'm fairly happy with is &lt;strong&gt;crawler verification&lt;/strong&gt;: instead of trusting the &lt;code&gt;User-Agent&lt;/code&gt; header (which anyone can fake), Googlebot/Bingbot/etc. are verified with &lt;strong&gt;double-DNS lookup&lt;/strong&gt; — reverse-resolve the visitor's IP, check the hostname matches the expected suffix (e.g. &lt;code&gt;.googlebot.com&lt;/code&gt;), then forward-resolve that hostname again and confirm it matches the original IP. This is the method Google and Bing themselves recommend, and it means legitimate crawlers are never accidentally rate-limited or banned.&lt;/p&gt;

&lt;p&gt;There's also an optional Apache integration: permanently banned IPs get synced into &lt;code&gt;.htaccess&lt;/code&gt;, so Apache rejects them before PHP even starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dashboard
&lt;/h2&gt;

&lt;p&gt;Once it's running, the plugin gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A traffic chart (visits / unique visitors / blocked requests) over 7, 14, or 30 days&lt;/li&gt;
&lt;li&gt;Top pages by hits and unique visitors&lt;/li&gt;
&lt;li&gt;Device breakdown (desktop / mobile / tablet)&lt;/li&gt;
&lt;li&gt;A breakdown of &lt;em&gt;why&lt;/em&gt; things got blocked&lt;/li&gt;
&lt;li&gt;A real-visitor log with browser, OS, and device detail&lt;/li&gt;
&lt;li&gt;A blocked-request log with attack type and reason&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Visitor uniqueness is tracked with a daily-resetting SHA-256 fingerprint (IP + user-agent + date), so raw IPs aren't stored in the fingerprint itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plugins → Add New → search "xZeroProtect" → Install → Activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default it runs in &lt;strong&gt;Production&lt;/strong&gt; mode (block + log), but there's also a &lt;strong&gt;Learning&lt;/strong&gt; mode that only logs what &lt;em&gt;would&lt;/em&gt; be blocked — useful for a few days of tuning before you flip it live. WordPress core paths (&lt;code&gt;/wp-admin&lt;/code&gt;, &lt;code&gt;/wp-login.php&lt;/code&gt;, &lt;code&gt;/wp-json&lt;/code&gt;) are always whitelisted, so there's no risk of locking yourself out.&lt;/p&gt;

&lt;p&gt;Requirements: WordPress 6.0+, PHP 8.0+, MySQL 5.7+ / MariaDB 10.3+. No Redis, no external API calls, no cloud dependency — everything runs on your own server and database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm sharing it
&lt;/h2&gt;

&lt;p&gt;It's brand new, completely free, and MIT-licensed — both the plugin and the underlying PHP library are open source, so you can read exactly what it's doing or reuse the firewall library in any PHP 8 project, WordPress or not.&lt;/p&gt;

&lt;p&gt;If you try it, I'd genuinely appreciate feedback, bug reports, or feature ideas — either on the &lt;a href="https://wordpress.org/support/plugin/xzeroprotect/" rel="noopener noreferrer"&gt;WordPress.org support forum&lt;/a&gt; or as a GitHub issue.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://github.com/webrium" rel="noopener noreferrer"&gt;Webrium&lt;/a&gt;. MIT licensed.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building Lightweight PHP Microservices with webrium/core — No Framework Bloat Required</title>
      <dc:creator>Benyamin Khalife</dc:creator>
      <pubDate>Mon, 15 Jun 2026 09:56:01 +0000</pubDate>
      <link>https://dev.to/benkhalife/building-lightweight-php-microservices-with-webriumcore-no-framework-bloat-required-3eko</link>
      <guid>https://dev.to/benkhalife/building-lightweight-php-microservices-with-webriumcore-no-framework-bloat-required-3eko</guid>
      <description>&lt;p&gt;Do you really need a full framework to handle a few API endpoints or webhooks?&lt;/p&gt;

&lt;p&gt;Laravel and Symfony are excellent tools — for large applications. But when you're building a focused microservice, a webhook receiver, or a lightweight REST API, bootstrapping a full-stack framework means carrying hundreds of files, a massive autoloader, and a dependency tree you'll never fully use.&lt;/p&gt;

&lt;p&gt;That's the problem &lt;strong&gt;webrium/core&lt;/strong&gt; was built to solve: a minimalist, zero-dependency PHP micro-framework written entirely from scratch, designed to stay out of your way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require webrium/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No configuration files to publish, no service providers to register.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Entry Point
&lt;/h2&gt;

&lt;p&gt;Every webrium application starts with the same three lines:&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;require_once&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/vendor/autoload.php'&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;Webrium\App&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;Webrium\Route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;__DIR__&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ... your routes here&lt;/span&gt;

&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;run&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;App::initialize()&lt;/code&gt; sets the root path and loads the global helper functions. &lt;code&gt;App::run()&lt;/code&gt; initializes error handling and dispatches the current request through the router.&lt;/p&gt;




&lt;h2&gt;
  
  
  Routing
&lt;/h2&gt;

&lt;p&gt;The router supports all standard HTTP methods. Route handlers can be closures, a &lt;code&gt;Controller@method&lt;/code&gt; string, or an &lt;code&gt;[Controller::class, 'method']&lt;/code&gt; array.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic routes:&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="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/status'&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="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'alive'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/items'&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="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'created'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/items/{id}'&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;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'updated'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/items/{id}'&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;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'patched'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/items/{id}'&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;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'deleted'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Route handlers return an array — the framework automatically encodes it as JSON and sends the correct &lt;code&gt;Content-Type&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic parameters:&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="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/users/{id}/posts/{postId}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$postId&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="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="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'post_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$postId&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;&lt;strong&gt;Named routes:&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="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/users/{id}'&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;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$id&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;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users.show'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Generate the URL elsewhere:&lt;/span&gt;
&lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users.show'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// /users/42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Route groups&lt;/strong&gt; let you share a prefix and/or middleware across multiple routes:&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="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'prefix'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'api/v1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'middleware'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'AuthMiddleware'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/profile'&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="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="s1"&gt;'James Carter'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/profile'&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="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'updated'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&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;Custom 404 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="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;setNotFoundHandler&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="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;'Route not found'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'code'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Reading Request Input
&lt;/h2&gt;

&lt;p&gt;The global &lt;code&gt;input()&lt;/code&gt; helper reads from &lt;code&gt;$_GET&lt;/code&gt; on GET requests and from the request body on POST, PUT, PATCH, and DELETE. It handles both &lt;code&gt;application/json&lt;/code&gt; and &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt; automatically.&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="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/search'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$query&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'q'&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="nv"&gt;$limit&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'limit'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&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="s1"&gt;'query'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'limit'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$limit&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;Pass &lt;code&gt;null&lt;/code&gt; (or call &lt;code&gt;input()&lt;/code&gt; with no arguments) to get the full input array:&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="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/echo'&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="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Validation
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Validator&lt;/code&gt; class uses a fluent interface. Pass the input array to its constructor, chain your rules, then call &lt;code&gt;isValid()&lt;/code&gt;, &lt;code&gt;passes()&lt;/code&gt;, or &lt;code&gt;fails()&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Webrium\Validator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/register'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$v&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;Validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Full Name'&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;required&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;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Email'&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;required&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;email&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Password'&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;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'password_confirmation'&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;required&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;confirmed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'password'&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="nv"&gt;$v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fails&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="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'errors'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getErrors&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// proceed with registration...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&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="s1"&gt;'Account created.'&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;Available rules include: &lt;code&gt;required&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;integer&lt;/code&gt;, &lt;code&gt;numeric&lt;/code&gt;, &lt;code&gt;boolean&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;min&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, &lt;code&gt;confirmed&lt;/code&gt;, &lt;code&gt;different&lt;/code&gt;, &lt;code&gt;in&lt;/code&gt;, &lt;code&gt;notIn&lt;/code&gt;, &lt;code&gt;regex&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt;, &lt;code&gt;array&lt;/code&gt;, &lt;code&gt;nullable&lt;/code&gt;, and &lt;code&gt;sometimes&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  JWT Authentication
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Jwt&lt;/code&gt; class handles token generation and verification with HS256, HS384, and HS512. Signature verification uses &lt;code&gt;hash_equals&lt;/code&gt; internally to prevent timing attacks.&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Webrium\Jwt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$jwt&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;Jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'your-secret-key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'HS256'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Generate a token&lt;/span&gt;
&lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$jwt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generateToken&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'sub'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'role'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'exp'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Verify and decode&lt;/span&gt;
&lt;span class="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$jwt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$token&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="nv"&gt;$payload&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;respond&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;'Invalid or expired token.'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;401&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="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="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sub'&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also read the payload without verification (useful for debugging):&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;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Jwt&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Password Hashing
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Hash&lt;/code&gt; class wraps PHP's &lt;code&gt;password_hash&lt;/code&gt; and &lt;code&gt;password_verify&lt;/code&gt; with sensible defaults and supports bcrypt, Argon2i, and Argon2id.&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Webrium\Hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Hash a password (bcrypt by default)&lt;/span&gt;
&lt;span class="nv"&gt;$hashed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Hash&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'secret-password'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Verify&lt;/span&gt;
&lt;span class="nv"&gt;$isValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Hash&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'secret-password'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$hashed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;

&lt;span class="c1"&gt;// Argon2id&lt;/span&gt;
&lt;span class="nv"&gt;$hashed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Hash&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;argon2id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'secret-password'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Generate a secure random token&lt;/span&gt;
&lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Hash&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Generate a UUID v4&lt;/span&gt;
&lt;span class="nv"&gt;$uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Hash&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Middleware
&lt;/h2&gt;

&lt;p&gt;Middleware can be a closure, a global function, a &lt;code&gt;Class@method&lt;/code&gt; string, or a class with a &lt;code&gt;handle()&lt;/code&gt; method that returns a boolean. A falsy return blocks the request with a 403.&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;// Closure middleware&lt;/span&gt;
&lt;span class="nv"&gt;$authCheck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_AUTHORIZATION'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;str_starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Bearer '&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'prefix'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'api/v1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'middleware'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$authCheck&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/me'&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="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="s1"&gt;'James Carter'&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;Class-based middleware:&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;AuthMiddleware&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;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$jwt&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;Webrium\Jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'your-secret-key'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$header&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_AUTHORIZATION'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$token&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Bearer '&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="nv"&gt;$header&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;$jwt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'middleware'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'AuthMiddleware'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/dashboard'&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="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'data'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;h2&gt;
  
  
  CORS
&lt;/h2&gt;

&lt;p&gt;CORS is handled at the application level with a single call before &lt;code&gt;App::run()&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="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;enableCors&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'https://app.example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'https://admin.example.com'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For stricter control:&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="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;configureCors&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'allowed_origins'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'https://app.example.com'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'allowed_methods'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'POST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'PUT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DELETE'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'allow_credentials'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&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;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Here is a minimal but complete authenticated REST API — health check, public registration, and a protected profile endpoint — in a single file:&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;require_once&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/vendor/autoload.php'&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;Webrium\App&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;Webrium\Route&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;Webrium\Validator&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;Webrium\Hash&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;Webrium\Jwt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;__DIR__&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;enableCors&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'https://app.example.com'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nv"&gt;$jwt&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;Jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'super-secret-key'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Health check&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&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="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="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'alive'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Public: register&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/register'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$v&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;Validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&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;required&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;email&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'password'&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;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&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="nv"&gt;$v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fails&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="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'errors'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getErrors&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$hashed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Hash&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// save user to DB...&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;respond&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="s1"&gt;'Account created.'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Public: login&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jwt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$email&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&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="nv"&gt;$password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'password'&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="c1"&gt;// fetch user from DB and verify password...&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'hashed'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Hash&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'demo'&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
    &lt;span class="nv"&gt;$passwordValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Hash&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'hashed'&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="nv"&gt;$passwordValid&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="nf"&gt;respond&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;'Invalid credentials.'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$jwt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generateToken&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'sub'&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="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;'exp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3600&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="s1"&gt;'token'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Protected routes&lt;/span&gt;
&lt;span class="nv"&gt;$authMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jwt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_AUTHORIZATION'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$token&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Bearer '&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="nv"&gt;$header&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;$jwt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;)&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="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'prefix'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'api'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'middleware'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$authMiddleware&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jwt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/profile'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jwt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$token&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Bearer '&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="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_AUTHORIZATION'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$jwt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$token&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="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sub'&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="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A complete, production-ready authenticated API service. No generated boilerplate, no hidden magic — just PHP.&lt;/p&gt;




&lt;p&gt;webrium/core is a good fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're building a &lt;strong&gt;webhook receiver&lt;/strong&gt; or a &lt;strong&gt;focused REST API&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You need &lt;strong&gt;minimal cold-start time&lt;/strong&gt; (serverless, edge environments)&lt;/li&gt;
&lt;li&gt;You want &lt;strong&gt;zero third-party runtime dependencies&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You're prototyping quickly and don't want framework overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; a replacement for a full-stack framework when you need an ORM,&lt;br&gt;
a template engine, queue workers, or a large ecosystem of packages.&lt;/p&gt;

&lt;p&gt;If you need all of that — but still want to stay in the Webrium ecosystem —&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/webrium/webrium" rel="noopener noreferrer"&gt;webrium/webrium&lt;/a&gt;&lt;/strong&gt; is the full-stack&lt;br&gt;
version, built on top of this same core. It ships with&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/webrium/foxdb" rel="noopener noreferrer"&gt;FoxDB&lt;/a&gt;&lt;/strong&gt; as its ORM and includes a&lt;br&gt;
dedicated template engine. Same philosophy, full feature set.&lt;/p&gt;




&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;The project is open source and available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/webrium/core" rel="noopener noreferrer"&gt;github.com/webrium/core&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Contributions, issues, and feedback are welcome.&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>backend</category>
    </item>
    <item>
      <title>How I Stopped Counting Bots as Visitors</title>
      <dc:creator>Benyamin Khalife</dc:creator>
      <pubDate>Mon, 08 Jun 2026 17:22:08 +0000</pubDate>
      <link>https://dev.to/benkhalife/how-i-stopped-counting-bots-as-visitors-26m1</link>
      <guid>https://dev.to/benkhalife/how-i-stopped-counting-bots-as-visitors-26m1</guid>
      <description>&lt;p&gt;A few months ago I was looking at the analytics on one of my projects. The numbers looked decent — hundreds of daily visits, decent traffic from search. But something felt off. The server logs told a completely different story.&lt;/p&gt;

&lt;p&gt;Half of those "visitors" were scanners probing for &lt;code&gt;.env&lt;/code&gt; files. A quarter were bots hammering &lt;code&gt;/wp-login.php&lt;/code&gt;. Maybe ten percent were actual humans.&lt;/p&gt;

&lt;p&gt;Google Analytics had no idea. It was counting everything.&lt;/p&gt;

&lt;p&gt;That's the problem I wanted to fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  The gap nobody talks about
&lt;/h2&gt;

&lt;p&gt;Every analytics tool I know of works the same way: a JavaScript snippet fires when a page loads, and the visit gets counted. The problem is that bots, scrapers, and scanners don't run JavaScript — but they still hit your server, and your server-side analytics still records them.&lt;/p&gt;

&lt;p&gt;Some tools try to filter bot traffic after the fact, using lists of known bot user-agents or behavioral heuristics. But these lists are always behind, always incomplete, and never aware of the specific threats targeting &lt;em&gt;your&lt;/em&gt; application.&lt;/p&gt;

&lt;p&gt;I already had a firewall — &lt;a href="https://github.com/webrium/xzeroprotect" rel="noopener noreferrer"&gt;xZeroProtect&lt;/a&gt; — running on my projects. It was blocking scanners, rate-limiting aggressive IPs, and verifying crawlers via double-DNS. It knew, with high confidence, which requests were real humans.&lt;/p&gt;

&lt;p&gt;The insight was simple: &lt;strong&gt;if the firewall already knows who's a real visitor, why not record that?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;In xZeroProtect, every request passes through a chain of checks before it reaches your application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Incoming request
       │
  Whitelisted? ──────────────────────────► Pass through
  Verified crawler (Googlebot etc.)? ────► Pass through  
  Banned IP? ────────────────────────────► Block
  Rate limit exceeded? ──────────────────► Block
  Suspicious path? ──────────────────────► Block
  Bad User-Agent? ────────────────────────► Block
  Payload attack (SQLi, XSS...)? ─────────► Block
       │
  All checks passed ─────────────────────► Real visit ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any request that reaches the bottom has survived every check. That's the right moment to record a visit — not before, not after.&lt;/p&gt;

&lt;p&gt;The API is intentionally simple. You pass a closure to &lt;code&gt;enableTracking()&lt;/code&gt;, and it fires for every verified real visit:&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Webrium\XZeroProtect\XZeroProtect&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;Webrium\XZeroProtect\VisitInfo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$firewall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;XZeroProtect&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;enableTracking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;VisitInfo&lt;/span&gt; &lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// store however you like — the library doesn't care&lt;/span&gt;
    &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"INSERT INTO visits ..."&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;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The library never touches your database. It hands you a &lt;code&gt;VisitInfo&lt;/code&gt; object and gets out of the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  What VisitInfo gives you
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;$visit&lt;/code&gt; object carries everything you need, parsed and ready:&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;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;              &lt;span class="c1"&gt;// '94.182.11.42'&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;            &lt;span class="c1"&gt;// '/blog/my-post'&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;          &lt;span class="c1"&gt;// 'GET'&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;referer&lt;/span&gt;         &lt;span class="c1"&gt;// 'https://google.com'&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;       &lt;span class="c1"&gt;// 1749388800&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;// '2026-06-08 14:30:00'&lt;/span&gt;

&lt;span class="c1"&gt;// Device info — parsed from User-Agent, no external service&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;browser&lt;/span&gt;         &lt;span class="c1"&gt;// 'Chrome'&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;browserVersion&lt;/span&gt;  &lt;span class="c1"&gt;// '124.0'&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;              &lt;span class="c1"&gt;// 'Windows'&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;osVersion&lt;/span&gt;       &lt;span class="c1"&gt;// '10/11'&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;            &lt;span class="c1"&gt;// 'desktop' | 'mobile' | 'tablet'&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;isMobile&lt;/span&gt;        &lt;span class="c1"&gt;// false&lt;/span&gt;

&lt;span class="c1"&gt;// Unique visitor fingerprint&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fingerprint&lt;/span&gt;     &lt;span class="c1"&gt;// 'a3f8c2...' (64-char SHA-256 hash)&lt;/span&gt;

&lt;span class="c1"&gt;// Flat array — ready for a direct DB insert&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The device detection is built in — no third-party service, no API call, just a User-Agent parser that covers Chrome, Firefox, Safari, Edge, Opera, Samsung Internet, IE, and all major operating systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fingerprint
&lt;/h2&gt;

&lt;p&gt;This is the part I'm most happy with.&lt;/p&gt;

&lt;p&gt;Traditional unique visitor tracking either uses cookies (which require consent banners and get cleared) or stores raw IPs (which is a privacy problem). I wanted something in between.&lt;/p&gt;

&lt;p&gt;The fingerprint is a SHA-256 hash of three things: the visitor's IP address, their User-Agent string, and today's date.&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;$raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;implode&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;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Y-m-d'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// resets daily&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nv"&gt;$fingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same person visiting twice today gets the same fingerprint — you can deduplicate&lt;/li&gt;
&lt;li&gt;Tomorrow their fingerprint is different — no persistent cross-session tracking&lt;/li&gt;
&lt;li&gt;The raw IP is not stored in the fingerprint — it cannot be reversed&lt;/li&gt;
&lt;li&gt;No cookies, no JavaScript, no consent required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not perfect — two people on the same NAT with the same browser will collide — but for the purpose of counting unique daily visitors it's good enough, and it respects privacy by design.&lt;/p&gt;

&lt;p&gt;Counting unique visitors becomes a simple query:&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;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;enableTracking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;VisitInfo&lt;/span&gt; &lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only record the first visit of the day for each fingerprint&lt;/span&gt;
    &lt;span class="nv"&gt;$seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"SELECT 1 FROM visits 
         WHERE fingerprint = ? AND DATE(visited_at) = CURDATE()"&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;execute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fingerprint&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;fetchColumn&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="nv"&gt;$seen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"INSERT INTO visits ..."&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;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&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;h2&gt;
  
  
  Why opt-in, and why a closure?
&lt;/h2&gt;

&lt;p&gt;Two deliberate design decisions worth explaining.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opt-in:&lt;/strong&gt; Tracking is disabled by default. You call &lt;code&gt;enableTracking()&lt;/code&gt; to turn it on. This keeps the library's core purpose — protecting your application — separate from the analytics concern. If you don't need tracking, you pay zero cost for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Closure instead of configuration:&lt;/strong&gt; I could have designed this as a config option with a built-in storage backend. But that would mean the library needs to know about your database, your schema, your connection. Instead, you own the storage completely. Want to write to MySQL? Redis? A log file? A third-party analytics API? The library doesn't care.&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;// Write to database&lt;/span&gt;
&lt;span class="nv"&gt;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;enableTracking&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="kt"&gt;VisitInfo&lt;/span&gt; &lt;span class="nv"&gt;$v&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;$db&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'visits'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

&lt;span class="c1"&gt;// Write to a log file&lt;/span&gt;
&lt;span class="nv"&gt;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;enableTracking&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="kt"&gt;VisitInfo&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="nb"&gt;file_put_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/var/log/visits.log'&lt;/span&gt;&lt;span class="p"&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;$v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;FILE_APPEND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Send to an external service&lt;/span&gt;
&lt;span class="nv"&gt;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;enableTracking&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="kt"&gt;VisitInfo&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://my-analytics.example.com/ingest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&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;Same API, any storage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Errors never reach your visitors
&lt;/h2&gt;

&lt;p&gt;One more thing: the callback runs inside a try/catch.&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;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;recordVisit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&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="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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;trackingEnabled&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;visitorCallback&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="k"&gt;return&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="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;visitorCallback&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;VisitInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&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;\Throwable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Tracking must never crash the application&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;If your database is down, if your callback throws, if anything goes wrong — the visitor still sees your page. Tracking is infrastructure, and infrastructure fails. The firewall's job is to protect your application; it shouldn't become a new point of failure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;After running this for a while, the difference is striking. My "real" visitor count is about 40% of what Google Analytics was reporting. The other 60% was noise — bots, scanners, crawlers, and monitoring tools that JavaScript analytics was happily counting as humans.&lt;/p&gt;

&lt;p&gt;The data is smaller, but it's accurate. And because the firewall is already running, there's no extra overhead — the tracking happens as a side effect of protection that was already in place.&lt;/p&gt;




&lt;p&gt;If you want to try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require webrium/xzeroprotect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full API reference and configuration docs are on &lt;a href="https://github.com/webrium/xzeroprotect" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. There's also a &lt;a href="https://github.com/webrium/xzeroprotect-wp" rel="noopener noreferrer"&gt;WordPress plugin&lt;/a&gt; if you want the dashboard out of the box.&lt;/p&gt;

</description>
      <category>php</category>
      <category>security</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>xZeroProtect 1.1.1 — Smarter Defaults, Cleaner Rules, Real Visitor Tracking</title>
      <dc:creator>Benyamin Khalife</dc:creator>
      <pubDate>Sun, 07 Jun 2026 19:11:25 +0000</pubDate>
      <link>https://dev.to/benkhalife/xzeroprotect-111-smarter-defaults-cleaner-rules-real-visitor-tracking-5db3</link>
      <guid>https://dev.to/benkhalife/xzeroprotect-111-smarter-defaults-cleaner-rules-real-visitor-tracking-5db3</guid>
      <description>&lt;p&gt;After using xZeroProtect across several projects, I kept running into the same friction points — default rules that blocked legitimate API clients, a &lt;code&gt;.php&lt;/code&gt; extension rule that silently broke non-routed apps, and an auto-ban threshold that was a little too eager. Version 1.1.1 fixes all of that, and adds something new.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is xZeroProtect?
&lt;/h2&gt;

&lt;p&gt;A lightweight, file-based PHP 8 firewall library. No database, no Redis, no external service. Drop it into any PHP project, call &lt;code&gt;run()&lt;/code&gt;, and it handles the rest — rate limiting, IP banning, payload scanning, bot detection, and crawler verification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require webrium/xzeroprotect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Webrium\XZeroProtect\XZeroProtect&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$firewall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;XZeroProtect&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole setup for most projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changed in 1.1.1
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Smarter default blocked agents
&lt;/h3&gt;

&lt;p&gt;The previous release blocked &lt;code&gt;curl/&lt;/code&gt;, &lt;code&gt;wget/&lt;/code&gt;, &lt;code&gt;python-requests&lt;/code&gt;, and &lt;code&gt;go-http-client&lt;/code&gt; out of the box. That made sense for public-facing websites, but caused real problems for anyone exposing an API — every legitimate client using those libraries got blocked on install.&lt;/p&gt;

&lt;p&gt;These four are no longer in the default list. If your site doesn't serve an API and you want to block them, it's one line:&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;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'curl/'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wget/'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'python-requests'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'go-http-client'&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;libwww-perl&lt;/code&gt; and &lt;code&gt;lwp-trivial&lt;/code&gt; are still blocked by default — there's no modern legitimate use case for those.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;.php&lt;/code&gt; extension no longer blocked by default
&lt;/h3&gt;

&lt;p&gt;The old default blocked any URL containing &lt;code&gt;.php&lt;/code&gt;. The intention was good — modern routed applications (Laravel, Symfony, Slim) have no public &lt;code&gt;.php&lt;/code&gt; files, so blocking the extension stops a lot of scanner noise.&lt;/p&gt;

&lt;p&gt;The problem: anyone running a traditional PHP app, or a CMS like WordPress, would have all their pages blocked immediately after installation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.php&lt;/code&gt; is now opt-in:&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;// Only add this if your app uses modern routing&lt;/span&gt;
&lt;span class="c1"&gt;// and no public .php files exist&lt;/span&gt;
&lt;span class="nv"&gt;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.php'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Auto-ban threshold raised from 5 to 10
&lt;/h3&gt;

&lt;p&gt;The previous threshold of 5 violations before a ban was aggressive. A real user hitting a few 404s, a misconfigured uptime monitor, or a developer testing locally could end up banned for 24 hours.&lt;/p&gt;

&lt;p&gt;The new default is 10 — still firm enough to catch actual scanners and brute-force attempts, but with enough room to avoid false positives on legitimate traffic.&lt;/p&gt;




&lt;h3&gt;
  
  
  New: &lt;code&gt;VisitInfo&lt;/code&gt; and real visitor tracking
&lt;/h3&gt;

&lt;p&gt;This release introduces opt-in visitor tracking. After all firewall checks pass, you can record the visit — and since the firewall has already filtered out bots and scanners, what gets recorded is real human traffic.&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Webrium\XZeroProtect\VisitInfo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;enableTracking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;VisitInfo&lt;/span&gt; &lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Store however you like — the library doesn't care&lt;/span&gt;
    &lt;span class="nv"&gt;$db&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'visits'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;VisitInfo&lt;/code&gt; object gives you everything you need:&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;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fingerprint&lt;/span&gt;      &lt;span class="c1"&gt;// daily SHA-256 hash — for unique visitor counting&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;browser&lt;/span&gt;  &lt;span class="c1"&gt;// 'Chrome', 'Firefox', 'Safari' ...&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;       &lt;span class="c1"&gt;// 'Windows', 'macOS', 'Android', 'iOS' ...&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;     &lt;span class="c1"&gt;// 'desktop' | 'mobile' | 'tablet'&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;           &lt;span class="c1"&gt;// formatted timestamp&lt;/span&gt;
&lt;span class="nv"&gt;$visit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;        &lt;span class="c1"&gt;// flat array ready for DB insert&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tracking is disabled by default — you enable it explicitly. The library stays zero-dependency; how and where you store data is entirely up to you.&lt;/p&gt;

&lt;p&gt;I'll be writing a dedicated post covering the full tracking API, unique visitor fingerprinting, and some practical analytics patterns. This is just the introduction.&lt;/p&gt;




&lt;h2&gt;
  
  
  What didn't change
&lt;/h2&gt;

&lt;p&gt;The core API is identical. No breaking changes. If you were on 1.0.x, updating is safe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer update webrium/xzeroprotect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All existing configuration, custom rules, IP bans, and rate limit data carry over.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/webrium/xzeroprotect" rel="noopener noreferrer"&gt;github.com/webrium/xzeroprotect&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Packagist:&lt;/strong&gt; &lt;a href="https://packagist.org/packages/webrium/xzeroprotect" rel="noopener noreferrer"&gt;packagist.org/packages/webrium/xzeroprotect&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changelog:&lt;/strong&gt; &lt;a href="https://github.com/webrium/xzeroprotect/releases/tag/1.1.1" rel="noopener noreferrer"&gt;github.com/webrium/xzeroprotect/releases/tag/1.1.1&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback and issues welcome on GitHub.&lt;/p&gt;

</description>
      <category>php</category>
      <category>security</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why You Should Stop Using Laravel for Commercial Projects (The Hidden Risks)</title>
      <dc:creator>Benyamin Khalife</dc:creator>
      <pubDate>Tue, 26 May 2026 13:57:50 +0000</pubDate>
      <link>https://dev.to/benkhalife/why-you-should-stop-using-laravel-for-commercial-projects-the-hidden-risks-3147</link>
      <guid>https://dev.to/benkhalife/why-you-should-stop-using-laravel-for-commercial-projects-the-hidden-risks-3147</guid>
      <description>&lt;p&gt;Every framework has its honeymoon phase. For PHP developers, Laravel has been that reliable, feature-rich partner for years. It’s elegant, it has an amazing community, and it gets things done fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But fast development doesn't always mean a sustainable commercial product.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With the recent wave of supply chain attacks targeting popular Laravel ecosystem dependencies (like &lt;code&gt;localization/lang&lt;/code&gt; packages and ignition RCEs), it’s time to take off the rose-colored glasses. &lt;em&gt;Here is why choosing Laravel&lt;/em&gt; for your next enterprise or high-scale commercial project might be a massive technical debt in disguise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe8uzhw8hp2bqba7zjoa0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe8uzhw8hp2bqba7zjoa0.jpg" alt="The " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The "Bloatware" Dilemma: Heavy, Slow, and Resource-Hungry
&lt;/h2&gt;

&lt;p&gt;Laravel is undeniably bloated. Out of the box, it boots up a massive container, hundreds of classes, and an abstraction layer for almost everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Cost:&lt;/strong&gt; While this makes writing code easy, it consumes significant memory and CPU cycles per request compared to modern micro-frameworks or compiled languages like Go and Rust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commercial Impact:&lt;/strong&gt; In a production environment, higher resource consumption directly translates to bloated cloud infrastructure bills. When scaling to millions of requests, Laravel requires expensive horizontal scaling setups far sooner than it should.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwal2x5b0d4xp3ygivjag.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwal2x5b0d4xp3ygivjag.jpg" alt="The Dependency Hell" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Dependency Hell &amp;amp; Supply Chain Vulnerabilities
&lt;/h2&gt;

&lt;p&gt;Laravel doesn't live in a vacuum; it heavily relies on Symfony components and a massive tree of third-party packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Risk:&lt;/strong&gt; You aren't just trusting Taylor Otwell; you are trusting hundreds of anonymous open-source maintainers. As we recently witnessed, hackers are actively exploiting this by poisoning the ecosystem (e.g., embedding backdoors in language and helper packages).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Control Problem:&lt;/strong&gt; Managing, auditing, and securing a giant vendor folder in Laravel is a logistical nightmare. One compromised sub-dependency can instantly introduce a Remote Code Execution (RCE) vulnerability into your commercial app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnn7ymolu37phiy2k4nuy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnn7ymolu37phiy2k4nuy.webp" alt="The " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The "Mass Popularity" Target
&lt;/h2&gt;

&lt;p&gt;Being the king of PHP frameworks comes with a massive bullseye on your back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Target:&lt;/strong&gt; Hackers love popular frameworks. Why waste time finding a zero-day vulnerability for a custom or niche framework when finding one exploit in Laravel or its core dependencies grants you access to hundreds of thousands of live websites?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automated Exploitation:&lt;/strong&gt; Shodan and automated botnets constantly scan the web for Laravel specific patterns (like exposed .env files or older Ignition pages). The moment an exploit is published, your site is targeted within minutes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkv975onf8wib859vu7f5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkv975onf8wib859vu7f5.jpg" alt="Architectural Magic" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Architectural Magic That Breaks at Scale
&lt;/h2&gt;

&lt;p&gt;Laravel relies heavily on "magic" methods, Facades, and Eloquent ORM. While great for rapid prototyping, this magic obfuscates what is actually happening under the hood.&lt;/p&gt;

&lt;p&gt;As your data grows, Eloquent can easily introduce N+1 query problems and hidden memory leaks if not meticulously optimized.&lt;/p&gt;

&lt;p&gt;Deeply nesting your business logic inside Laravel's heavily opinionated structure makes migrating or rewriting microservices incredibly difficult later on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion: Is Laravel Dead?
&lt;/h3&gt;

&lt;p&gt;No. It’s still fantastic for MVPs, SaaS internal tools, or small-to-medium business apps.&lt;/p&gt;

&lt;p&gt;However, if you are building an enterprise-grade, high-performance, and security-critical commercial system, the heavy architectural footprint and the massive attack surface of its ecosystem should make you think twice. Sometimes, going lighter, stricter, and more compiled is the only way to scale safely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What’s your take?&lt;/strong&gt; Have you migrated away from Laravel for performance or security reasons? Let's discuss below!&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>cybersecurity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop Scanners from Hammering Your PHP App — Without a Database or External Services</title>
      <dc:creator>Benyamin Khalife</dc:creator>
      <pubDate>Sat, 16 May 2026 12:46:35 +0000</pubDate>
      <link>https://dev.to/benkhalife/stop-scanners-from-hammering-your-php-app-without-a-database-or-external-services-39og</link>
      <guid>https://dev.to/benkhalife/stop-scanners-from-hammering-your-php-app-without-a-database-or-external-services-39og</guid>
      <description>&lt;p&gt;Every day, automated bots are scanning your website. Not just yours — everyone's. They probe for exposed &lt;code&gt;.env&lt;/code&gt; files, old WordPress admin panels, SQL injection points, and known CVEs. Some of them send thousands of requests per minute, not because they're targeting &lt;em&gt;you&lt;/em&gt; specifically, but because scanning the entire internet is cheap and easy.&lt;/p&gt;

&lt;p&gt;This article is about what's actually happening out there, why it matters even for small projects, and how a lightweight PHP library can help you deal with it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Are These Scanners, Exactly?
&lt;/h2&gt;

&lt;p&gt;Web scanners are automated programs that crawl the internet looking for security vulnerabilities. Some are legitimate security tools — like Shodan or Censys — that map the internet for research purposes. But many others are operated by attackers who are looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exposed configuration files&lt;/strong&gt; — &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;web.config&lt;/code&gt;, &lt;code&gt;.git/config&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unprotected admin panels&lt;/strong&gt; — &lt;code&gt;/wp-admin&lt;/code&gt;, &lt;code&gt;/phpmyadmin&lt;/code&gt;, &lt;code&gt;/adminer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Known vulnerable endpoints&lt;/strong&gt; — like &lt;code&gt;xmlrpc.php&lt;/code&gt; in WordPress or &lt;code&gt;setup.php&lt;/code&gt; left behind after an install&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL injection and XSS vectors&lt;/strong&gt; — via query strings, POST bodies, and cookies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path traversal attacks&lt;/strong&gt; — trying &lt;code&gt;../../etc/passwd&lt;/code&gt; variations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tools they use are well-known: &lt;code&gt;sqlmap&lt;/code&gt;, &lt;code&gt;nikto&lt;/code&gt;, &lt;code&gt;nmap&lt;/code&gt;, &lt;code&gt;dirbuster&lt;/code&gt;, &lt;code&gt;gobuster&lt;/code&gt;, &lt;code&gt;masscan&lt;/code&gt;, and many more. These aren't obscure hacking tools — they're freely available, widely documented, and actively maintained.&lt;/p&gt;

&lt;p&gt;Even if your app has no actual vulnerabilities, these scanners generate noise, consume server resources, pollute your logs, and can slow down real users during a burst of requests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Server-Level Solutions Aren't Always Enough
&lt;/h2&gt;

&lt;p&gt;You might wonder: "Can't Nginx or Apache handle this?"&lt;/p&gt;

&lt;p&gt;Sort of. You can write firewall rules, block known bad IPs, or configure rate limiting in your server config. But these approaches have limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They require system-level access (not always available on shared hosting)&lt;/li&gt;
&lt;li&gt;They need manual maintenance as attack patterns evolve&lt;/li&gt;
&lt;li&gt;They can't easily inspect request payloads for things like SQL injection signatures&lt;/li&gt;
&lt;li&gt;They don't integrate naturally with your application logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A PHP-level firewall runs inside your application, which means it can inspect everything: headers, query strings, POST bodies, cookies, request paths, and user agents — and react accordingly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing xZeroProtect
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/webrium/xzeroprotect" rel="noopener noreferrer"&gt;xZeroProtect&lt;/a&gt; is a lightweight, file-based PHP 8 firewall with zero external dependencies. No Redis, no MySQL, no external services — everything is stored on disk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxl1kmwc7d02htmfxa47.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxl1kmwc7d02htmfxa47.webp" alt="Introducing xZeroProtect layouts" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install it via Composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require webrium/xzeroprotect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The simplest possible integration — two lines at the top of your &lt;code&gt;index.php&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="k"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'vendor/autoload.php'&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;Webrium\XZeroProtect\XZeroProtect&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;XZeroProtect&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&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;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Default rules activate immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Actually Does
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Blocks Known Scanner User Agents
&lt;/h3&gt;

&lt;p&gt;The library maintains a list of User-Agent signatures associated with scanning tools and attack frameworks: &lt;code&gt;sqlmap&lt;/code&gt;, &lt;code&gt;nikto&lt;/code&gt;, &lt;code&gt;nessus&lt;/code&gt;, &lt;code&gt;acunetix&lt;/code&gt;, &lt;code&gt;masscan&lt;/code&gt;, &lt;code&gt;dirbuster&lt;/code&gt;, &lt;code&gt;gobuster&lt;/code&gt;, &lt;code&gt;feroxbuster&lt;/code&gt;, &lt;code&gt;wfuzz&lt;/code&gt;, &lt;code&gt;ffuf&lt;/code&gt;, &lt;code&gt;hydra&lt;/code&gt;, &lt;code&gt;metasploit&lt;/code&gt;, and many others. Requests from these tools are blocked before they get anywhere near your application logic.&lt;/p&gt;

&lt;p&gt;You can extend or trim this list:&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;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'custom-bad-bot'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;removeAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'curl'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// if your API clients use curl legitimately&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Blocks Sensitive Path Probes
&lt;/h3&gt;

&lt;p&gt;Scanners routinely probe paths that don't exist in modern PHP apps — and shouldn't. xZeroProtect blocks requests targeting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CMS admin panels: &lt;code&gt;/wp-admin&lt;/code&gt;, &lt;code&gt;/wp-login&lt;/code&gt;, &lt;code&gt;/administrator&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Config files: &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.git&lt;/code&gt;, &lt;code&gt;.htpasswd&lt;/code&gt;, &lt;code&gt;web.config&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Database tools: &lt;code&gt;/phpmyadmin&lt;/code&gt;, &lt;code&gt;/adminer&lt;/code&gt;, &lt;code&gt;/dbadmin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Dangerous files: &lt;code&gt;.sql&lt;/code&gt;, &lt;code&gt;.bak&lt;/code&gt;, &lt;code&gt;.backup&lt;/code&gt;, &lt;code&gt;dump.sql&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Web shells: &lt;code&gt;shell.php&lt;/code&gt;, &lt;code&gt;c99.php&lt;/code&gt;, &lt;code&gt;r57.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Path traversal: &lt;code&gt;../&lt;/code&gt;, &lt;code&gt;..%2f&lt;/code&gt;, &lt;code&gt;%2e%2e&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're running a fully-routed PHP app with no &lt;code&gt;.php&lt;/code&gt; files in URLs, you can add that as a rule too:&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;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.php'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Scans Payloads for Attack Signatures
&lt;/h3&gt;

&lt;p&gt;The payload scanner inspects GET parameters, POST body, raw input, and cookies using compiled regular expressions. It detects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL injection variants: &lt;code&gt;UNION SELECT&lt;/code&gt;, &lt;code&gt;DROP TABLE&lt;/code&gt;, &lt;code&gt;SLEEP()&lt;/code&gt;, &lt;code&gt;BENCHMARK()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;XSS: &lt;code&gt;&amp;lt;script&lt;/code&gt; tags, inline event handlers, &lt;code&gt;javascript:&lt;/code&gt; protocol&lt;/li&gt;
&lt;li&gt;Path traversal: &lt;code&gt;../&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;PHP code injection: &lt;code&gt;system()&lt;/code&gt;, &lt;code&gt;exec()&lt;/code&gt;, &lt;code&gt;eval(base64_decode(...))&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;LFI: &lt;code&gt;/etc/passwd&lt;/code&gt;, &lt;code&gt;/etc/shadow&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Remote file inclusion&lt;/li&gt;
&lt;li&gt;Command injection via shell metacharacters&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Rate Limiting Without Redis
&lt;/h3&gt;

&lt;p&gt;The rate limiter uses a sliding-window counter stored per-IP on disk. No Redis or Memcached required.&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;$firewall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;XZeroProtect&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'rate_limit'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'max_requests'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'per_seconds'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&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;When a client exceeds the limit, they accumulate violations. After enough violations, they get temporarily banned. After enough bans, permanently.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Doesn't Block Legitimate Crawlers
&lt;/h3&gt;

&lt;p&gt;This is important: Google, Bing, and other legitimate search engines should not get caught in your firewall. xZeroProtect handles this with &lt;strong&gt;double-DNS verification&lt;/strong&gt; for major crawlers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Resolve the visitor's IP to a hostname via reverse DNS&lt;/li&gt;
&lt;li&gt;Confirm the hostname ends with the expected suffix (e.g. &lt;code&gt;.googlebot.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Re-resolve that hostname back to an IP&lt;/li&gt;
&lt;li&gt;Confirm the re-resolved IP matches the original&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Anyone can fake a User-Agent string. DNS cannot be faked. This is the same verification method recommended by Google and Bing in their official documentation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Learning Mode: Test Before You Block
&lt;/h2&gt;

&lt;p&gt;Before switching to production, you can run in learning mode. All threats are detected and logged, but nothing is blocked. This lets you review what would have been caught and tune your configuration accordingly.&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;// During tuning&lt;/span&gt;
&lt;span class="nc"&gt;XZeroProtect&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'mode'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'learning'&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;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Once satisfied&lt;/span&gt;
&lt;span class="nc"&gt;XZeroProtect&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'mode'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'production'&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;run&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 genuinely useful. Running learning mode for a few days on a real server will show you the volume and variety of automated probes hitting your application — and help you avoid accidentally blocking legitimate traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Apache Integration for Zero-PHP-Overhead Blocking
&lt;/h2&gt;

&lt;p&gt;For permanently banned IPs, you can optionally write them into &lt;code&gt;.htaccess&lt;/code&gt; so Apache rejects the connection before PHP even starts:&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;$firewall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;XZeroProtect&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'apache_blocking'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'htaccess_path'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/.htaccess'&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 particularly useful for confirmed attackers who you never want to reach your application layer again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Custom Rules
&lt;/h2&gt;

&lt;p&gt;If the built-in modules don't cover a specific case, you can register your own:&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Webrium\XZeroProtect\RuleResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$firewall&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'no-php-extension'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&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="nf"&gt;str_ends_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'.php'&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="nc"&gt;RuleResult&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PHP extension not valid on this server'&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="nc"&gt;RuleResult&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;pass&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;Rules can block, log without blocking, or pass. They run in priority order and can be enabled, disabled, or removed at runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  What xZeroProtect Is Not
&lt;/h2&gt;

&lt;p&gt;To be clear about expectations: this is not a commercial WAF. It won't stop a sophisticated, targeted attack from a determined adversary. It doesn't do deep packet inspection, it doesn't have a threat intelligence feed, and it doesn't update its rule set automatically.&lt;/p&gt;

&lt;p&gt;What it does is significantly reduce the automated noise that hits most PHP applications — the opportunistic scanners, the script-kiddie tools, the mass-probing bots — with a setup that takes minutes and has no infrastructure dependencies.&lt;/p&gt;

&lt;p&gt;For many projects, especially those on shared hosting or small VPS instances where you don't have the option to configure Nginx rate limiting or cloud WAF rules, this is a practical and useful layer of defense.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require webrium/xzeroprotect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full documentation and source: &lt;a href="https://github.com/webrium/xzeroprotect" rel="noopener noreferrer"&gt;github.com/webrium/xzeroprotect&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start in learning mode, review your logs, tune your configuration, then flip to production.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have feedback or found an edge case? Issues and PRs are welcome on GitHub.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>php</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building a Modular Ecosystem: How I’m Rethinking the CLI for My Personal</title>
      <dc:creator>Benyamin Khalife</dc:creator>
      <pubDate>Fri, 15 May 2026 18:01:04 +0000</pubDate>
      <link>https://dev.to/benkhalife/building-a-modular-ecosystem-how-im-rethinking-the-cli-for-my-personal-4p95</link>
      <guid>https://dev.to/benkhalife/building-a-modular-ecosystem-how-im-rethinking-the-cli-for-my-personal-4p95</guid>
      <description>&lt;p&gt;Every developer has that one project they pour their soul into. For me, it’s &lt;strong&gt;Webrium&lt;/strong&gt; — my personal PHP framework. Recently, I’ve been focusing on its "brain" for command-line operations: the &lt;strong&gt;Webrium Console&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can check out the progress here: &lt;a href="https://github.com/webrium/console" rel="noopener noreferrer"&gt;https://github.com/webrium/console&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Vision: Beyond Simple Commands
&lt;/h2&gt;

&lt;p&gt;While I’ve always admired how Laravel’s Artisan simplifies development, I wanted to build something that fits a specific architectural need I had: &lt;strong&gt;True Modularity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The goal for the Webrium Console wasn't just to run migrations or seeders. I wanted a system where you can build a website normally, and then "export" or "modularize" specific parts of it as independent plugins or components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning Features into Components
&lt;/h2&gt;

&lt;p&gt;The most exciting part of this console library is how it handles site sections. Imagine you’ve built a robust &lt;strong&gt;Admin Panel&lt;/strong&gt; or a &lt;strong&gt;Blog system&lt;/strong&gt;. Instead of having them buried deep within your project’s monolith, Webrium Console allows you to treat them as portable components.&lt;br&gt;
For example:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Blog Component: It’s not just a set of files. It includes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;everything—the management interface for the admin and the public-facing views for the client.&lt;/li&gt;
&lt;li&gt;The Admin Portal: You can build it once and potentially plug it into different projects managed by the framework.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This "extract and reuse" approach is what I’m currently refining. It’s about being able to say: "I need a blog on this new site," and simply plugging in your pre-built, console-managed component.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I’d love to hear your thoughts on this:&lt;/strong&gt;&lt;br&gt;
Laravel is the industry standard for a reason, but sometimes we seek something different. What is the one thing that would make you look beyond established frameworks like Laravel and try something newer or custom-built for your projects?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do you need a custom website or a specialized web solution?&lt;br&gt;
Feel free to reach out! I’m available for new projects, or even just a free consultation to help you figure out the best tech stack for your needs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let’s connect:&lt;br&gt;
Telegram: @benyaminir&lt;br&gt;
Email: &lt;a href="mailto:benkhalifedev@gmail.com"&gt;benkhalifedev@gmail.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>framework</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Generating Sitemaps is Easy</title>
      <dc:creator>Benyamin Khalife</dc:creator>
      <pubDate>Thu, 14 May 2026 06:02:44 +0000</pubDate>
      <link>https://dev.to/benkhalife/generating-sitemaps-is-easy-pa0</link>
      <guid>https://dev.to/benkhalife/generating-sitemaps-is-easy-pa0</guid>
      <description>&lt;p&gt;I've been building PHP projects for a while, and one of those annoying little tasks that keeps coming up is generating sitemaps. Not because it's technically hard — it's just tedious. You either roll your own XML string builder (and inevitably forget an edge case), or you pull in a heavy package that does ten things you don't need.&lt;br&gt;
So I wrote a small library called webrium/sitemap to handle it cleanly. No bloat, just a focused tool that covers the real-world cases: images, videos, hreflang for multilingual sites, gzip, and automatic splitting for large sites.&lt;/p&gt;


&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require webrium/sitemap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's it. PHP 8.1+ and the xmlwriter extension (which is almost always enabled by default).&lt;/p&gt;


&lt;h2&gt;
  
  
  The Basics
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Webrium\Sitemap\Sitemap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$sitemap&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;Sitemap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://example.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$sitemap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addUrl&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="n"&gt;changefreq&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Sitemap&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FREQ_DAILY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// → https://example.com&lt;/span&gt;
&lt;span class="nv"&gt;$sitemap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/about'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;changefreq&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Sitemap&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FREQ_MONTHLY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$sitemap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/blog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;changefreq&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Sitemap&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FREQ_DAILY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$sitemap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One thing worth noting: passing &lt;code&gt;'/'&lt;/code&gt; for the homepage correctly produces &lt;code&gt;https://example.com&lt;/code&gt; — no trailing slash appended. Clean URLs matter, especially when Google is reading them.&lt;/p&gt;


&lt;h2&gt;
  
  
  Full URL Options
&lt;/h2&gt;

&lt;p&gt;Each URL can carry a lastmod, changefreq, and priority. Named arguments make this readable even when you're using all of them:&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$sitemap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="s1"&gt;'/blog/my-first-post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;lastmod&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="s1"&gt;'2025-04-15'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;changefreq&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Sitemap&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FREQ_WEEKLY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="mf"&gt;0.7&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;changefreq tells crawlers how often the content changes. Available values:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;changefreq&lt;/strong&gt; tells crawlers how often the content changes. Available values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constant&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sitemap::FREQ_ALWAYS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sitemap::FREQ_HOURLY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hourly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sitemap::FREQ_DAILY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sitemap::FREQ_WEEKLY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;weekly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sitemap::FREQ_MONTHLY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;monthly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sitemap::FREQ_YEARLY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yearly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sitemap::FREQ_NEVER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;never&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;priority&lt;/strong&gt; is a float between &lt;code&gt;0.0&lt;/code&gt; and &lt;code&gt;1.0&lt;/code&gt;. Your homepage is typically &lt;code&gt;1.0&lt;/code&gt;, blog posts somewhere around &lt;code&gt;0.6&lt;/code&gt;–&lt;code&gt;0.8&lt;/code&gt;. Search engines don't treat this as a hard rule, but it's still worth setting thoughtfully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;lastmod&lt;/strong&gt; accepts any &lt;code&gt;DateTimeInterface&lt;/code&gt;, so you can pass a &lt;code&gt;DateTime&lt;/code&gt;, &lt;code&gt;DateTimeImmutable&lt;/code&gt;, or Carbon instance from your models directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adding URLs in Bulk
&lt;/h2&gt;

&lt;p&gt;If you're pulling pages from a database, &lt;code&gt;addUrls()&lt;/code&gt; is cleaner than looping manually:&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;$sitemap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addUrls&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/services'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'priority'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/portfolio'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'changefreq'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Sitemap&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FREQ_MONTHLY&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'lastmod'&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;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-01-10'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'priority'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&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;Duplicate URLs are silently ignored — you don't need to deduplicate before passing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Images
&lt;/h2&gt;

&lt;p&gt;Google's image sitemap extension lets you associate images with pages, which can improve image search visibility:&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;$sitemap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="s1"&gt;'/portfolio/project-alpha'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'loc'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com/images/project-alpha-hero.jpg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'title'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Project Alpha — hero shot'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'caption'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'The main dashboard view of Project Alpha.'&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="s1"&gt;'loc'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com/images/project-alpha-mobile.jpg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Project Alpha — mobile view'&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;loc&lt;/code&gt; is required. &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;caption&lt;/code&gt; are optional but recommended.&lt;/p&gt;




&lt;h2&gt;
  
  
  Videos
&lt;/h2&gt;

&lt;p&gt;Same idea for videos, with a few more required fields:&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;$sitemap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="s1"&gt;'/tutorials/getting-started'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;videos&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'thumbnail_loc'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com/thumbs/getting-started.jpg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'title'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Getting Started in 5 Minutes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'A quick walkthrough of the core features.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'duration'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// seconds&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;&lt;code&gt;thumbnail_loc&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, and &lt;code&gt;description&lt;/code&gt; are required. &lt;code&gt;duration&lt;/code&gt; is optional.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multilingual Sites (Hreflang)
&lt;/h2&gt;

&lt;p&gt;If your site serves content in multiple languages, you can attach hreflang alternates to each URL:&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;$sitemap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="s1"&gt;'/about'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hreflangs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'lang'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'en'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com/en/about'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'lang'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'fa'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com/fa/about'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'lang'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'x-default'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com/about'&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;The &lt;code&gt;x-default&lt;/code&gt; entry tells Google which version to show when no language preference matches.&lt;/p&gt;




&lt;h2&gt;
  
  
  Saving to File
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Plain XML&lt;/span&gt;
&lt;span class="nv"&gt;$sitemap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;saveToFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/var/www/public/sitemap.xml'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Gzip compressed — just add .gz to the filename&lt;/span&gt;
&lt;span class="nv"&gt;$sitemap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;saveToFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/var/www/public/sitemap.xml.gz'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gzip is worth enabling in production. The file ends up 5–10x smaller, and most crawlers handle it fine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Large Sites — Auto Splitting
&lt;/h2&gt;

&lt;p&gt;The sitemap protocol caps files at 50,000 URLs and 50 MB. For bigger sites, &lt;code&gt;splitAndSave()&lt;/code&gt; handles the splitting automatically and generates a sitemap index:&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;$sitemap&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;Sitemap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://example.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$allPages&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$sitemap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$indexXml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sitemap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;splitAndSave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="s1"&gt;'/var/www/public/sitemaps'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;baseFileUrl&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com/sitemaps'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="s1"&gt;'sitemap'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;file_put_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/var/www/public/sitemap-index.xml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$indexXml&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces &lt;code&gt;sitemap-1.xml.gz&lt;/code&gt;, &lt;code&gt;sitemap-2.xml.gz&lt;/code&gt;, and so on — plus a sitemap index file that points to all of them. Submit the index URL to Google Search Console and you're done.&lt;/p&gt;




&lt;h2&gt;
  
  
  Namespace Cleanliness
&lt;/h2&gt;

&lt;p&gt;One small detail I care about: the library only adds XML namespaces that are actually used. If your sitemap has no images or videos, the output won't carry &lt;code&gt;xmlns:image&lt;/code&gt; or &lt;code&gt;xmlns:video&lt;/code&gt;. It's a minor thing, but it keeps the XML clean and honest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Nothing revolutionary here — it's a sitemap generator. But it handles the real cases correctly and gets out of your way. If you've been copy-pasting sitemap XML by hand or using a bloated package, give it a try.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/webrium/sitemap" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; / Packagist:&lt;/strong&gt; &lt;code&gt;composer require webrium/sitemap&lt;/code&gt;&lt;/p&gt;




</description>
      <category>php</category>
      <category>seo</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
