<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Abstraction on PHP Boy Scout</title><link>https://phpboyscout.uk/tags/abstraction/</link><description>Recent content in Abstraction on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Sat, 05 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://phpboyscout.uk/tags/abstraction/index.xml" rel="self" type="application/rss+xml"/><item><title>Posting unifies, tokens don't</title><link>https://phpboyscout.uk/posting-unifies-tokens-dont/</link><pubDate>Sat, 05 Sep 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/posting-unifies-tokens-dont/</guid><description>&lt;img src="https://phpboyscout.uk/posting-unifies-tokens-dont/cover-posting-unifies-tokens-dont.png" alt="Featured image of post Posting unifies, tokens don't" /&gt;&lt;p&gt;There&amp;rsquo;s a moment in every multi-platform tool where the abstraction clicks into place and you feel briefly like a proper software architect. keryx posts reels to four platforms, and the whole thing sits behind an interface you could read aloud in one breath:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Publisher posts one reel to one platform.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Publisher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// Name is the platform key (e.g. &amp;#34;instagram&amp;#34;).&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// Publish uploads video with post&amp;#39;s copy and returns the live post URL/id.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Video&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PostMeta&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="nx"&gt;PostResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(&lt;a class="link" href="https://gitlab.com/phpboyscout/keryx/-/blob/d643a7a/pkg/publish/publish.go#L41-47" target="_blank" rel="noopener"
 &gt;pinned at d643a7a&lt;/a&gt;.) Instagram, YouTube, TikTok, LinkedIn: each adapter self-registers under its name, takes the same platform-neutral &lt;code&gt;PostMeta&lt;/code&gt;, maps it onto its own caption limits and upload dance, and hands back a URL. Adding a fifth platform is a directory and an &lt;code&gt;init()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s the tidy half of the story. This post is about the other half&amp;hellip; because every abstraction leaks somewhere, and this one didn&amp;rsquo;t leak where I thought it would.&lt;/p&gt;
&lt;h2 id="the-part-i-thought-would-hurt"&gt;The part I thought would hurt
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;d braced myself for posting itself to be the mess. Captions have different length limits, uploads are chunked differently, one platform wants a cover frame&amp;hellip; and none of it mattered, not architecturally. Those are exactly the differences an adapter is &lt;em&gt;for&lt;/em&gt;: each backend soaks up its platform&amp;rsquo;s quirks behind &lt;code&gt;Publish&lt;/code&gt; and the caller stays innocent. Even the properly annoying cases stay contained. TikTok won&amp;rsquo;t render a clickable link, so &lt;a class="link" href="https://gitlab.com/phpboyscout/keryx/-/blob/d643a7a/internal/publish/tiktok/tiktok.go#L329-330" target="_blank" rel="noopener"
 &gt;its adapter drops the URL from the caption&lt;/a&gt; rather than posting dead text; LinkedIn wants the link in a first comment. Per-platform behaviour, one interface, no drama.&lt;/p&gt;
&lt;p&gt;No, the leak is in the bit that happens &lt;em&gt;before&lt;/em&gt; any posting: staying logged in.&lt;/p&gt;
&lt;h2 id="then-came-the-tokens"&gt;Then came the tokens
&lt;/h2&gt;&lt;p&gt;Token refresh looked like it ought to be one algorithm: token gets old, you exchange it, you store the new one. The spec that fell out of building it says otherwise, in as many words: refresh isn&amp;rsquo;t one algorithm, it&amp;rsquo;s a per-platform &lt;em&gt;strategy&lt;/em&gt; behind a common interface. Behold the taxonomy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Instagram&lt;/strong&gt; hands you a ~60-day token you refresh &lt;em&gt;in place&lt;/em&gt;. Same credential, new lease. Tidy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;YouTube&lt;/strong&gt; gives you a durable refresh token that never rotates: refresh mints a fresh access token and there&amp;rsquo;s nothing new to store. Its refresher takes the write-back hook every other backend needs and &lt;a class="link" href="https://gitlab.com/phpboyscout/keryx/-/blob/d643a7a/internal/publish/youtube/refresh.go#L22" target="_blank" rel="noopener"
 &gt;ignores it, by name&lt;/a&gt;: &lt;code&gt;_ oauth.WriteBack&lt;/code&gt;. That underscore is the leak, wearing a suit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TikTok&lt;/strong&gt; &lt;em&gt;rotates&lt;/em&gt; the refresh token on every use. The exchange hands you a new one, and the old one is now a used match: the adapter&amp;rsquo;s own comment says the caller must persist it immediately, because losing that write means the next refresh has nothing valid to present, and re-auth is the only way back.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt; doesn&amp;rsquo;t refresh at all. Without a partnership programme I don&amp;rsquo;t qualify for, a 60-day token simply expires, full stop. The only honest thing a tool can do is what keryx does: watch the calendar and raise an alert saying &lt;em&gt;re-auth required by this date&lt;/em&gt;. An abstraction over &amp;ldquo;refresh&amp;rdquo; whose fourth implementation is &amp;ldquo;there is no refresh, set an alarm&amp;rdquo; has told you something true about the world.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One interface, four contradictory theories about what a token even &lt;em&gt;is&lt;/em&gt;. And it goes one layer deeper: the platforms also disagree about what your &lt;em&gt;app&lt;/em&gt; is allowed to be, before any token exists (YouTube wants a verification-and-audit gauntlet for public posting; TikTok caps you at posting to yourself until you&amp;rsquo;ve passed review). The interface can&amp;rsquo;t hide that; it can only be upfront about it in each backend&amp;rsquo;s setup docs.&lt;/p&gt;
&lt;h2 id="why-i-built-all-four-first"&gt;Why I built all four first
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the decision that saved this design, and I made it on purpose: I didn&amp;rsquo;t write the refresh abstraction until all four publishers existed. Get the posting working first, &lt;em&gt;then&lt;/em&gt; go looking for the abstraction, because until all four are up you&amp;rsquo;re guessing at what they&amp;rsquo;ve got in common.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s the old rule of three with the stakes cranked up. If I&amp;rsquo;d drawn the refresh interface after Instagram (refresh-in-place, so pleasant, so misleading), I&amp;rsquo;d have baked in &amp;ldquo;exchange old for new, store the result&amp;rdquo;&amp;hellip; which is wrong for YouTube (nothing to store), a disaster for TikTok (storing it isn&amp;rsquo;t optional, and it&amp;rsquo;s a &lt;em&gt;different&lt;/em&gt; token), and plain gibberish for LinkedIn (nothing to exchange). Instagram&amp;rsquo;s shape would&amp;rsquo;ve gone through life passing itself off as the general case, and every backend after it would be &lt;a class="link" href="https://phpboyscout.uk/desktop-apps-dont-paste/" &gt;bending itself around a first guess it never got a vote on&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Instead the interface got distilled out of four worked examples, and what it unifies is only what&amp;rsquo;s genuinely universal: &lt;em&gt;a backend can be asked to make its credential current, and to say when a human needs to re-auth.&lt;/em&gt; That&amp;rsquo;s the lot. That one sentence survives contact with all four platforms. Everything more specific stays down in the adapters, where the weirdness gets paid for locally.&lt;/p&gt;
&lt;p&gt;Could I have guessed all this after building Instagram? Probably. I&amp;rsquo;d also have guessed it wrong, and spent the next three backends unpicking the guess&amp;hellip; which is the pricey sort of quick. Four publishers up front, then the interface. Slower, and I&amp;rsquo;d do it again tomorrow.&lt;/p&gt;</description></item></channel></rss>