<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>PHP - Tag - Tracy Atteberry</title><link>https://tracyatteberry.com/tags/php/</link><description>PHP - Tag - Tracy Atteberry</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><managingEditor>tracy@magicbydesign.com (Tracy Atteberry)</managingEditor><webMaster>tracy@magicbydesign.com (Tracy Atteberry)</webMaster><lastBuildDate>Fri, 16 Jan 2026 00:00:00 +0000</lastBuildDate><image><url>https://tracyatteberry.com/images/feed-icon.jpg</url><title>PHP - Tag - Tracy Atteberry</title><link>https://tracyatteberry.com/tags/php/</link></image><atom:link href="https://tracyatteberry.com/tags/php/" rel="self" type="application/rss+xml"/><item><title>Enshortener</title><link>https://tracyatteberry.com/portfolio/enshortener/</link><pubDate>Fri, 16 Jan 2026 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/portfolio/enshortener/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/enshortener/screenshot.png" referrerpolicy="no-referrer">
            </div><p>Building a personal URL shortener deployable via SFTP on shared hosting.</p>
]]></description></item><item><title>Enshortener</title><link>https://tracyatteberry.com/posts/enshortener/</link><pubDate>Fri, 16 Jan 2026 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/posts/enshortener/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/enshortener/screenshot.png" referrerpolicy="no-referrer">
            </div><h1 id="building-a-url-shortener-the-new-old-fashioned-way">Building a URL shortener the new old-fashioned way</h1>
<p>I built a personal URL shortener last week. It&rsquo;s called Enshortener, and it
does exactly what you&rsquo;d expect: turns long URLs into short ones and tracks
clicks. Nothing revolutionary.</p>
<p>What made it interesting wasn&rsquo;t the feature set. It was the constraints.</p>
<h2 id="the-problem-with-modern-deployment">The problem with modern deployment</h2>
<p>My hosting situation is boring: a shared hosting account I&rsquo;ve had for years. No
Docker. No Kubernetes. No CI/CD pipeline. Just SFTP and a folder.</p>
<p>Most modern projects don&rsquo;t work in this environment. They assume you have:</p>
<ul>
<li>A container runtime</li>
<li>A build step</li>
<li>A dependency manager</li>
<li>SSH access</li>
</ul>
<p>I wanted something I could deploy by uploading files with Cyberduck. Visit the
URL, done.</p>
<h2 id="tech-stack">Tech stack</h2>
<ul>
<li><strong>PHP 8.1</strong> – Already installed everywhere</li>
<li><strong>SQLite</strong> – No database server needed</li>
<li><strong>Tailwind CSS</strong> – Compiled locally, deployed as one CSS file</li>
<li><strong>Chart.js</strong> – Loaded from CDN for analytics charts</li>
</ul>
<p>No framework. A custom 50-line router handles all the URL mapping.</p>
<h2 id="what-actually-got-built">What actually got built</h2>
<p><strong>URL shortening:</strong> Create short codes, optionally custom. Visit <code>trcy.cc/abc</code>
and get redirected to the long URL.</p>
<p><strong>Analytics:</strong> Every click records timestamp, referrer, and user agent. The
dashboard shows clicks over time, top referrers, and recent activity.</p>
<p><strong>Admin interface:</strong> Password-protected. CSRF protection on all forms.
Mobile-responsive sidebar.</p>
<p><strong>Zero-config deployment:</strong> This was the fun part.</p>
<h2 id="the-zero-configuration-design">The zero-configuration design</h2>
<p>The original setup flow was clunky:</p>
<ol>
<li>Visit <code>/admin</code></li>
<li>System generates random password, writes to <code>setup.txt</code></li>
<li>User copies password from file</li>
<li>User deletes <code>setup.txt</code></li>
<li>User logs in</li>
</ol>
<p>Too many steps. Too easy to forget deleting that password file.</p>
<p>The new flow:</p>
<ol>
<li>Copy files to server</li>
<li>Visit <code>/admin</code></li>
<li>Set your password in the browser</li>
<li>Done</li>
</ol>
<p>If the database doesn&rsquo;t exist, it creates itself. If you forget your password,
create an empty <code>reset.txt</code> via FTP and visit <code>/admin</code> again.</p>
<h2 id="interesting-bugs">Interesting bugs</h2>
<h3 id="the-csrf-token-that-wasnt">The CSRF token that wasn&rsquo;t</h3>
<p>PHP heredoc strings look like this:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-php">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nv">$html</span> <span class="o">=</span> <span class="s">&lt;&lt;&lt;</span><span class="dl">HTML</span><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">&lt;input type=&#34;hidden&#34; name=&#34;csrf&#34; value=&#34;{csrf_token()}&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="dl">HTML</span><span class="p">;</span>
</span></span></code></pre></div></div>
<p>Except that doesn&rsquo;t work. Heredoc only interpolates variables (<code>{$variable}</code>),
not function calls (<code>{function()}</code>). The output was literally the string
<code>{csrf_token()}</code>.</p>
<p>Fix: call the function before the heredoc, store in a variable.</p>
<h3 id="the-redirect-that-skipped-analytics">The redirect that skipped analytics</h3>
<p>I used HTTP 301 (permanent redirect) for short URLs. Browsers cache 301s
aggressively. After the first click, subsequent visits went straight to the
destination, bypassing my server entirely.</p>
<p>No server hit = no analytics recorded.</p>
<p>Fix: HTTP 302 (temporary redirect). Every click hits the server.</p>
<h3 id="the-route-that-returned-nothing">The route that returned nothing</h3>
<p>PHP functions return <code>null</code> by default. My route handlers rendered HTML but
didn&rsquo;t return anything:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-php">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nv">$router</span><span class="o">-&gt;</span><span class="na">get</span><span class="p">(</span><span class="s1">&#39;/dashboard&#39;</span><span class="p">,</span> <span class="k">function</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">render_dashboard</span><span class="p">();</span>  <span class="c1">// Renders HTML, returns null
</span></span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p>The router checked if the handler returned <code>null</code> to detect &ldquo;no route matched.&rdquo;
Valid routes looked like 404s.</p>
<p>Fix: route handlers return their render result.</p>
<h2 id="what-i-didnt-build">What I didn&rsquo;t build</h2>
<ul>
<li>User accounts (single admin only)</li>
<li>URL editing (delete and recreate)</li>
<li>Expiring disused URLs</li>
<li>API endpoints</li>
<li>Rate limiting</li>
<li>Database migrations</li>
</ul>
<p>These would be nice. They weren&rsquo;t necessary for personal use.</p>
<h2 id="using-ai-for-the-boring-parts">Using AI for the boring parts</h2>
<p>I used Claude Code throughout this project, somtimes with Sonnet, sometimes
with GLM. Not to design the architecture or make product decisions, but to
handle the mechanical parts.</p>
<p>Things AI did well:</p>
<ul>
<li>Translate my designs into working code</li>
<li>Write the PHPUnit tests (though you have to push it to do this)</li>
<li>Fix syntax errors and typos</li>
<li>Implement standard patterns (CSRF, password hashing)</li>
</ul>
<p>Things I kept control of:</p>
<ul>
<li>What features to build</li>
<li>How to structure the codebase</li>
<li>Which tradeoffs to accept</li>
<li>Whether the code actually made sense</li>
</ul>
<p>It&rsquo;s like pair programming with someone who types faster than you but needs
clear direction.</p>
<h2 id="files-structure">Files structure</h2>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><pre tabindex="0"><code>/
├── admin.php              # Admin routes and handlers
├── index.php              # Short URL redirect
├── router.php             # Custom mini-router (50 lines)
├── config.php             # Configuration
├── database.sqlite        # SQLite database (auto-created)
├── lib/
│   ├── db.php            # Database helper
│   ├── csrf.php          # CSRF protection
│   └── auth.php          # Authentication
├── views/
│   ├── admin_layout.php  # Sidebar, navigation
│   ├── admin_dashboard.php
│   ├── admin_urls.php
│   ├── admin_analytics.php
│   └── admin_settings.php
└── tests/                 # PHPUnit tests</code></pre></div>
<h2 id="what-i-learned">What I learned</h2>
<p><strong>Constraints spark creativity.</strong> &ldquo;Just copy and go&rdquo; forced actual
design decisions instead of reaching for defaults.</p>
<p><strong>Zero-config is hard.</strong> The deployment experience matters as much as the
features. Shaving steps from the setup flow was worth the effort.</p>
<p><strong>Simple tech works.</strong> PHP gets a bad reputation sometimes, but for this use
case—small app, shared hosting where it&rsquo;s guaranteed to already be there,
single user—it&rsquo;s actually ideal. One language, one file per feature,
predictable execution.</p>
<h2 id="try-it">Try it</h2>
<p>The source is on GitHub at <a href="https://github.com/grymoire7/enshortener" target="_blank" rel="noopener noreffer ">github.com/grymoire7/enshortener</a>. MIT licensed.</p>
<p>It&rsquo;s nothing fancy. That&rsquo;s the point.</p>
]]></description></item></channel></rss>