<?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: Sibidharan</title>
    <description>The latest articles on DEV Community by Sibidharan (@sibidharan).</description>
    <link>https://dev.to/sibidharan</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1851709%2F6604a7c8-09d8-47a7-97b7-111062469777.png</url>
      <title>DEV Community: Sibidharan</title>
      <link>https://dev.to/sibidharan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sibidharan"/>
    <language>en</language>
    <item>
      <title>MultiPlayer Tic-Tac-Toe with WebSocket in PHP</title>
      <dc:creator>Sibidharan</dc:creator>
      <pubDate>Sun, 17 May 2026 10:11:20 +0000</pubDate>
      <link>https://dev.to/sibidharan/multiplayer-tic-tac-toe-with-websocket-in-php-5gm6</link>
      <guid>https://dev.to/sibidharan/multiplayer-tic-tac-toe-with-websocket-in-php-5gm6</guid>
      <description>&lt;p&gt;I built a multiplayer tic-tac-toe demo as the capstone of a PHP framework's learn site. &lt;strong&gt;Open it in two browser tabs, join the same room ID, and you're playing yourself:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lesson &lt;a href="https://php.zeal.ninja/learn/tictactoe" rel="noopener noreferrer"&gt;https://php.zeal.ninja/learn/tictactoe&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Working Demo &lt;a href="https://php.zeal.ninja/demo/view/tictactoe/play" rel="noopener noreferrer"&gt;https://php.zeal.ninja/demo/view/tictactoe/play&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a real WebSocket game. WebSocket run on pure PHP!!!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;First two players in a room get X and O; everyone after that joins as a spectator and sees the board update live. Add &lt;code&gt;?view=1&lt;/code&gt; to force spectator mode even with a seat free. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Win detection, alternating starter on reset, running scoreboard, reconnect handling - all server-authoritative, all over one socket per client.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it's built on
&lt;/h2&gt;

&lt;p&gt;The framework is &lt;a href="https://php.zeal.ninja" rel="noopener noreferrer"&gt;&lt;strong&gt;ZealPHP&lt;/strong&gt;&lt;/a&gt; — a PHP framework I've been building on top of &lt;a href="https://openswoole.com/" rel="noopener noreferrer"&gt;OpenSwoole&lt;/a&gt;. Same PHP you already know (&lt;code&gt;session_start()&lt;/code&gt;, &lt;code&gt;$_GET(?)&lt;/code&gt;, &lt;code&gt;header()&lt;/code&gt;, the lot), but the runtime model is different: workers stay alive between requests, routes and shared state load once, and WebSockets share the same process as your HTTP routes. No boot-per-request, no Node sidecar for sockets, no Redis for in-memory state on small apps.&lt;/p&gt;

&lt;p&gt;It's PSR-15 middleware end-to-end and ships with the boring stuff in the box — CORS, ETag + 304, gzip, Range requests, rate limiting, IP allow/deny, Basic Auth. Apache &lt;code&gt;.htaccess&lt;/code&gt; reflexes mostly carry over, and unmodified WordPress runs on it via a CGI worker bridge.&lt;/p&gt;

&lt;p&gt;That runtime model is what makes a multiplayer game small enough to fit in one file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The heart of it
&lt;/h2&gt;

&lt;p&gt;OpenSwoole gives each connection a numeric &lt;code&gt;$fd&lt;/code&gt; (file descriptor). Seat assignment is just: look at the room's current &lt;code&gt;px_fd&lt;/code&gt; / &lt;code&gt;po_fd&lt;/code&gt;, claim whichever is &lt;code&gt;0&lt;/code&gt; first, otherwise spectate.&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;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/ws/tictactoe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;onOpen&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$room&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ttt_sanitize_room&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'room'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$room&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1008&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'no_room'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nv"&gt;$viewMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'view'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// ?view=1 forces spectator regardless of free seats.&lt;/span&gt;
        &lt;span class="nv"&gt;$symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'S'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$viewMode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;\ZealPHP\Store&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;'ws_tictactoe_rooms'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$room&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'px_fd'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nc"&gt;\ZealPHP\Store&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;'ws_tictactoe_rooms'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'px_fd'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'px_name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$username&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;elseif&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'po_fd'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'O'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nc"&gt;\ZealPHP\Store&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;'ws_tictactoe_rooms'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'po_fd'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'po_name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$username&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nc"&gt;\ZealPHP\Store&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;'ws_tictactoe_clients'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'room'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'symbol'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'joined'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="nf"&gt;ttt_broadcast_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$room&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;onMessage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="cm"&gt;/* validate + mutate + broadcast */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;onClose&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="cm"&gt;/* zero the seat, keep the name */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Store&lt;/code&gt; is a thin wrapper around &lt;code&gt;OpenSwoole\Table&lt;/code&gt; — a lock-free hash map in shared memory that every worker sees. Two tables: one keyed by &lt;code&gt;fd&lt;/code&gt; for who's connected, one keyed by room for the board state. No Redis, no extra process.&lt;/p&gt;

&lt;p&gt;Total server-side: &lt;strong&gt;~270 lines&lt;/strong&gt; of PHP for the game (two &lt;code&gt;Store&lt;/code&gt; schemas, four helpers, the WS handler with &lt;code&gt;move&lt;/code&gt; / &lt;code&gt;reset&lt;/code&gt; / &lt;code&gt;reset_score&lt;/code&gt; / &lt;code&gt;leave&lt;/code&gt;). Client is ~180 lines of vanilla JS. The whole thing lives in one file alongside the framework's other routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole API surface, in 15 lines
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/vendor/autoload.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;ZealPHP\App&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;superglobals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&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;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/hello/{name}'&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;$name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Hi &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; 👋"&lt;/span&gt;&lt;span class="p"&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;ws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/chat'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$frame&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"echo: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$frame&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;One process, HTTP + WebSocket. &lt;code&gt;$ php app.php&lt;/code&gt; and it's serving. Coroutines mean &lt;code&gt;$app-&amp;gt;route('/users', fn() =&amp;gt; User::all())&lt;/code&gt; blocks the coroutine, not the worker — straight-line code, no &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; ceremony.&lt;/p&gt;

&lt;p&gt;See full working demo here: &lt;a href="https://php.zeal.ninja/demo/view/tictactoe/play" rel="noopener noreferrer"&gt;https://php.zeal.ninja/demo/view/tictactoe/play&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A few more demos worth clicking
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://php.zeal.ninja/learn/notes" rel="noopener noreferrer"&gt;Personal Notes&lt;/a&gt; — open in two tabs, watch them stay in sync via WS broadcasts.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://php.zeal.ninja/learn/streaming" rel="noopener noreferrer"&gt;Streaming&lt;/a&gt; — &lt;code&gt;yield "&amp;lt;section&amp;gt;";&lt;/code&gt; flushes immediately. Generators are responses.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://php.zeal.ninja/learn/async" rel="noopener noreferrer"&gt;Coroutines&lt;/a&gt; — async PHP without &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://php.zeal.ninja/learn/ai-chat" rel="noopener noreferrer"&gt;AI Chat&lt;/a&gt; — SSE token streaming from a Python agent bridge.&lt;/li&gt;
&lt;/ul&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer create-project sibidharan/zealphp-project my-app
&lt;span class="nb"&gt;cd &lt;/span&gt;my-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; php app.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;http://localhost:8080&lt;/code&gt; and you're running the same site, locally. If you get into any issues, please raise issue in Github. The project is still in alpha! &lt;/p&gt;

&lt;p&gt;Need your feedbacks to improve it. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/sibidharan/zealphp" rel="noopener noreferrer"&gt;sibidharan/zealphp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tic-tac-toe source:&lt;/strong&gt; &lt;a href="https://github.com/sibidharan/zealphp/blob/master/route/learn.php" rel="noopener noreferrer"&gt;route/learn.php&lt;/a&gt; (search for &lt;code&gt;tictactoe&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever wished PHP felt a little more alive between requests — go play a round.&lt;/p&gt;

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