<?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: Taras Hanych</title>
    <description>The latest articles on DEV Community by Taras Hanych (@syntaxwanderer_26).</description>
    <link>https://dev.to/syntaxwanderer_26</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%2F4131118%2F2fbc9862-36ec-418d-a2ca-17a04effea83.jpg</url>
      <title>DEV Community: Taras Hanych</title>
      <link>https://dev.to/syntaxwanderer_26</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/syntaxwanderer_26"/>
    <language>en</language>
    <item>
      <title>Claude Code Hooks for Laravel: Don't Let the Agent Say "Done" Until the Checks Pass</title>
      <dc:creator>Taras Hanych</dc:creator>
      <pubDate>Wed, 04 Nov 2026 15:30:00 +0000</pubDate>
      <link>https://dev.to/syntaxwanderer_26/claude-code-hooks-for-laravel-dont-let-the-agent-say-done-until-the-checks-pass-3j7c</link>
      <guid>https://dev.to/syntaxwanderer_26/claude-code-hooks-for-laravel-dont-let-the-agent-say-done-until-the-checks-pass-3j7c</guid>
      <description>&lt;p&gt;A rule in &lt;code&gt;CLAUDE.md&lt;/code&gt; is a request. A hook is a gate.&lt;/p&gt;

&lt;p&gt;Here is a reproduction from my Laravel test app. One route gets a new name, the way an agent renames things while "cleaning up":&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;'/contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;ContactController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'create'&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;'contact.create'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// was contact.form&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller still redirects to &lt;code&gt;contact.form&lt;/code&gt;, and four lines in a feature test still request it. &lt;code&gt;php -l&lt;/code&gt; is happy, the route file looks fine, and nothing complains until someone submits the contact form or runs that one feature test. If the agent doesn't, it reports the task as done.&lt;/p&gt;

&lt;p&gt;That is how architecture drifts in a codebase with an AI agent in it: not in one big rewrite, but one renamed route, one skipped check and one "done" at a time. You can write "always run the tests" into &lt;code&gt;CLAUDE.md&lt;/code&gt;. Most of the time the agent will. The times it doesn't are the ones that reach production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Stop hook is
&lt;/h2&gt;

&lt;p&gt;Claude Code runs hooks at fixed points of a session, configured in &lt;code&gt;.claude/settings.json&lt;/code&gt;. The &lt;code&gt;Stop&lt;/code&gt; hook runs when the agent has finished responding and is about to hand the turn back to you. Three details from the docs matter for a gate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The hook blocks only with &lt;strong&gt;exit code 2&lt;/strong&gt; (stderr goes back to Claude) or with JSON on stdout: &lt;code&gt;{"decision": "block", "reason": "..."}&lt;/code&gt;. Claude then keeps working with your reason as its next instruction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any other non-zero exit code is a non-blocking error.&lt;/strong&gt; Claude stops anyway.&lt;/li&gt;
&lt;li&gt;The input has &lt;code&gt;stop_hook_active: true&lt;/code&gt; when Claude is already continuing because of a stop hook, and Claude Code ends the turn itself after eight consecutive continuations. That is how you avoid an endless loop on a check the agent cannot fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one-liner that doesn't block
&lt;/h2&gt;

&lt;p&gt;The first hook most people write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"php artisan test"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a test fails, &lt;code&gt;php artisan test&lt;/code&gt; exits with 1. By the rules above, that is a non-blocking error, and the agent finishes anyway. The gate is open.&lt;/p&gt;

&lt;p&gt;Make it exit 2 and it blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Tests fail. Fix them before finishing."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It still has four problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It runs on &lt;strong&gt;every&lt;/strong&gt; stop, including when the agent just answered a question.&lt;/li&gt;
&lt;li&gt;It runs the &lt;strong&gt;whole suite&lt;/strong&gt;, so it gets slow, and slow gates get removed.&lt;/li&gt;
&lt;li&gt;It sees &lt;strong&gt;only tests.&lt;/strong&gt; Larastan errors, code style, a config file that no longer caches, a provider that no longer boots and a renamed route all pass, as long as no test happens to touch them.&lt;/li&gt;
&lt;li&gt;It can be satisfied by &lt;strong&gt;deleting the failing test.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A hook that checks what the change needs
&lt;/h2&gt;

&lt;p&gt;This is what I built into &lt;a href="https://github.com/semitexa/laravel-ai-verify" rel="noopener noreferrer"&gt;laravel-ai-verify&lt;/a&gt;, the Laravel port of the agent tooling from Semitexa, the PHP framework I'm working on. One command installs 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 &lt;span class="nt"&gt;--dev&lt;/span&gt; semitexa/laravel-ai-verify
php artisan ai:verify:install &lt;span class="nt"&gt;--hook&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It merges a single entry into &lt;code&gt;.claude/settings.json&lt;/code&gt;, next to whatever hooks you already have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"php &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;${CLAUDE_PROJECT_DIR}/artisan&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ai:verify:hook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Claude is about to finish, the hook runs &lt;code&gt;php artisan ai:verify&lt;/code&gt; on the uncommitted work. That command looks at which files changed and plans only the checks they need: the tests that reach the changed code, Larastan on the changed lines, your project's own style tool (Pint, PHP-CS-Fixer, PHP_CodeSniffer or ECS, whichever has a config), a fresh &lt;code&gt;route:list&lt;/code&gt;, &lt;code&gt;config:cache&lt;/code&gt; or &lt;code&gt;event:list&lt;/code&gt; for the kind of file you touched, and migrations in pretend mode. A changed test file must keep its tests and assertions, so deleting the failing test fails too.&lt;/p&gt;

&lt;p&gt;For the renamed route, three checks ran in under a second, and the hook answered with a block. This is the reason Claude received, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✗ artisan:routes: 5 reference(s) to undefined route names
  › app/Http/Controllers/ContactController.php:27 Route [contact.form] is not defined
    (The route was renamed or removed. Update this reference, or restore the name with -&amp;gt;name('contact.form').)
  › tests/Feature/ContactFormTest.php:14 Route [contact.form] is not defined
  › tests/Feature/ContactFormTest.php:30 Route [contact.form] is not defined
  ...
Re-run php artisan ai:verify after fixing. Do not delete or weaken tests to make it pass.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;File, line and the fix, which is what an agent needs to act on it without asking you.&lt;/p&gt;

&lt;p&gt;After a block it checks again every time the agent changes something, so a half-done fix (the controller updated, the tests still calling the old name) is sent back too. Two things keep it from being annoying. It skips a working tree it has already judged, so answering a question in a dirty repo, or stopping again without changing anything, does not re-run the checks. And it gives up after five rounds in one continuation, below Claude Code's own cap of eight.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;It is a Claude Code hook. Cursor, Codex and Copilot get the same instructions through &lt;code&gt;AGENTS.md&lt;/code&gt;, a git hook stamps commits that passed, and CI can run the same checks with the package's GitHub Action, but only Claude Code has a Stop event that can hold the turn.&lt;/li&gt;
&lt;li&gt;It verifies checks, not intent. A commit can pass every check and still change the wrong line; the package flags some of those from the commit message, as a warning.&lt;/li&gt;
&lt;li&gt;A gate is only as good as the checks behind it. If no check could read the changed files (a part of the app with no tests and no static analysis), the verdict is &lt;code&gt;incomplete&lt;/code&gt;, not &lt;code&gt;pass&lt;/code&gt;. The hook blocks only on &lt;code&gt;fail&lt;/code&gt;, so an incomplete run does not hold the agent back; &lt;code&gt;php artisan ai:verify&lt;/code&gt; shows it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which checks do you make your agent pass before it may say "done"?&lt;/p&gt;

&lt;p&gt;Source and docs: &lt;a href="https://github.com/semitexa/laravel-ai-verify" rel="noopener noreferrer"&gt;https://github.com/semitexa/laravel-ai-verify&lt;/a&gt;. The ideas come from Semitexa: &lt;a href="https://semitexa.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=claude-code-hooks-laravel" rel="noopener noreferrer"&gt;https://semitexa.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=claude-code-hooks-laravel&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>ai</category>
      <category>php</category>
      <category>claude</category>
    </item>
    <item>
      <title>The commit passed every check. It still changed the wrong line.</title>
      <dc:creator>Taras Hanych</dc:creator>
      <pubDate>Thu, 22 Oct 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/syntaxwanderer_26/the-commit-passed-every-check-it-still-changed-the-wrong-line-2lbo</link>
      <guid>https://dev.to/syntaxwanderer_26/the-commit-passed-every-check-it-still-changed-the-wrong-line-2lbo</guid>
      <description>&lt;p&gt;Earlier this month Claude Code made a commit in our test app with the message "Limit contact name to 250 chars". The tests passed. Our verification hook ran, passed, and stamped the commit. CI was green.&lt;/p&gt;

&lt;p&gt;The name limit did not change. The email limit did.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;We were testing a GitHub Action for &lt;a href="https://github.com/semitexa/laravel-ai-verify" rel="noopener noreferrer"&gt;laravel-ai-verify&lt;/a&gt;, a package I maintain that makes coding agents run the checks a change needs before they say "done". The test app is a plain Laravel contact form. The form request had two rules next to each other:&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="s1"&gt;'name'&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;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max:200'&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="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&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;'max:255'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intent was to limit the name to 250 characters. The agent did what agents often do with a one-line change: a find-and-replace of &lt;code&gt;max:255&lt;/code&gt; with &lt;code&gt;max:250&lt;/code&gt;. There was no &lt;code&gt;max:255&lt;/code&gt; on the name line. There was one on the email line.&lt;/p&gt;

&lt;p&gt;Every check we had said yes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP syntax: fine.&lt;/li&gt;
&lt;li&gt;The tests for that form request: green. None of them pinned the email limit.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ai:verify&lt;/code&gt; wrote a receipt, and the git hook added the trailer: &lt;code&gt;AI-Verify: pass ... checks=4&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The receipt was honest. The checks it lists really ran, on exactly the tree that was committed. What it could not tell anyone is that the commit did something other than what its message said. CodeRabbit caught it in review a day later, as an outside-the-diff comment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two different questions
&lt;/h2&gt;

&lt;p&gt;That slip made something obvious that I had been blurring:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Did the checks pass on this exact change?&lt;/strong&gt; A receipt answers that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is this the change the message describes?&lt;/strong&gt; Nothing in our pipeline asked.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most "verify the agent" setups only answer the first one. Tests catch broken behaviour, not a correct-looking change in the wrong place. And when the subject line says "name", a human reviewer reads the diff expecting the name, which is exactly how this kind of slip gets approved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we added
&lt;/h2&gt;

&lt;p&gt;Since 0.5.0, the same git hook and the CI check read each commit's subject against its diff. The rule is deliberately narrow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A word in the subject names something in a changed code file, that line is left alone, and a line of the same shape within 12 lines of it did change. That is the wrong-line slip: &lt;code&gt;'name' =&amp;gt; [...]&lt;/code&gt; untouched, its twin &lt;code&gt;'email' =&amp;gt; [...]&lt;/code&gt; edited.&lt;/li&gt;
&lt;li&gt;A number in the subject that no added line contains, such as &lt;code&gt;200&lt;/code&gt; in the message when the diff writes &lt;code&gt;250&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It ignores prose, JSON, YAML and HTML files, tests, merges, reverts and commits touching more than 25 files. Agents commit with &lt;code&gt;git commit -m&lt;/code&gt;, so the hook can print the warning before the commit exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ai-verify: the commit message may not describe this change:
  The subject names `name`, but its line app/Http/Requests/StoreContactMessageRequest.php:26 is unchanged; the change went to the similar line 27: `'email' =&amp;gt; ['required', 'email', 'max:250'],`
  If the change is right, say what it does; if the message is right, change the code (git commit --amend).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent reads stderr, so it gets the chance to fix either the code or the message right away. In CI the same finding becomes a warning annotation on the changed line.&lt;/p&gt;

&lt;h2&gt;
  
  
  How noisy is it?
&lt;/h2&gt;

&lt;p&gt;Before shipping it we ran it over 937 commits from five of our own PHP repositories. It flagged 23 of them, about 2.5%. One of the 23 was the commit above. That is low enough to leave it on by default as a warning, and it is why the default is &lt;code&gt;warn&lt;/code&gt;, not &lt;code&gt;fail&lt;/code&gt;: it is a heuristic, and a heuristic that blocks commits gets switched off.&lt;/p&gt;

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

&lt;p&gt;It does not understand intent. A commit that says "fix rounding" and rounds the wrong way passes. A message that is too vague to name anything passes. It catches one specific, very common shape of mistake: the edit landed on the neighbour of the line the message talks about. That shape is common precisely because agents edit by pattern, and similar lines sit next to each other in validation rules, config arrays and route files.&lt;/p&gt;

&lt;p&gt;The bigger lesson for me is about what a green check means. "Verified" should say what was verified. Our receipt now says which checks ran on which tree, and the message check says whether the subject matches the diff. Neither says the change is right. That part is still review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;How do you check that an agent's commit does what its message says? Do you rely on a review bot, on humans reading every diff, or on something deterministic that runs before the commit lands?&lt;/p&gt;




&lt;p&gt;The package is a Laravel port of the verification step from &lt;a href="https://semitexa.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=receipt-vs-claim" rel="noopener noreferrer"&gt;Semitexa&lt;/a&gt;, the PHP framework I build. The message check, the receipts and the Action are in the &lt;a href="https://github.com/semitexa/laravel-ai-verify" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>laravel</category>
      <category>php</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Swoole PHP Frameworks in 2026: Laravel Octane vs Hyperf vs Semitexa</title>
      <dc:creator>Taras Hanych</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/semitexa/swoole-php-frameworks-in-2026-laravel-octane-vs-hyperf-vs-semitexa-4hn9</link>
      <guid>https://dev.to/semitexa/swoole-php-frameworks-in-2026-laravel-octane-vs-hyperf-vs-semitexa-4hn9</guid>
      <description>&lt;p&gt;Keeping a PHP application in memory is no longer exotic. The question in 2026 is which shape of it fits your project: an existing Laravel app on an application server, a coroutine framework built for services, or a framework designed around the worker from the start.&lt;/p&gt;

&lt;p&gt;This guide compares three ways to run PHP on Swoole: &lt;a href="https://laravel.com/docs/13.x/octane" rel="noopener noreferrer"&gt;Laravel Octane&lt;/a&gt;, &lt;a href="https://github.com/hyperf/hyperf" rel="noopener noreferrer"&gt;Hyperf&lt;/a&gt; and Semitexa. They are not three versions of the same product. Octane is a package that serves a normal Laravel application from a long-running server. Hyperf and Semitexa are frameworks written for that server. That difference matters more than any benchmark, so the comparison starts there. Facts about Octane and Hyperf come from their own documentation and repositories, checked on October 17, 2026; we build Semitexa, so read that part knowing who wrote it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Choose Laravel Octane if you already run Laravel.&lt;/strong&gt; You keep the ecosystem, the packages and the team's habits, and gain a warm application between requests. You also take on the job of keeping your own code free of request state in long-lived objects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Hyperf for coroutine-heavy services.&lt;/strong&gt; Microservices, RPC, many concurrent outbound calls, connection pools everywhere. It is the most mature Swoole-native framework of the three, with a large component set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Semitexa for a new application where the server owns the page and AI agents work on the code.&lt;/strong&gt; Server-rendered pages with live updates over SSE, multi-tenant SaaS, and a codebase that agents query and verify instead of grep. It is the youngest of the three, with the smallest ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laravel Octane vs Hyperf vs Semitexa at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Laravel Octane&lt;/th&gt;
&lt;th&gt;Hyperf&lt;/th&gt;
&lt;th&gt;Semitexa&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;A package that boots a Laravel app once and serves requests from memory.&lt;/td&gt;
&lt;td&gt;A coroutine framework for Swoole or Swow.&lt;/td&gt;
&lt;td&gt;A framework with a Swoole-first runtime, designed for long-running workers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version and PHP&lt;/td&gt;
&lt;td&gt;Octane 2.x (v2.22.0), PHP 8.1+, Laravel 10 to 13.&lt;/td&gt;
&lt;td&gt;Hyperf 3.2, PHP 8.2+.&lt;/td&gt;
&lt;td&gt;Date-tagged releases, PHP 8.4, Swoole 6.x.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Servers&lt;/td&gt;
&lt;td&gt;FrankenPHP, Swoole, Open Swoole, RoadRunner.&lt;/td&gt;
&lt;td&gt;Swoole 5+ or Swow.&lt;/td&gt;
&lt;td&gt;Swoole, inside Docker.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State between requests&lt;/td&gt;
&lt;td&gt;Laravel resets its own state; your singletons and statics are yours to keep clean.&lt;/td&gt;
&lt;td&gt;Container objects are singletons; per-request data goes in the coroutine &lt;code&gt;Context&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Services are worker-scoped; handlers and request, auth, tenant and locale context are execution-scoped.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Octane::concurrently&lt;/code&gt;, ticks, cache and tables, on Swoole only.&lt;/td&gt;
&lt;td&gt;Coroutines throughout, pooled clients.&lt;/td&gt;
&lt;td&gt;Coroutines, Channel-based connection pools with bounded waiting.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-tenancy&lt;/td&gt;
&lt;td&gt;Third-party, usually stancl/tenancy.&lt;/td&gt;
&lt;td&gt;Small community packages.&lt;/td&gt;
&lt;td&gt;Built in: ORM, cache and storage scope to the resolved tenant.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live pages&lt;/td&gt;
&lt;td&gt;Blade; SSE responses in Laravel 13; WebSockets through Reverb.&lt;/td&gt;
&lt;td&gt;Blade, Twig and others; a WebSocket server component.&lt;/td&gt;
&lt;td&gt;Server-rendered pages with deferred blocks and live updates over SSE.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI coding agents&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://laravel.com/docs/13.x/boost" rel="noopener noreferrer"&gt;Laravel Boost&lt;/a&gt;: guidelines, skills and an MCP server.&lt;/td&gt;
&lt;td&gt;Early MCP packages.&lt;/td&gt;
&lt;td&gt;Project Graph for impact queries, &lt;code&gt;ai:verify&lt;/code&gt; for every change.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem&lt;/td&gt;
&lt;td&gt;Laravel's, the largest in PHP.&lt;/td&gt;
&lt;td&gt;Large component set, a mostly Chinese-speaking community.&lt;/td&gt;
&lt;td&gt;Young and small; first-party packages only.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Versions and counts change. Octane 2.22.0 and Hyperf 3.2.0 were current on Packagist when this was written.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a long-running worker changes
&lt;/h2&gt;

&lt;p&gt;Under PHP-FPM, each request starts a clean application and throws it away at the end. A long-running worker boots once and serves many requests, often several at a time with coroutines. That removes repeated bootstrap work, and it turns object lifetimes into a design decision: anything that survives the request can carry one user's data into the next user's response.&lt;/p&gt;

&lt;p&gt;All three frameworks answer the same three questions differently. Which objects live with the worker? Where does per-request data go? What stops one slow request from holding up the others? &lt;a href="https://semitexa.com/blog/long-running-php-semitexa?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=octane-vs-hyperf-vs-semitexa" rel="noopener noreferrer"&gt;Long-running PHP: inside the Semitexa runtime&lt;/a&gt; walks through the model with a live example; here the focus is on how each framework draws the lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laravel Octane: your Laravel app, kept warm
&lt;/h2&gt;

&lt;p&gt;Octane's strength is that it asks very little of an existing Laravel application. Routes, controllers, form requests and middleware stay as they are, and type-hinting the &lt;code&gt;Request&lt;/code&gt; in a controller method is safe. Service providers register and boot once per worker, and requests reuse the result.&lt;/p&gt;

&lt;p&gt;The cost is a rule you now have to follow everywhere. The &lt;a href="https://laravel.com/docs/13.x/octane#dependency-injection-and-octane" rel="noopener noreferrer"&gt;Octane documentation&lt;/a&gt; says Laravel resets its own state between requests but "does not always know how to reset the global state created by your application". Injecting the container, the request or the config repository into a singleton leaves it holding a stale copy; static arrays that grow per request leak memory. The documented fixes are a resolver closure instead of the object, or passing request data at call time. Workers restart after 500 requests by default as a safety net, and &lt;code&gt;config/octane.php&lt;/code&gt; lets you list bindings to flush per request.&lt;/p&gt;

&lt;p&gt;Some of Octane's extras depend on the server. Concurrent tasks, ticks, the Octane cache and tables are Swoole features; on FrankenPHP or RoadRunner you get the warm application without them. Multi-tenancy comes from packages such as &lt;a href="https://tenancyforlaravel.com/docs/v3/" rel="noopener noreferrer"&gt;stancl/tenancy&lt;/a&gt;, so check how your tenancy package handles long-lived workers before you switch.&lt;/p&gt;

&lt;p&gt;For AI-assisted work, Laravel has the most complete official story of the three: &lt;a href="https://laravel.com/docs/13.x/boost" rel="noopener noreferrer"&gt;Boost&lt;/a&gt; gives coding agents guidelines, skills and an MCP server that can read your schema, query the database and see the last error, and the &lt;a href="https://laravel.com/docs/13.x/ai-sdk" rel="noopener noreferrer"&gt;Laravel AI SDK&lt;/a&gt; covers agents inside the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hyperf: coroutines all the way down
&lt;/h2&gt;

&lt;p&gt;Hyperf was written for Swoole, and it shows. Coroutines run throughout the framework, its database and Redis clients are coroutine-aware, and built-in components handle connection pooling so you do not acquire and release connections by hand. For services that spend most of their time waiting on other services, that model is the point.&lt;/p&gt;

&lt;p&gt;Its rule for state is short and strict. The &lt;a href="https://github.com/hyperf/hyperf/blob/master/docs/en/di.md" rel="noopener noreferrer"&gt;DI documentation&lt;/a&gt; says objects managed by the container are all singletons and must not hold request state. Per-request data goes into &lt;code&gt;Hyperf\Context\Context&lt;/code&gt;, which belongs to the current coroutine and is released when it ends. Globals and statics are shared by every coroutine in the worker, and a blocking call stalls them all, so every I/O client has to be coroutine-enabled.&lt;/p&gt;

&lt;p&gt;Hyperf uses attributes for routing and injection, supports lazy proxies and offers AOP through generated proxy classes, which apply only to objects the container creates. Views are supported through several engines, but rendering runs in a task worker by default, where the request and session are not available. Multi-tenancy is left to small community packages. The project reports about 104,000 requests per second in its own README benchmark; there is no independent 2026 figure to compare it with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semitexa: the worker as the starting point
&lt;/h2&gt;

&lt;p&gt;Semitexa starts from the same place as Hyperf, an application that lives in a Swoole worker, and adds two opinions: there is one way through a request, and the framework should be able to describe itself to the people and agents changing it.&lt;/p&gt;

&lt;p&gt;A request is a typed payload, a handler, a resource, then rendering. The container knows two lifetimes. Services injected with &lt;code&gt;#[InjectAsReadonly]&lt;/code&gt; are built once per worker. Handlers are cloned for each execution, and anything injected with &lt;code&gt;#[InjectAsMutable]&lt;/code&gt;, such as the request or the tenant context, belongs to that execution. Database connections come from a Channel-based pool with bounded waiting. Workers recycle after a configurable number of requests and also watch their own memory, leaving through the same drain a reload uses before PHP's limit would cut a request off.&lt;/p&gt;

&lt;p&gt;Tenancy is part of the framework. The tenant is resolved per request by path, subdomain, header or query parameter, and the ORM scopes queries, the cache namespaces keys and storage isolates paths to it; &lt;a href="https://semitexa.com/blog/tenant-context-background-work?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=octane-vs-hyperf-vs-semitexa" rel="noopener noreferrer"&gt;the tenant also travels with queued work&lt;/a&gt;. Pages are rendered on the server, slow regions are deferred and streamed in over SSE, and server-side changes can be pushed to pages that are already open. The &lt;a href="https://semitexa.com/blog/live-data-grid?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=octane-vs-hyperf-vs-semitexa" rel="noopener noreferrer"&gt;live data grid&lt;/a&gt; is one example.&lt;/p&gt;

&lt;p&gt;For coding agents, the framework scans itself into a &lt;a href="https://semitexa.com/blog/project-graph-semitexa?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=octane-vs-hyperf-vs-semitexa" rel="noopener noreferrer"&gt;Project Graph&lt;/a&gt; that answers "who uses this class" and "what does this change affect" before an edit, and &lt;code&gt;bin/semitexa ai:verify&lt;/code&gt; runs the checks a change touches and records what it checked. If you work in Laravel, the same idea is available as &lt;a href="https://github.com/semitexa/laravel-ai-verify" rel="noopener noreferrer"&gt;semitexa/laravel-ai-verify&lt;/a&gt;, without leaving the framework.&lt;/p&gt;

&lt;p&gt;The honest limits: Semitexa requires PHP 8.4 and Swoole 6.x and is developed and deployed with Docker; it has no FPM mode. Its ecosystem is its own packages, and its community is small. It runs semitexa.com and a few production projects, not thousands.&lt;/p&gt;

&lt;h2&gt;
  
  
  One task, three frameworks: the current tenant
&lt;/h2&gt;

&lt;p&gt;A small task shows the difference in where state lives: a service needs the current tenant. These are minimal sketches of each framework's documented approach, not complete code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Laravel Octane
&lt;/h3&gt;

&lt;p&gt;Do not hand the singleton the request or a tenant object when it is built. Give it a way to ask for the current one, as the Octane docs recommend for the container and request.&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;// A singleton built once per worker must not keep the first request's tenant.&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;app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InvoiceService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InvoiceService&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="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CurrentTenant&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&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;h3&gt;
  
  
  Hyperf
&lt;/h3&gt;

&lt;p&gt;Keep the tenant in the coroutine context and read it where you need it.&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;Hyperf\Context\Context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// In middleware, once per request (coroutine):&lt;/span&gt;
&lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$tenantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// In a singleton service:&lt;/span&gt;
&lt;span class="nv"&gt;$tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Context&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;'tenant_id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Semitexa
&lt;/h3&gt;

&lt;p&gt;Declare the lifetime on the property. The handler is a fresh clone per execution, and the tenant context is injected for that execution.&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="na"&gt;#[AsPayloadHandler(payload: InvoicePayload::class, resource: InvoiceResource::class)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvoiceHandler&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;TypedHandlerInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;InjectAsReadonly&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;// built once per worker&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;InvoiceRepository&lt;/span&gt; &lt;span class="nv"&gt;$invoices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;InjectAsMutable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="c1"&gt;// this execution's tenant&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;TenantContextInterface&lt;/span&gt; &lt;span class="nv"&gt;$tenantContext&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;None of the three makes the mistake impossible. They differ in where the rule is written: in your discipline with Octane, in a context API with Hyperf, and in the type and attribute of each dependency with Semitexa, where a review or an agent can see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;h3&gt;
  
  
  You have a Laravel application
&lt;/h3&gt;

&lt;p&gt;Start with Octane. Audit your singletons and statics, load-test, and keep the worker restart limit until you trust the code. Rewriting a working Laravel app in another framework for speed alone rarely pays off.&lt;/p&gt;

&lt;h3&gt;
  
  
  You are building services that mostly wait on other services
&lt;/h3&gt;

&lt;p&gt;Hyperf's coroutine model, pools and service components are built for that work, and it has years of production use behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  You are starting a product with live pages, tenants and AI agents in the workflow
&lt;/h3&gt;

&lt;p&gt;That is the case Semitexa was designed for: one typed path per request, tenancy and SSE in the framework, and a codebase agents can query and verify. Weigh that against a younger ecosystem and the PHP 8.4, Docker and Swoole requirements.&lt;/p&gt;

&lt;p&gt;See the runtime before you choose: &lt;a href="https://framework.semitexa.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=octane-vs-hyperf-vs-semitexa" rel="noopener noreferrer"&gt;80+ live Semitexa demos&lt;/a&gt;, each running on the server you are reading, or &lt;a href="https://semitexa.com/docs/get-started?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=octane-vs-hyperf-vs-semitexa" rel="noopener noreferrer"&gt;get started with Semitexa&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Laravel Octane a Swoole framework?
&lt;/h3&gt;

&lt;p&gt;No. Octane is a Laravel package that serves a Laravel application from a long-running server. Swoole is one of four servers it supports, alongside Open Swoole, RoadRunner and FrankenPHP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Hyperf faster than Laravel Octane?
&lt;/h3&gt;

&lt;p&gt;Hyperf publishes higher numbers in its own benchmark, and a coroutine-native stack usually does less work per request than a full Laravel boot kept warm. For most applications the database and external calls decide response time, so measure your own workload before choosing on throughput.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Laravel packages in Hyperf or Semitexa?
&lt;/h3&gt;

&lt;p&gt;Not as Laravel packages. Hyperf has its own components and can use framework-agnostic Composer libraries. Semitexa uses its own packages plus framework-agnostic libraries. If the Laravel ecosystem is the reason you chose Laravel, Octane keeps it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Semitexa run without Swoole or Docker?
&lt;/h3&gt;

&lt;p&gt;No. Semitexa targets PHP 8.4 with Swoole 6.x and ships its runtime as Docker images, so the host needs Docker rather than a local PHP install.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the biggest risk when moving to long-running PHP?
&lt;/h3&gt;

&lt;p&gt;State that outlives the request: a singleton that captured the first user, a static cache that only grows, a connection one coroutine is still using. Each framework gives you tools for it; none removes the need to think about lifetimes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://semitexa.com/blog/octane-vs-hyperf-vs-semitexa?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=octane-vs-hyperf-vs-semitexa" rel="noopener noreferrer"&gt;Semitexa blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Why is there still no plug-and-play data grid?</title>
      <dc:creator>Taras Hanych</dc:creator>
      <pubDate>Thu, 15 Oct 2026 14:53:36 +0000</pubDate>
      <link>https://dev.to/syntaxwanderer_26/why-is-there-still-no-plug-and-play-data-grid-5bbh</link>
      <guid>https://dev.to/syntaxwanderer_26/why-is-there-still-no-plug-and-play-data-grid-5bbh</guid>
      <description>&lt;p&gt;I've built more forms and data grids than I can count, and no two were alike. I'll never forget the magic of Magento 2 grids, or the hours I spent finding out why the magic wasn't working. In the end it was routine work, and then just boring work.&lt;/p&gt;

&lt;p&gt;Every time, one thought came back: why is there still no universal, plug-and-play grid? A rhetorical question with a hundred answers. Here is mine, and what I tried instead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9a4mhmqwmou4ugzq8ij.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9a4mhmqwmou4ugzq8ij.gif" alt="Two tabs of the live grid: one places trades, the other gets the new rows over SSE" width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A grid usually knows too much
&lt;/h2&gt;

&lt;p&gt;Look at any admin grid you've shipped. The backend knows which columns exist, which ones can be sorted, which filters are allowed and how big a page is. Then the frontend knows all of it again: column definitions, sort keys, filter widgets, page sizes. Two descriptions of the same thing, kept in sync by hand.&lt;/p&gt;

&lt;p&gt;They drift. Someone adds a sortable column on the server and forgets the client. Someone adds a filter in the UI that the query silently ignores. Every "plug-and-play" grid library I've used solved the rendering and left this part to me, because it can't know my backend.&lt;/p&gt;

&lt;p&gt;Then the grid needs to be live, and it gets worse. Now there is a client store holding a copy of the data, and a WebSocket handler trying to keep that copy honest. Two systems that must agree: the server's truth and the browser's copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if the grid knew nothing?
&lt;/h2&gt;

&lt;p&gt;In Semitexa, the PHP framework I'm building, I tried the opposite. The page gives the grid an endpoint and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;include&lt;/span&gt; &lt;span class="s1"&gt;'@platform-ui/components/runtime/grid-v2.html.twig'&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;gridId&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'showroom-exchange'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;endpoint&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'/_feed/exchange'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt; &lt;span class="nv"&gt;only&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No columns, no sort keys, no filter definitions. The grid's first request is &lt;code&gt;OPTIONS /_feed/exchange&lt;/code&gt;, and the answer is the route contract, built from the same attributes that run the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"site.showroom.exchange"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"modes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"plain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sse-update"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"collection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pagination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"modes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"defaultPerPage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"perPageOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"asset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"trader"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"asset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"eq"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"side"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"eq"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"eq"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"live"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"site_showroom_exchange"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same declarations guard the way back in. Ask for a sort the contract doesn't offer and the request is refused before any row is read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="s2"&gt;"https://semitexa.com/_feed/exchange?sort=lot"&lt;/span&gt;
HTTP 400
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;: &lt;span class="s2"&gt;"invalid_sort"&lt;/span&gt;, &lt;span class="s2"&gt;"allowed_fields"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"at"&lt;/span&gt;, &lt;span class="s2"&gt;"asset"&lt;/span&gt;, &lt;span class="s2"&gt;"trader"&lt;/span&gt;, &lt;span class="s2"&gt;"amount"&lt;/span&gt;, &lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the part I stopped describing twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The table is a subscription
&lt;/h2&gt;

&lt;p&gt;The second half is the live part. The grid doesn't keep a dataset. It subscribes over Server-Sent Events with its current view: page, page size, sort, search and filters. From then on the server holds that view.&lt;/p&gt;

&lt;p&gt;Changing the sort doesn't fetch. The new view goes up as a small POST, and the answer arrives on the stream, in the same &lt;code&gt;{data, meta}&lt;/code&gt; envelope a plain JSON request would return. The browser holds exactly one page, the one on screen.&lt;/p&gt;

&lt;p&gt;When the data changes, the server re-runs each subscriber's own view and sends only what moved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;event:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ui.collection.patch&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;data:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"patch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                 &lt;/span&gt;&lt;span class="nl"&gt;"upsert"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TRX-985F23"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"asset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$108,268.81"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
                 &lt;/span&gt;&lt;span class="nl"&gt;"remove"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"TRX-1A0C7E"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
                 &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"TRX-985F23"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TRX-4C2BBB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TRX-673208"&lt;/span&gt;&lt;span class="p"&gt;]}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A visitor on page three filtered to gold gets the patch for page three filtered to gold. A visitor whose page didn't change gets nothing. Rows that didn't change keep their DOM nodes, so focus and selection survive the update.&lt;/p&gt;

&lt;p&gt;Nothing polls. The feed declares the scope it watches, and the code that changes the data says so:&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;$trade&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;trades&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;place&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;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getAsset&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;invalidator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'site_showroom_exchange'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The bug was production, not the grid
&lt;/h2&gt;

&lt;p&gt;semitexa.com runs four Swoole workers, each with its own in-memory cache. The first version kept the demo trades in that cache, so a trade placed on one worker was invisible to feeds held by the others. Locally, with a shared Redis cache, it worked perfectly. The tape lives in MySQL now.&lt;/p&gt;

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

&lt;p&gt;Server-owned views are not free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One query per subscriber per publish.&lt;/strong&gt; A thousand open grids on one scope re-run a thousand views. Patches keep the wire small, not the database work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every open tab holds a connection.&lt;/strong&gt; One stream per page instead of one per widget, but still one per tab. Turn off proxy buffering, or frames sit in a buffer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The grid itself is plain.&lt;/strong&gt; No column resize, no inline editing, no charts in cells yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, plug and play? Not for every stack. It works because the framework already describes its endpoints in a form the grid can read. But for the first time, a grid was the part I didn't have to describe twice.&lt;/p&gt;

&lt;p&gt;You can try it with two tabs: place a trade in one and watch the other. The other traders are simulated, the frames are real, and a panel under the table shows each one as it crosses the wire: &lt;a href="https://semitexa.com/blog/live-data-grid?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=live-data-grid" rel="noopener noreferrer"&gt;semitexa.com/blog/live-data-grid&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Which grid cost you the most hours, and was it the rendering or keeping two descriptions in sync?&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>discuss</category>
    </item>
    <item>
      <title>AI code verification for Laravel.</title>
      <dc:creator>Taras Hanych</dc:creator>
      <pubDate>Mon, 12 Oct 2026 06:32:20 +0000</pubDate>
      <link>https://dev.to/syntaxwanderer_26/ai-code-verification-for-laravel-1nm2</link>
      <guid>https://dev.to/syntaxwanderer_26/ai-code-verification-for-laravel-1nm2</guid>
      <description>&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;, &lt;strong&gt;Cursor&lt;/strong&gt;, &lt;strong&gt;Codex&lt;/strong&gt;, &lt;strong&gt;Copilot&lt;/strong&gt; or &lt;strong&gt;Gemini&lt;/strong&gt; says the change is done. php artisan ai:verify checks whether it is.&lt;/p&gt;

&lt;p&gt;It runs only the tests, Larastan, Pint and Laravel boot checks the diff actually needs. It fails the change if the agent deleted, skipped or weakened a test to get green. It returns one verdict the agent must act on: pass, fail or incomplete. One more command writes the instructions into AGENTS.md and CLAUDE.md. For Claude Code it also adds a Stop hook, so the agent cannot finish while verification fails. Verified commits get a trailer that CI can check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before the agent edits anything, php artisan ai:graph gives it a code graph of the Laravel app&lt;/strong&gt;. It covers routes → controllers → form requests → models → tables → policies → views → tests, and events → listeners. It also does impact analysis: what a change affects and which tests cover it.&lt;/p&gt;

&lt;p&gt;composer require --dev semitexa/laravel-ai-verify&lt;br&gt;
php artisan ai:verify:install --hook&lt;/p&gt;

&lt;p&gt;How it works: ai:verify reads what changed (uncommitted work, a git ref, or a file list). It works out the smallest set of checks that can vouch for that change and runs them with timeouts. The verdict comes as streamed NDJSON for agents, a JSON envelope or terminal text.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc26hg9rpuikpax1ce34f.png" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc26hg9rpuikpax1ce34f.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>ai</category>
      <category>review</category>
      <category>varification</category>
    </item>
    <item>
      <title>A timeout tells you what you know. Not what the receiver did.</title>
      <dc:creator>Taras Hanych</dc:creator>
      <pubDate>Thu, 08 Oct 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/syntaxwanderer_26/a-timeout-tells-you-what-you-know-not-what-the-receiver-did-1djm</link>
      <guid>https://dev.to/syntaxwanderer_26/a-timeout-tells-you-what-you-know-not-what-the-receiver-did-1djm</guid>
      <description>&lt;p&gt;Picture an integration. Your app sends a webhook: order 42 is confirmed, please create the shipment. The partner commits the shipment and returns success. The connection breaks before your sender reads the response.&lt;/p&gt;

&lt;p&gt;From your side, delivery failed. From theirs, the work is done.&lt;/p&gt;

&lt;p&gt;A timeout tells you what &lt;em&gt;you&lt;/em&gt; know. It says nothing about what the receiver did. So you retry, and the only thing standing between your customer and a second parcel is whether the receiver can recognize that this is the same thing again.&lt;/p&gt;

&lt;p&gt;I ran into this while documenting the webhook module of Semitexa, the PHP framework I'm building. The mechanics were fine. What turned out to be hard was the vocabulary: "the key" meant three different things depending on who was asking.&lt;/p&gt;

&lt;h2&gt;
  
  
  One key, three questions
&lt;/h2&gt;

&lt;p&gt;When people say "make it idempotent", they usually mean one key. But there are three separate questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Publication identity:&lt;/strong&gt; did &lt;em&gt;we&lt;/em&gt; already queue this outbound intent for this endpoint?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event identity:&lt;/strong&gt; did the &lt;em&gt;receiver&lt;/em&gt; already record this event?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business identity:&lt;/strong&gt; is the &lt;em&gt;effect&lt;/em&gt; already done, one shipment for this fulfilment intent?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the example, that could be &lt;code&gt;order:42:confirmed:v1&lt;/code&gt;, &lt;code&gt;evt-order-42-v1&lt;/code&gt;, and a business rule. They protect different things, and mixing them up fails in two opposite directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A fresh key on every retry&lt;/strong&gt; and the receiver can never recognize the repeat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One key for everything about order 42&lt;/strong&gt; and the next legitimate shipment (a replacement, a second confirmation) gets swallowed as a duplicate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule I ended up writing down: the key describes &lt;em&gt;what may happen once&lt;/em&gt;, and it has to survive every attempt at that same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry the right failures
&lt;/h2&gt;

&lt;p&gt;Not every failure deserves another attempt. The policy we settled on:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2xx&lt;/td&gt;
&lt;td&gt;delivered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;408, 429&lt;/td&gt;
&lt;td&gt;retry if attempts remain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;other 4xx&lt;/td&gt;
&lt;td&gt;permanent rejection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5xx, timeout, no status&lt;/td&gt;
&lt;td&gt;retry with exponential backoff and jitter, within a budget&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 400 is the receiver telling you the request is wrong. Sending it again ten times is just noise. A 503 or a timeout is the opposite: you don't know, so you ask again, and the receiver's memory makes asking again safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Duplicate" is not "completed"
&lt;/h2&gt;

&lt;p&gt;This one is quiet. The receiver gets the event, records it in its inbox, and crashes before creating the shipment. Your retry arrives. The inbox finds the record and says: duplicate, ignore.&lt;/p&gt;

&lt;p&gt;The shipment never happens, and every log line looks healthy.&lt;/p&gt;

&lt;p&gt;An inbox match tells you the event was &lt;em&gt;seen&lt;/em&gt;. It does not tell you the work was &lt;em&gt;done&lt;/em&gt;. The recovery decision has to look at business state, not only at the inbox. Where the action is a local database write, the completion record and the business update should commit together. Where it's an external side effect, you need the recipient's own idempotency mechanism or an explicit reconciliation path.&lt;/p&gt;

&lt;h2&gt;
  
  
  A lease protects the record, not the request
&lt;/h2&gt;

&lt;p&gt;Delivery workers claim a record with a lease. Suppose a worker sends the HTTP request, then loses its lease before writing "delivered". A well-behaved worker won't overwrite a record it no longer owns, and ours doesn't. Good. But the request has already gone out. The receiver may already have acted.&lt;/p&gt;

&lt;p&gt;The lease coordinates &lt;em&gt;your&lt;/em&gt; bookkeeping. It cannot retract anything on the other side. Which brings it back to the receiver: the repeat has to be recognizable there, by the event identity, and the effect has to be protected where it happens, by the business identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd check in any integration
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Can you name all three identities for your most important webhook?&lt;/li&gt;
&lt;li&gt;Does a retry carry the &lt;em&gt;same&lt;/em&gt; event identity as the first attempt?&lt;/li&gt;
&lt;li&gt;If the receiver records and then crashes, what finishes the work?&lt;/li&gt;
&lt;li&gt;Do you inspect business state before you replay anything by hand?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If question 3 has no answer, that's the one I'd fix first.&lt;/p&gt;

&lt;p&gt;Which of the three identities does your integration actually store today?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The longer version, with the retry ranges, the inbox key format and the recovery cases, is on the Semitexa blog: &lt;a href="https://semitexa.com/blog/webhook-delivered-twice" rel="noopener noreferrer"&gt;Your Webhook Was Delivered Twice. Now What?&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>php</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Your AI agent doesn't understand your codebase. It greps it.</title>
      <dc:creator>Taras Hanych</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/syntaxwanderer_26/your-ai-agent-doesnt-understand-your-codebase-it-greps-it-9kp</link>
      <guid>https://dev.to/syntaxwanderer_26/your-ai-agent-doesnt-understand-your-codebase-it-greps-it-9kp</guid>
      <description>&lt;p&gt;Before touching a class, a careful developer asks one question: who depends on this?&lt;/p&gt;

&lt;p&gt;An agent answers it the only way it can. It runs a text search, reads whatever comes back, and decides the change is local. Most of the time that's fine. The times it isn't are the expensive ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  What grep can't see
&lt;/h2&gt;

&lt;p&gt;I've been writing PHP for over 18 years, and the agent-written bugs that cost me the most had the same shape. The edit looked local. The nearby tests passed. Something three modules away broke.&lt;/p&gt;

&lt;p&gt;In a modern PHP app a lot of wiring never appears as a string you can search for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A handler is connected to its payload by an attribute, not by a call.&lt;/li&gt;
&lt;li&gt;An event listener is discovered at boot by scanning, so nothing "calls" it.&lt;/li&gt;
&lt;li&gt;A template reads a field by name, so renaming a property breaks a page that never imports the class.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Grep finds the string. It doesn't find the edge. So the agent does what a cautious person does with a flashlight in a dark warehouse: it reads more. Half the project lands in context "just to be sure", the answer gets slower and more expensive, and it is still a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the agent a map instead
&lt;/h2&gt;

&lt;p&gt;While building Semitexa, my PHP framework, I stopped asking agents to rebuild the structure from text on every task. The framework keeps a project graph: real edges between classes, collected from the same attributes and conventions the runtime itself uses. Edges like &lt;code&gt;implements&lt;/code&gt;, &lt;code&gt;handles&lt;/code&gt;, &lt;code&gt;serves_route&lt;/code&gt;, &lt;code&gt;returns&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before an edit, the agent asks two questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/semitexa ai:review-graph:query &lt;span class="nt"&gt;--usages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;Class&amp;gt;
bin/semitexa ai:review-graph:impact &amp;lt;Class&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first lists what points at a class. The second walks outward and returns the blast radius: the routes, handlers and templates downstream of a change. It comes back as data, so the agent acts on it instead of guessing.&lt;/p&gt;

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

&lt;p&gt;The side effect I liked most wasn't accuracy. The agent stopped reading half the project. With the impact list in hand it opens the files that matter and leaves the rest alone. Less context, better decisions.&lt;/p&gt;

&lt;p&gt;It also changed code review. "What else did this touch?" used to be a question I answered by reading the diff and remembering the codebase. Now it's a query.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-offs
&lt;/h2&gt;

&lt;p&gt;A map has its own failure modes, and they're worth being honest about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It goes stale.&lt;/strong&gt; A graph built once describes code that no longer exists five edits later. Ours refreshes by content hash when a query runs, and says so when it can't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It has blind spots.&lt;/strong&gt; Anything wired dynamically, like a class name built from a string, is invisible to a static scan. The useful behaviour is for the graph to say what it could not see instead of pretending coverage is complete. An impact report with an "unknown" section is more honest than a clean one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is only as good as the conventions.&lt;/strong&gt; The graph works because the framework has one way to wire a handler and one way to declare a route. In a codebase where routes come from YAML on Monday and attributes on Friday, there are no clean edges to collect. That is a big part of why I care about frameworks having one way to do each job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grep still has a job
&lt;/h2&gt;

&lt;p&gt;I use grep every day, and so does the agent. It is the right tool for finding text. It is the wrong tool for understanding a system, because a system is made of relationships, and relationships are exactly what text search throws away.&lt;/p&gt;

&lt;p&gt;If you run agents on a large codebase, here is something cheap to try even without a framework: generate a rough dependency list for the area being changed and put it in front of the agent before it starts editing. Watch how much less it reads.&lt;/p&gt;

&lt;p&gt;How do your agents judge the impact of a change today: tests, grep, a map, or trust?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm building Semitexa in the open. The project graph is described in more detail here: &lt;a href="https://semitexa.com/blog/project-graph-semitexa" rel="noopener noreferrer"&gt;Project Graph in Semitexa&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
      <category>programming</category>
      <category>php</category>
    </item>
    <item>
      <title>Your AI Agent Fixed the UI. Where Did the Screenshot Go?</title>
      <dc:creator>Taras Hanych</dc:creator>
      <pubDate>Fri, 02 Oct 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/syntaxwanderer_26/your-ai-agent-fixed-the-ui-where-did-the-screenshot-go-1fge</link>
      <guid>https://dev.to/syntaxwanderer_26/your-ai-agent-fixed-the-ui-where-did-the-screenshot-go-1fge</guid>
      <description>&lt;p&gt;Picture a normal afternoon with a coding agent. It fixes a broken table layout, runs the checks, and opens a PR. To make review easy, it attaches a before/after screenshot. You glance at it, the table looks right, you approve.&lt;/p&gt;

&lt;p&gt;Now the question nobody asked in that review: &lt;strong&gt;where is that screenshot hosted?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What PixelLeak found
&lt;/h2&gt;

&lt;p&gt;Glow Labs &lt;a href="https://www.glow.io/blogs/how-ai-agents-exposed-developer-screenshots-from-leading-tech-companies" rel="noopener noreferrer"&gt;published research they call PixelLeak&lt;/a&gt; this week. The mechanism is almost boring.&lt;/p&gt;

&lt;p&gt;GitHub lets you attach an image to a PR comment by dragging it into the browser. There is no equivalent for a CLI, which is where coding agents live. An agent that wants to show the reviewer a screenshot hits a wall. Rather than fail, agents found a workaround: create a &lt;strong&gt;public&lt;/strong&gt; repository under the developer's personal account, push the image there, and link to it from the PR. Some teams used an open source helper that does the same thing.&lt;/p&gt;

&lt;p&gt;Glow counted over 13,000 internal images in 900+ public repositories, from more than 300 organizations. The screenshots showed customer billing records, internal treasury and settlement consoles, money movement screens, and features that hadn't shipped yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent did its job
&lt;/h2&gt;

&lt;p&gt;What bothers me most: there is no villain in this story. Nobody told the agent to publish anything. It was told, more or less, "show the reviewer what you changed". A public repo is a place where an image URL works without authentication. From the agent's point of view, that is a perfectly good solution.&lt;/p&gt;

&lt;p&gt;We spend a lot of energy on what an agent may change in the code: which files, which commands, which branches. We spend much less on where its &lt;strong&gt;byproducts&lt;/strong&gt; go. Screenshots, logs, HAR files, screen recordings, test fixtures dumped from a real database. None of those are code, so none of them go through the same review. And a screenshot of an internal tool is often more sensitive than the diff it documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd put in place
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Treat review evidence as data.&lt;/strong&gt; A screenshot of a billing screen is customer data, the same as a database dump. It falls under the same rules about where it may be stored and who may see it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Give the agent a sanctioned place.&lt;/strong&gt; If there is a private, approved location for evidence (an artifact store of your CI, a folder inside the project that never leaves your infrastructure), the agent has no reason to improvise. Agents improvise when the obvious path is blocked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Make "I couldn't attach it" a valid outcome.&lt;/strong&gt; The workaround happened because failing felt worse than succeeding creatively. Say in your agent instructions, and ideally enforce in tooling, that "here is the local path, I could not upload it" is a complete answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Restrict what the agent's credentials can do.&lt;/strong&gt; If the token the agent uses cannot create public repositories, this whole class of leak is gone. That is a much stronger guarantee than a sentence in a prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Audit today.&lt;/strong&gt; Look at your GitHub account and your team's accounts for repositories nobody remembers creating, especially ones full of PNGs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this sits in my own work
&lt;/h2&gt;

&lt;p&gt;I'm building &lt;a href="https://semitexa.com" rel="noopener noreferrer"&gt;Semitexa&lt;/a&gt;, a PHP framework designed for working with AI agents. There, the agent's work log (what it planned, what it ran, what it verified) is kept as local files inside the project rather than on an external host. That was a design choice for reviewability, but PixelLeak made me realize it matters for leakage too. Screenshots and other visual evidence are not covered yet; they are on our list now, with the same rule: evidence stays where the code is. The longer version, with a test matrix for the refusal path, is &lt;a href="https://semitexa.com/blog/ai-agent-screenshot-leaks" rel="noopener noreferrer"&gt;on semitexa.com&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;How does your team handle visual evidence from agents today? Do you let them post screenshots to PRs at all, and if so, where do the files actually live?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>codereview</category>
      <category>discuss</category>
    </item>
    <item>
      <title>AGENTS.md doesn't work. Here's what did.</title>
      <dc:creator>Taras Hanych</dc:creator>
      <pubDate>Wed, 30 Sep 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/syntaxwanderer_26/agentsmd-doesnt-work-heres-what-did-1gg0</link>
      <guid>https://dev.to/syntaxwanderer_26/agentsmd-doesnt-work-heres-what-did-1gg0</guid>
      <description>&lt;p&gt;I spent a long time trying to make AI coding agents follow project rules through instruction files: AGENTS.md, CLAUDE.md, a rules folder. For a handful of basic instructions it works. Put your whole development "bible" in there and it falls apart.&lt;/p&gt;

&lt;p&gt;The pattern is probably familiar. The agent breaks a rule, apologizes, promises it won't happen again, and breaks it again two changes later. It isn't being lazy. A rule in the prompt is a wish: it competes with everything else in the context, and the longer the session, the weaker it gets.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually worked
&lt;/h2&gt;

&lt;p&gt;While building &lt;a href="https://semitexa.com" rel="noopener noreferrer"&gt;Semitexa&lt;/a&gt;, an open-source PHP framework designed for agent-driven development, I moved the rules out of the prompt and into a command. The only instruction the agent gets about rules is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Check every code change you make with &lt;code&gt;ai:verify&lt;/code&gt;. If it lets you pass, move forward.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;ai:verify&lt;/code&gt; looks at what changed and runs the lint and the tests that matter for that diff, plus the structural checks: where logic is allowed to live, what may call what. When it fails, the agent gets a concrete message about the thing it just did.&lt;/p&gt;

&lt;p&gt;Three things changed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The rules stopped competing for context.&lt;/strong&gt; Adding a constraint no longer makes the prompt longer, so I can have as many as the project needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failures became specific.&lt;/strong&gt; "Handler returns a raw Response" is something an agent can fix in one step. "Follow the architecture" is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent got better over the session.&lt;/strong&gt; The feedback arrives right after the mistake, not at review time, and more and more often the next change passes on the first try.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the instruction file is still for
&lt;/h2&gt;

&lt;p&gt;I didn't delete AGENTS.md. It got short. Now it's a map: where things live, which commands to run, and the one rule above. Anything that can be checked moved into the check. Anything that can't be checked (naming taste, whether an abstraction pulls its weight, whether the change matches the intent of the ticket) stays with human review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off
&lt;/h2&gt;

&lt;p&gt;A check costs more to write than a sentence. Some rules are genuinely fuzzy and never become checks. But every time I noticed myself writing the same review comment twice, it turned out the rule was never fuzzy, just unwritten. Those are the ones worth turning into code first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;Where do your project rules live today: in instruction files, in CI, or in reviewers' heads? And has anyone found a good way to measure how often an agent actually follows an instruction file?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
      <category>programming</category>
      <category>php</category>
    </item>
    <item>
      <title>From Idea to First Customer: How Semitexa Shortens Time to Market</title>
      <dc:creator>Taras Hanych</dc:creator>
      <pubDate>Mon, 28 Sep 2026 10:40:15 +0000</pubDate>
      <link>https://dev.to/semitexa/from-idea-to-first-customer-how-semitexa-shortens-time-to-market-767</link>
      <guid>https://dev.to/semitexa/from-idea-to-first-customer-how-semitexa-shortens-time-to-market-767</guid>
      <description>&lt;p&gt;&lt;em&gt;The first release of a business product has one job: help a real customer complete something valuable. Every week spent rebuilding common software foundations is a week before you can learn from that customer.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The clock stops at a customer outcome
&lt;/h2&gt;

&lt;p&gt;A deployed homepage is a milestone. A usable business is a different one. Imagine a client portal: a customer signs in, submits a request, sees its status, and receives an update when the team acts. The team can see who owns the request and respond. That complete loop creates value and produces feedback worth acting on.&lt;/p&gt;

&lt;p&gt;Time to market is the time until that loop works for its first customer. It includes product decisions, implementation, review and release. The shorter that path, the sooner a founder can validate demand, refine pricing and find out what customers actually need next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most launch delays hide between the features
&lt;/h2&gt;

&lt;p&gt;The visible screen is often the easy part. A real client portal also needs identity and permissions, persistent data, an interface people can use, notifications, background work and a way to check that one customer's information stays with that customer. Teams can spend their early budget joining these pieces before they get to the workflow that makes the product distinct.&lt;/p&gt;

&lt;p&gt;Semitexa addresses that integration work with a coherent set of modules and conventions. Its Framework has typed routes and handlers, data access, authentication and authorization, tenant context, events, queues and API capabilities. Its Platform provides reusable interface building blocks. The ecosystem describes the OS as the experience layer, Platform as the interface layer and Framework as the execution layer. Each layer gives the team a clearer starting point for a different part of the product.&lt;/p&gt;

&lt;p&gt;These capabilities still need product choices and configuration. Their value is that the team can make those choices around an existing structure, then spend more of its time on the customer journey.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the differentiator on a connected foundation
&lt;/h2&gt;

&lt;p&gt;For the portal example, the differentiator might be how requests are assessed, routed and resolved. Semitexa lets a team focus on that flow while using established paths for the surrounding work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Move from screen to behavior.&lt;/strong&gt; A page generator can scaffold the payload, handler, resource and template together, so the first interaction has an application path behind it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep customer boundaries visible.&lt;/strong&gt; Tenant resolution and tenant-aware data patterns give multi-customer products a place to express ownership early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let work continue after the response.&lt;/strong&gt; Events and queues provide a path for notifications and longer tasks without making the customer wait on the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expose the same operation where it is needed.&lt;/strong&gt; Typed API routes can support other clients and integrations as the product grows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can inspect these ideas in the running Framework Demo: &lt;a href="https://framework.semitexa.com/demo/cli/scaffolding-generators" rel="noopener noreferrer"&gt;scaffolding&lt;/a&gt;, &lt;a href="https://framework.semitexa.com/demo/platform/tenancy-isolation" rel="noopener noreferrer"&gt;tenant data isolation&lt;/a&gt; and &lt;a href="https://framework.semitexa.com/demo/events/queued" rel="noopener noreferrer"&gt;queued work&lt;/a&gt; each have their own walkthrough. The examples are useful because a buyer can look past a feature list and see the implementation shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release one complete workflow first
&lt;/h2&gt;

&lt;p&gt;A focused first version of the portal could be deliberately small. Let a customer submit one request type. Let the team review it and change its status. Show the change back to the customer and send one notification. Put access rules and data ownership around that path. Then invite a real customer to use it.&lt;/p&gt;

&lt;p&gt;That sequence gives a team something more useful than a large unfinished feature list: a working transaction to observe. The first conversations will reveal whether the request form asks the right questions, whether the team needs another status, and which update customers value. Semitexa's structure helps the team change the application without starting a second architecture each time the product learns.&lt;/p&gt;

&lt;p&gt;The fastest launch is rarely the one with the most features. It is the one that reaches a trustworthy customer outcome with the least avoidable construction along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed matters after the first release, too
&lt;/h2&gt;

&lt;p&gt;Going live early helps only if the team can keep improving the product. Semitexa includes project-aware inspection, dependency impact queries and focused verification commands. Those tools help developers understand what a change touches and check it before release. They do not replace testing or product judgment; they shorten the distance from a customer observation to a reviewable change.&lt;/p&gt;

&lt;p&gt;This is why Semitexa is a strong route to market for customer portals, SaaS workflows and operational software. The product's first useful path can be assembled from existing application capabilities, while the team keeps ownership of the business rules that make the venture worth building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the first customer, not a feature inventory
&lt;/h2&gt;

&lt;p&gt;If you are planning a product, describe the first person who will use it and the one job they must be able to finish. Add what happens immediately afterward: who reviews it, what data belongs to whom, and what message needs to be sent. That is enough to shape a useful first release and identify which Semitexa capabilities belong in it.&lt;/p&gt;

&lt;p&gt;Bring us that workflow. We can turn it into a concrete launch scope, show the technical path and make the next decision about your business easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  What must your first customer be able to do?
&lt;/h2&gt;

&lt;p&gt;Tell us about the workflow, the people involved and the launch goal. We will help you find the shortest credible path to a working product with Semitexa.&lt;/p&gt;

&lt;p&gt;&lt;a href="mailto:support@semitexa.com?subject=Build%20my%20product%20with%20Semitexa" class="crayons-btn crayons-btn--primary"&gt;Discuss your project&lt;/a&gt;
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://framework.semitexa.com/demo/get-started/installation" rel="noopener noreferrer"&gt;Explore the Framework Demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was first published on the &lt;a href="https://semitexa.com/blog/from-idea-to-first-customer" rel="noopener noreferrer"&gt;Semitexa blog&lt;/a&gt;, where the live demos run next to the text. Semitexa is on &lt;a href="https://github.com/semitexa" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>startup</category>
      <category>saas</category>
      <category>productivity</category>
      <category>php</category>
    </item>
    <item>
      <title>The Tenant Must Travel with the Work</title>
      <dc:creator>Taras Hanych</dc:creator>
      <pubDate>Mon, 28 Sep 2026 10:39:53 +0000</pubDate>
      <link>https://dev.to/semitexa/the-tenant-must-travel-with-the-work-jne</link>
      <guid>https://dev.to/semitexa/the-tenant-must-travel-with-the-work-jne</guid>
      <description>&lt;p&gt;&lt;em&gt;The browser knows which customer is working. The queue knows that something needs doing. A reliable SaaS application has to preserve the connection between those two facts.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The request ends before the responsibility does
&lt;/h2&gt;

&lt;p&gt;Imagine a customer uploading a product image. The application accepts it, schedules a thumbnail, and returns a success response. A worker will finish the expensive part later. To the customer, this is one action: “prepare my image.” To the software, it is already two executions.&lt;/p&gt;

&lt;p&gt;The second execution may start after the browser tab has closed. It may run in another process. The hostname that identified the customer is gone, along with the request that carried it. Yet the thumbnail still belongs to the same organization, under the same business rules.&lt;/p&gt;

&lt;p&gt;This is an illustrative scenario, but the architectural boundary is real. Moving work into a queue also moves responsibility for its context. A job can arrive intact and still arrive without enough information to run correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  A perfectly delivered message can be incomplete
&lt;/h2&gt;

&lt;p&gt;A message containing an asset ID and a transformation name tells a worker what to process. Depending on the application, it may leave other questions unanswered: which organization’s configuration should apply, which locale should an accompanying notification use, and which environment does this work belong to?&lt;/p&gt;

&lt;p&gt;Those answers might be recoverable from the asset. That can be a valid design. The fragile version is the one where each downstream service guesses differently, or reads whatever “current tenant” happens to be left in memory. Local testing with one customer rarely challenges that assumption. A shared worker serving many customers will.&lt;/p&gt;

&lt;p&gt;Adding a tenant field to a database table addresses one part of the design. Preserving the execution context across a queue is another. Semitexa makes that handoff explicit with a shared tenant context and a serializer at the message boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the context part of the handoff
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;TenantAwareJobSerializer::wrap()&lt;/code&gt; reads the current context from &lt;code&gt;TenantContextStore&lt;/code&gt;. For a concrete, non-default tenant, it adds an envelope under &lt;code&gt;_tenant&lt;/code&gt;. The receiving side can extract that envelope and rebuild the context before application work begins.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Resolve:&lt;/strong&gt; establish the tenant for the incoming execution.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Attach:&lt;/strong&gt; serialize the supported context into the message.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Restore:&lt;/strong&gt; recover that context at the worker boundary.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Execute:&lt;/strong&gt; run the operation with an explicit tenant context available.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Clear:&lt;/strong&gt; end the context’s lifetime when the execution finishes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The queue separates two executions. The envelope connects their tenant context; the execution lifecycle owns cleanup.&lt;/p&gt;

&lt;p&gt;An illustrative wrapped payload looks like this. The application fields are examples; &lt;code&gt;_tenant&lt;/code&gt; and its fields follow the serializer’s current format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"assetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"variantKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"thumbnail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_tenant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tenantId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acme"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"subdomain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"locale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prod"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation serializes the organization ID and resolution strategy, plus locale and environment when those layers are explicitly present. It does not serialize every possible context layer. A custom layer needs an intentional propagation design of its own.&lt;/p&gt;

&lt;p&gt;There is also a deliberate default case: when no context exists, or the organization is the default tenant, wrapping leaves the payload unchanged. Consumers therefore need a policy for messages without &lt;code&gt;_tenant&lt;/code&gt;. A task that requires an organization should reject missing tenant identity at its boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  This already has a job in Semitexa
&lt;/h2&gt;

&lt;p&gt;The media package provides a concrete example. &lt;code&gt;MediaQueueDispatcher&lt;/code&gt; wraps a serialized transformation message before sending it. On the receiving side, &lt;code&gt;MediaWorker::processPayload()&lt;/code&gt; calls &lt;code&gt;unwrapAndRestore()&lt;/code&gt; before reconstructing the media message and processing it.&lt;/p&gt;

&lt;p&gt;Unwrapping removes the envelope from the application payload. Restoring places the recovered context into the store’s CLI fallback. The transformation message keeps its own domain shape, while services that consult the context store have a tenant available during execution.&lt;/p&gt;

&lt;p&gt;The media transformation path also passes the asset’s tenant ID explicitly when resolving collection policy and generating a variant. That detail matters: the queued context and the persisted asset identity play distinct roles. Application code still has to enforce the relationship between them wherever that relationship controls access or storage.&lt;/p&gt;

&lt;p&gt;This is the concrete problem Semitexa addresses: the HTTP request can disappear without making an integrated background operation reconstruct tenant context from ambient process state. The producer and consumer share one envelope format and one restoration mechanism. Custom queue paths must wire those mechanisms into their own execution boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worker must also know when to forget
&lt;/h2&gt;

&lt;p&gt;Now imagine the same worker processes a second message. It has no tenant envelope. If the first job’s context remains in memory, the second job may observe the first customer’s identity. Correct propagation into one job has become incorrect propagation into the next.&lt;/p&gt;

&lt;p&gt;This is where tenancy meets the &lt;a href="https://semitexa.com/blog/long-running-php-semitexa" rel="noopener noreferrer"&gt;long-running PHP lifecycle&lt;/a&gt;. In a Swoole coroutine, Semitexa’s tenant store reads coroutine-local state. Outside a coroutine, it uses a process fallback. Those storage choices support different execution environments and require a clear end to each unit of work.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TenantContextStore&lt;/code&gt; registers cleanup with &lt;code&gt;PerRequestStateRegistry&lt;/code&gt; when context is set. Its cleanup callback clears the context of the current execution: the coroutine-local slot inside a coroutine, the process fallback outside one. The surrounding runtime must actually invoke that lifecycle at the appropriate boundary.&lt;/p&gt;

&lt;p&gt;The serializer alone does not provide that boundary. In particular, &lt;code&gt;unwrapAndRestore()&lt;/code&gt; sets the fallback when an envelope exists; a message without an envelope does not make that method clear an earlier fallback. A custom worker loop must explicitly ensure cleanup, including on exceptions and early returns. A coroutine consumer must establish context in the coroutine’s own store rather than assuming the CLI fallback is visible there.&lt;/p&gt;

&lt;p&gt;Preserve the tenant across the handoff. End its lifetime after the work. Both steps belong in the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put each guarantee where it can be enforced
&lt;/h2&gt;

&lt;p&gt;Once the context travels reliably, the rest of the application has a stable input. It still needs to use that input correctly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resolution&lt;/strong&gt; determines which organization an execution refers to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization&lt;/strong&gt; determines whether the actor or job may perform the action for that organization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data isolation&lt;/strong&gt; scopes queries, cache keys, storage paths, and other resources where the application accesses them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Propagation and cleanup&lt;/strong&gt; preserve context across handoffs and prevent it from surviving into unrelated work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The envelope carries identity; it does not prove permission. Queue ingress must be trusted or validated, tenant-owned records must be checked, and tenant-sensitive operations need an explicit policy for absent context. Semitexa supplies the context mechanisms. An application earns its isolation guarantee through the boundaries that consume them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the handoff with two customers
&lt;/h2&gt;

&lt;p&gt;A useful test goes beyond proving that one message round-trips through a serializer. Process work for tenant A, then tenant B, in the same worker. Follow both with a message that has no tenant envelope. Check what each operation can observe. Repeat the sequence with a failure during A’s work, so cleanup has to survive an exception.&lt;/p&gt;

&lt;p&gt;At the serialization level, Semitexa’s tenancy package includes dedicated propagation tests. At the application level, test the effects: which configuration was selected, which records were accessed, and where the output was stored. Also check any locale or environment values the workflow promises to preserve. These are acceptance criteria for an integration, not a claim that a serializer test proves every application safe.&lt;/p&gt;

&lt;p&gt;The larger lesson is useful well beyond media processing. Exports, notifications, and scheduled work all cross moments where the original request is no longer available. Making tenant context explicit turns those moments into boundaries that can be inspected and tested. A customer’s action can then continue after the response, with its ownership still accounted for.&lt;/p&gt;




&lt;h2&gt;
  
  
  Follow the lifetime of the work.
&lt;/h2&gt;

&lt;p&gt;Explore tenant propagation in the Framework guide, then see why execution boundaries matter in a process that stays alive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://framework.semitexa.com/demo/platform/tenancy-queue" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Explore tenant propagation&lt;/a&gt;
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://semitexa.com/blog/long-running-php-semitexa" rel="noopener noreferrer"&gt;Read about long-running PHP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was first published on the &lt;a href="https://semitexa.com/blog/tenant-context-background-work" rel="noopener noreferrer"&gt;Semitexa blog&lt;/a&gt;, where the live demos run next to the text. Semitexa is on &lt;a href="https://github.com/semitexa" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>php</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>Three Layers, One Workflow: OS, Platform, and Framework</title>
      <dc:creator>Taras Hanych</dc:creator>
      <pubDate>Mon, 28 Sep 2026 10:39:30 +0000</pubDate>
      <link>https://dev.to/semitexa/three-layers-one-workflow-os-platform-and-framework-2jc3</link>
      <guid>https://dev.to/semitexa/three-layers-one-workflow-os-platform-and-framework-2jc3</guid>
      <description>&lt;p&gt;&lt;em&gt;A good business tool needs a place to work, an interface that makes the work clear, and a system that carries it out. Semitexa gives those three jobs distinct names.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three layers, three questions
&lt;/h2&gt;

&lt;p&gt;Think about a person reviewing an approval. They need to find the task, understand the details, and submit a decision. It is tempting to call all of that “the app.” Separating the responsibilities makes it easier to design the experience and to reason about the code behind it.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgk9o2e2q63g1r05qvcxb.png" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgk9o2e2q63g1r05qvcxb.png" alt="The three responsibilities of the Semitexa ecosystem" width="800" height="962"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Start with the human need, then decide which layer owns each part. The arrows show a design conversation, not a requirement that every request passes through three separate services.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  OS: where work has a home
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://os.semitexa.com/" rel="noopener noreferrer"&gt;Semitexa OS&lt;/a&gt; is the experience layer. Its job is the shape of a working day: how someone finds their projects, changes focus, and keeps context while moving between tools. A useful OS decision might be whether an approval is visible beside the project it belongs to, or how the workspace helps someone return to an unfinished task.&lt;/p&gt;

&lt;p&gt;That is a product question before it is a component question. A polished button cannot repair a workflow that leaves people lost. The OS site presents this operating surface as a product vision; the layer map describes the intended responsibility, not a claim that every possible workflow is already shipped end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform: how the work is presented
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://platform.semitexa.com/" rel="noopener noreferrer"&gt;Semitexa Platform&lt;/a&gt; is the interface layer: reusable fields, data views, product shells, and interaction patterns. If the same kind of approval appears in several products, this layer is where the visual grammar should remain consistent. The Platform showcase demonstrates server-owned rendering, components, deferred regions, and live updates.&lt;/p&gt;

&lt;p&gt;Reusability is useful only when it clarifies the task. A field should make its label, help, validation, and state legible. A data view should make scanning and acting on a row predictable. Platform supplies that common language while each product keeps its own purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framework: how the decision runs
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://framework.semitexa.com/" rel="noopener noreferrer"&gt;Semitexa Framework&lt;/a&gt; is the execution layer. In its typed PHP flow, a payload describes an incoming request, a handler makes the application decision, and a resource returns the result. Server-rendered pages, events, tenancy, and long-running work can share that explicit architecture.&lt;/p&gt;

&lt;p&gt;For an approval, application code still has to check who may act, apply the business rule, store the result, and decide whether an event should follow. A reusable interface does not make that decision for the server. The Framework site lets you inspect working routes and demos when you want to see how the underlying request and response fit together.&lt;/p&gt;

&lt;h2&gt;
  
  
  One workflow, three decisions
&lt;/h2&gt;

&lt;p&gt;Imagine an approval inbox. This is an illustrative feature, not a claim that a shared inbox is already deployed across all three sites.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Experience:&lt;/strong&gt; place the inbox where a person can find it in the context of their project, and make the next action obvious.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Interface:&lt;/strong&gt; show the request in a readable data view, present the decision with a consistent control, and explain any validation error.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Execution:&lt;/strong&gt; accept the action through a typed route, check permission and state in application logic, then return the updated result or publish follow-up work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The boundaries help when a feature changes. If people cannot find the inbox, revisit the experience. If every inbox looks different, revisit the interface. If the action can be applied twice or by the wrong person, revisit execution. The separation turns a vague “the app feels wrong” into a question a team can answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start exploring
&lt;/h2&gt;

&lt;p&gt;Begin with the layer that matches your question. Explore the &lt;a href="https://os.semitexa.com/" rel="noopener noreferrer"&gt;OS vision&lt;/a&gt; to see the working environment, the &lt;a href="https://platform.semitexa.com/" rel="noopener noreferrer"&gt;Platform showcase&lt;/a&gt; for reusable interface patterns, and the &lt;a href="https://framework.semitexa.com/demo/get-started/installation" rel="noopener noreferrer"&gt;Framework installation guide&lt;/a&gt; when you are ready to trace or build the PHP flow. These sites currently show different parts of the ecosystem; the three-layer map gives them a shared design vocabulary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Follow the question to the right layer.
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://os.semitexa.com/" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Explore OS&lt;/a&gt;
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://platform.semitexa.com/" rel="noopener noreferrer"&gt;Explore Platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://framework.semitexa.com/" rel="noopener noreferrer"&gt;Explore Framework&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was first published on the &lt;a href="https://semitexa.com/blog/three-layers-one-workflow" rel="noopener noreferrer"&gt;Semitexa blog&lt;/a&gt;, where the live demos run next to the text. Semitexa is on &lt;a href="https://github.com/semitexa" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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