<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:blog="https://jonesrussell.github.io/blog/ns"><channel><title>Composer on Web Developer Blog</title><link>https://jonesrussell.github.io/blog/tags/composer/</link><description>Recent content in Composer on Web Developer Blog</description><image><title>Web Developer Blog</title><url>https://jonesrussell.github.io/blog/images/og-default.png</url><link>https://jonesrussell.github.io/blog/images/og-default.png</link></image><generator>Hugo -- 0.165.0</generator><language>en-us</language><lastBuildDate>Thu, 20 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jonesrussell.github.io/blog/tags/composer/feed.xml" rel="self" type="application/rss+xml"/><item><title>Splitting Anokii into composable core, identity, and operator packages</title><link>https://jonesrussell.github.io/blog/anokii-composable-packages/</link><pubDate>Thu, 20 Aug 2026 00:00:00 +0000</pubDate><guid>https://jonesrussell.github.io/blog/anokii-composable-packages/</guid><category>php</category><category>waaseyaa</category><blog:tag>php</blog:tag><blog:tag>waaseyaa</blog:tag><blog:tag>architecture</blog:tag><blog:tag>composer</blog:tag><description>How Anokii went from one monolithic repo with duplicate identity code to three composable Composer packages, published by a governed CI split instead of a manual release process.</description><content:encoded><![CDATA[<p>Ahnii!</p>
<p><a href="https://github.com/waaseyaa/anokii">Anokii</a> is a distribution built on the <a href="https://github.com/waaseyaa/framework">Waaseyaa</a> framework. It used to carry its own copy of identity handling duplicated across the root project instead of living in one place. This post covers how that got split into three composable Composer packages, core, identity, and operator, and the governed CI workflow that publishes them without a full release process.</p>
<h2 id="the-problem-duplicate-identity-no-boundary">The Problem: Duplicate Identity, No Boundary</h2>
<p>Before the split, the root Anokii distribution kept its own Identity implementation instead of depending on a single canonical one. Any change to authentication or permissions had to be made carefully in more than one place, and there was no enforced boundary stopping other code from reaching into identity internals.</p>
<p>The fix was to extract two things:</p>
<ul>
<li><strong><code>waaseyaa/anokii-core</code></strong>, shared primitives only, nothing domain-specific.</li>
<li><strong><code>waaseyaa/anokii-identity</code></strong>, one canonical identity domain: entity, service, permissions, policy, provider, migrations, and an opt-in read-only host surface.</li>
</ul>
<p>A third package, <strong><code>waaseyaa/anokii-operator</code></strong>, followed the same pattern. The root distribution now consumes all three instead of maintaining its own duplicate implementations.</p>
<h2 id="composer-path-repositories-keep-local-dev-fast">Composer Path Repositories Keep Local Dev Fast</h2>
<p>Anokii&rsquo;s root <code>composer.json</code> points at each package directory as a local path repository, symlinked, so changes inside <code>packages/core</code> or <code>packages/identity</code> are picked up immediately without a publish step:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;repositories&#34;</span>: [
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;path&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;url&#34;</span>: <span style="color:#e6db74">&#34;packages/core&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;options&#34;</span>: {
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&#34;symlink&#34;</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&#34;versions&#34;</span>: {
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">&#34;waaseyaa/anokii-core&#34;</span>: <span style="color:#e6db74">&#34;dev-main&#34;</span>
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;path&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;url&#34;</span>: <span style="color:#e6db74">&#34;packages/identity&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;options&#34;</span>: {
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&#34;symlink&#34;</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&#34;versions&#34;</span>: {
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">&#34;waaseyaa/anokii-identity&#34;</span>: <span style="color:#e6db74">&#34;dev-main&#34;</span>
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;require&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;waaseyaa/anokii-core&#34;</span>: <span style="color:#e6db74">&#34;dev-main&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;waaseyaa/anokii-identity&#34;</span>: <span style="color:#e6db74">&#34;dev-main&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>symlink</code> option means editing a file under <code>packages/identity/src</code> is the same as editing it in the standalone <code>anokii-identity</code> package, no <code>composer update</code> round trip. The monorepo stays the single source of truth: everything is authored in <code>waaseyaa/anokii</code>, not in the split repos directly.</p>
<h2 id="the-split-main-workflow">The Split-Main Workflow</h2>
<p>Getting code out of the monorepo and into standalone <code>anokii-core</code> and <code>anokii-identity</code> repositories is a <code>workflow_dispatch</code> job, not a script anyone runs by hand. It takes three inputs: the exact 40-character <code>main</code> SHA to split, a comma-separated list of package names, and a reason (an issue URL or similar).</p>
<p>Before anything is split, the workflow enforces a few gates:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Only admin/maintain/write collaborators can dispatch the split</span>
</span></span><span style="display:flex;"><span>permission<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>gh api <span style="color:#e6db74">&#34;repos/</span><span style="color:#e6db74">${</span>REPOSITORY<span style="color:#e6db74">}</span><span style="color:#e6db74">/collaborators/</span><span style="color:#e6db74">${</span>ACTOR<span style="color:#e6db74">}</span><span style="color:#e6db74">/permission&#34;</span> --jq <span style="color:#e6db74">&#39;.permission&#39;</span><span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>permission<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> in
</span></span><span style="display:flex;"><span>  admin|maintain|write<span style="color:#f92672">)</span> ;;
</span></span><span style="display:flex;"><span>  *<span style="color:#f92672">)</span> echo <span style="color:#e6db74">&#34;::error::</span><span style="color:#e6db74">${</span>ACTOR<span style="color:#e6db74">}</span><span style="color:#e6db74"> cannot dispatch split-main.&#34;</span>; exit <span style="color:#ae81ff">1</span> ;;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">esac</span>
</span></span></code></pre></div><p>It also refuses a stale or unmerged SHA, requiring the requested commit to match <code>origin/main</code> exactly, and it blocks until the Quality workflow (PHPUnit, PHPStan, php-cs-fixer, Composer audit) has reported success for that same SHA. Only then does it split:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>split_sha<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>splitsh-lite --prefix<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>LOCAL_PREFIX<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>git push split --force-with-lease<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;refs/heads/main:</span><span style="color:#e6db74">${</span>existing<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>split_sha<span style="color:#e6db74">}</span><span style="color:#e6db74">:refs/heads/main&#34;</span>
</span></span></code></pre></div><p><a href="https://github.com/splitsh/lite">splitsh-lite</a> extracts the git history for one subdirectory (<code>packages/core</code>, <code>packages/identity</code>) into its own tree, preserving history instead of squashing it. The workflow pushes that exact commit to the split repo&rsquo;s <code>main</code> branch, then verifies with <code>git ls-remote</code> that what landed matches what was pushed, and uploads a provenance JSON artifact recording the source SHA, prefix, split SHA, actor, and reason.</p>
<h2 id="development-main-only-no-releases-yet">Development Main Only, No Releases Yet</h2>
<p>The workflow can update <code>main</code> on <code>anokii-core</code> and <code>anokii-identity</code>, but it cannot create tags, GitHub releases, or Packagist releases. That&rsquo;s deliberate: the split-main job proves the extraction is clean and history-preserving without committing to a versioning scheme before one exists. Tagged releases stay a separate, manual, exact-main procedure. Anyone consuming the split repos today does so at <code>dev-main</code>, the same version constraint the root project itself uses.</p>
<h2 id="what-the-split-buys-you">What the Split Buys You</h2>
<p>Splitting this way doesn&rsquo;t just move files around, it enforces a boundary. Every returned identity entity now passes through one canonical access handler instead of whatever each duplicate implementation happened to do. The root distribution&rsquo;s test suite, PHPStan at max level, and a Composer audit all have to pass on the exact commit before that commit is eligible to split, so the packages that ship are never ahead of what was verified.</p>
<p>If you&rsquo;re running a monorepo that needs to publish more than one standalone package, the pattern is worth stealing even without Anokii&rsquo;s specific tooling: keep authoring in one place with Composer path repositories for fast local iteration, gate any split on your CI status for that exact commit, and use <code>splitsh-lite</code> to push history-preserving mirrors instead of hand-copying files into separate repos.</p>
<p>Baamaapii</p>
]]></content:encoded></item></channel></rss>