<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
    xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Laravel Tips | Punyapal Shah</title>
        <link>https://mrpunyapal.dev/tips</link>
        <atom:link href="https://mrpunyapal.dev/tips/feed.xml" rel="self" type="application/rss+xml"/>
        <description>Curated bite-sized engineering tips for Laravel developers and the PHP ecosystem by Punyapal Shah.</description>
        <language>en-US</language>
        <lastBuildDate>Sat, 12 Sep 2026 00:00:00 GMT</lastBuildDate>
        <managingEditor>contact@mrpunyapal.dev (Punyapal Shah)</managingEditor>
        <webMaster>contact@mrpunyapal.dev (Punyapal Shah)</webMaster>
        <item>
            <title><![CDATA[Use Laravel's remember_token as a Revocation Signal]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-remember-token-revocation-signal</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-remember-token-revocation-signal</guid>
            <pubDate>Sat, 12 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Use Laravel's remember_token as an invalidation signal for saved account-switching state without maintaining a dedicated token or revocation table.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use Laravel&#39;s remember_token as an invalidation signal for saved account-switching state without maintaining a dedicated token or revocation table.</p>
</blockquote>
<p>When building a multi-account switcher, the application needs a way to verify whether a previously saved account entry is still valid before switching into it.</p>
<p>Before adding another token or revocation table to your database, consider whether Laravel already maintains state that can serve as the invalidation signal you need.</p>
<p>Laravel&#39;s <code>remember_token</code> on the <code>users</code> table already rotates whenever a user logs out, resets their password, or updates their credentials. You can bind saved account entries to this existing token using an HMAC.</p>
<hr>
<h2>Verifying Saved Accounts with remember_token</h2>
<p>Store a signature derived from the user ID, current <code>remember_token</code>, and the application key, rather than storing the raw token:</p>
<pre><code class="language-php">use App\Models\User;
use Illuminate\Support\Facades\Auth;

// 1. Keep an account available for fast switching in session or local state
$account = [
    &#039;id&#039; =&gt; $user-&gt;id,

    // Tie this entry to the user&#039;s current remember_token
    &#039;hash&#039; =&gt; hash_hmac(
        &#039;sha256&#039;,
        $user-&gt;id . &#039;|&#039; . $user-&gt;remember_token,
        config()-&gt;string(&#039;app.key&#039;),
    ),
];

// 2. Later, before switching to the saved account...
$user = User::findOrFail($account[&#039;id&#039;]);

// Recreate the expected hash from the user&#039;s current remember_token
$expected = hash_hmac(
    &#039;sha256&#039;,
    $user-&gt;id . &#039;|&#039; . $user-&gt;remember_token,
    config()-&gt;string(&#039;app.key&#039;),
);

// If the remember_token changed, the saved account entry is no longer valid
if (! hash_equals($account[&#039;hash&#039;], $expected)) {
    // Invalidate or remove the saved entry instead of switching
    return redirect()-&gt;route(&#039;login&#039;)-&gt;with(&#039;status&#039;, &#039;Session expired. Please log in again.&#039;);
}

// Valid: log into the target account
Auth::login($user);</code></pre><hr>
<h2>How It Works</h2>
<ul>
<li><strong>No raw token storage</strong>: The raw <code>remember_token</code> is never stored in the account-switching entry; only the derived HMAC hash is retained.</li>
<li><strong>Tied to current user state</strong>: The derived value depends on the user&#39;s active <code>remember_token</code>. When the user logs out (<code>Auth::logout()</code>), changes their password, or cycles tokens, Laravel rotates <code>remember_token</code>.</li>
<li><strong>Automatic invalidation</strong>: Once <code>remember_token</code> changes, recomputing the expected hash produces a different value, causing <code>hash_equals()</code> to fail and rejecting the stale switcher entry.</li>
<li><strong>Timing-safe comparison</strong>: <code>hash_equals()</code> prevents timing attacks when validating the stored hash against the expected value.</li>
<li><strong>Zero extra database tables</strong>: Reuses Laravel&#39;s built-in <code>users.remember_token</code> column instead of creating and maintaining a separate revocation table.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Architecture</category>
            <category>Laravel</category>
            <category>Authentication</category>
            <category>Security</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Git Submodule vs. Git Subtree: Which Should You Use?]]></title>
            <link>https://mrpunyapal.dev/tips/git-submodule-vs-git-subtree</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/git-submodule-vs-git-subtree</guid>
            <pubDate>Tue, 01 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A practical comparison of Git Submodules and Git Subtrees to help you choose the right strategy for embedding one repository inside another.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>A practical comparison of Git Submodules and Git Subtrees to help you choose the right strategy for embedding one repository inside another.</p>
</blockquote>
<p>When managing projects that share common libraries, documentation sets, microservices, or reusable UI components, you frequently need to embed one Git repository inside another.</p>
<p>Git provides two distinct mechanisms to handle this:</p>
<ul>
<li><strong>Git Submodules</strong>: Keep the external repository strictly independent; the parent repository only stores a pointer to a specific commit SHA.</li>
<li><strong>Git Subtrees</strong>: Merge the external repository files and commit history directly into a subdirectory of the parent repository.</li>
</ul>
<hr>
<h2>1. Git Submodules: Independent Pointer</h2>
<p>With submodules, the external project remains an entirely separate repository. The parent repository does not store the files directly; it only tracks the submodule URL and a commit hash in <code>.gitmodules</code>.</p>
<pre><code class="language-text">Main Repository
├── app/
├── config/
└── packages/shared-lib/  → [Pointer to Commit abc1234 in external repo]</code></pre><h3>Adding a Submodule</h3>
<pre><code class="language-bash"># Add external repo into packages/shared-lib
git submodule add https://github.com/example/shared-lib.git packages/shared-lib

# Stage and commit the .gitmodules and submodule reference
git commit -m &quot;Add shared-lib submodule&quot;</code></pre><h3>Cloning a Repository with Submodules</h3>
<p>When team members clone a repository containing submodules, standard <code>git clone</code> leaves submodule directories empty. They must pass the <code>--recurse-submodules</code> flag:</p>
<pre><code class="language-bash"># Clone parent and initialize all nested submodules
git clone --recurse-submodules https://github.com/example/main-app.git

# Or initialize submodules in an existing clone
git submodule update --init --recursive</code></pre><h3>Updating a Submodule</h3>
<p>To pull the latest changes from the submodule remote branch and update the parent commit pointer:</p>
<pre><code class="language-bash">git submodule update --remote packages/shared-lib
git add packages/shared-lib
git commit -m &quot;Update shared-lib to latest commit&quot;</code></pre><hr>
<h2>2. Git Subtrees: In-Tree Merged History</h2>
<p>With subtrees, the external repository&#39;s files are committed directly into your repository. Collaborators can clone, pull, and edit files normally without needing extra Git flags or submodule commands.</p>
<pre><code class="language-text">Main Repository
├── app/
├── config/
└── packages/shared-lib/  → [Real files + merged commit history]</code></pre><h3>Adding a Subtree</h3>
<pre><code class="language-bash"># 1. Add the external repository as a remote
git remote add shared-lib https://github.com/example/shared-lib.git

# 2. Add the subtree into packages/shared-lib using --squash
git subtree add --prefix=packages/shared-lib shared-lib main --squash</code></pre><h3>Pulling Upstream Updates</h3>
<p>When the external repository publishes new commits, pull them into the parent project:</p>
<pre><code class="language-bash">git subtree pull --prefix=packages/shared-lib shared-lib main --squash</code></pre><h3>Pushing Local Changes Upstream</h3>
<p>If you make modifications inside <code>packages/shared-lib</code> within the parent repo and want to push those commits back to the original repository:</p>
<pre><code class="language-bash">git subtree push --prefix=packages/shared-lib shared-lib main</code></pre><hr>
<h2>3. Side-by-Side Comparison</h2>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Git Submodule</th>
<th>Git Subtree</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Storage in Parent Repo</strong></td>
<td>Only commit SHA reference (<code>.gitmodules</code>)</td>
<td>Full file contents and history</td>
</tr>
<tr>
<td><strong>Cloning Experience</strong></td>
<td>Requires <code>--recurse-submodules</code></td>
<td>Normal <code>git clone</code> (no extra flags)</td>
</tr>
<tr>
<td><strong>Developer Complexity</strong></td>
<td>Higher (requires submodule workflow knowledge)</td>
<td>Lower (files behave like normal code)</td>
</tr>
<tr>
<td><strong>History Isolation</strong></td>
<td>Completely separate Git trees</td>
<td>Merged into the parent repository tree</td>
</tr>
<tr>
<td><strong>Locking Exact Versions</strong></td>
<td>Strict (pinned to exact commit hash)</td>
<td>Flexible (updated on demand via pull)</td>
</tr>
<tr>
<td><strong>Pushing Changes Back</strong></td>
<td>Direct commits inside submodule directory</td>
<td>Requires <code>git subtree push</code> command</td>
</tr>
<tr>
<td><strong>Third-Party Dependencies</strong></td>
<td>Ideal for external code you rarely modify</td>
<td>Ideal for shared code you actively edit</td>
</tr>
</tbody></table>
<hr>
<h2>4. Which One Should You Choose?</h2>
<h3>Choose Git Submodules if:</h3>
<ul>
<li>The embedded repository has its own strict release cycle and version tags.</li>
<li>You want the parent repository to lock onto an exact commit hash.</li>
<li>Team members and CI pipelines are comfortable initializing submodules (<code>--recurse-submodules</code>).</li>
<li>The embedded project is large and you want to keep repository clone sizes small.</li>
</ul>
<h3>Choose Git Subtrees if:</h3>
<ul>
<li>You want a friction-free clone experience for team members and contributors.</li>
<li>You want the external code to behave like ordinary files inside the repository.</li>
<li>You frequently edit the embedded files directly alongside your application code.</li>
<li>You want to avoid detached HEAD states and submodule initialization errors in CI.</li>
</ul>
<hr>
<h2>5. Common Gotchas</h2>
<h3>Submodule Gotchas:</h3>
<ul>
<li><strong>Empty Folders on Clone</strong>: Forgetting <code>--recurse-submodules</code> leaves submodule folders blank.</li>
<li><strong>Uncommitted Pointer Updates</strong>: Making changes inside the submodule folder without committing the updated reference in the parent repository causes inconsistent states between team members.</li>
<li><strong>Detached HEAD</strong>: By default, submodules check out detached commits rather than tracking branches.</li>
</ul>
<h3>Subtree Gotchas:</h3>
<ul>
<li><strong>Longer Commands</strong>: Subtree syntax (<code>--prefix</code>, <code>--squash</code>, remote names) requires precise terminal commands.</li>
<li><strong>Repo Bloat</strong>: Importing large repositories with long commit histories increases the size of your parent repository.</li>
</ul>
<hr>
<h2>Summary</h2>
<ul>
<li>Use <strong>Git Submodules</strong> when you need strict commit pinning and want the dependency to remain an isolated external project.</li>
<li>Use <strong>Git Subtrees</strong> when you want shared code to live directly in your repository with direct cloning for collaborators.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Git</category>
            <category>Workflow</category>
            <category>Git</category>
            <category>Version Control</category>
            <category>Workflow</category>
            <category>DevOps</category>
            <category>Monorepo</category>
        </item>
        <item>
            <title><![CDATA[Cache Playwright and Puppeteer Browsers in GitHub Actions]]></title>
            <link>https://mrpunyapal.dev/tips/github-actions-cache-playwright-puppeteer</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/github-actions-cache-playwright-puppeteer</guid>
            <pubDate>Mon, 31 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Cache browser binaries with actions/cache to avoid downloading 150MB+ on every CI run.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Cache browser binaries with <code>actions/cache</code> to avoid downloading 150MB+ on every CI run.</p>
</blockquote>
<p>Playwright and Puppeteer download full browser binaries (Chromium, Firefox, WebKit) during installation. Without caching, every workflow run downloads these binaries from scratch, adding 30-90 seconds of network time depending on the browser set and runner region.</p>
<h2>Cache Playwright Browsers</h2>
<pre><code class="language-yaml">jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - run: npm ci

      - name: Cache Playwright browsers
        id: playwright-cache
        uses: actions/cache@v4
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ runner.os }}-${{ hashFiles(&#039;package-lock.json&#039;) }}

      - name: Install Playwright Chromium
        if: steps.playwright-cache.outputs.cache-hit != &#039;true&#039;
        run: npx playwright install chromium

      - name: Install Playwright system deps
        run: npx playwright install-deps chromium

      - name: Run browser tasks
        run: node scripts/generate-screenshots.js</code></pre><h3>How This Works</h3>
<ol>
<li><code>actions/cache</code> checks for a cached copy of <code>~/.cache/ms-playwright</code> matching the lock file hash.</li>
<li>On a cache hit, browser binaries are restored in ~5 seconds instead of downloaded in ~60 seconds.</li>
<li>The <code>install chromium</code> step is skipped entirely on cache hits.</li>
<li>System dependencies (<code>install-deps</code>) must run every time because they install OS-level packages (<code>libnss3</code>, <code>libatk1.0</code>, etc.) that are not persisted across runner instances.</li>
</ol>
<hr>
<h2>Cache Puppeteer Browsers</h2>
<p>Puppeteer stores browsers in a different default location. The pattern is the same with a different cache path:</p>
<pre><code class="language-yaml">      - name: Cache Puppeteer browsers
        id: puppeteer-cache
        uses: actions/cache@v4
        with:
          path: ~/.cache/puppeteer
          key: puppeteer-${{ runner.os }}-${{ hashFiles(&#039;package-lock.json&#039;) }}

      - name: Install Puppeteer browser
        if: steps.puppeteer-cache.outputs.cache-hit != &#039;true&#039;
        run: npx puppeteer browsers install chrome</code></pre><hr>
<h2>Cache Key Strategy</h2>
<p>The cache key <code>playwright-${{ runner.os }}-${{ hashFiles(&#39;package-lock.json&#39;) }}</code> ensures:</p>
<ul>
<li><strong>OS specificity</strong>: Browser binaries are platform-specific. A Linux-cached browser will not work on macOS or Windows runners.</li>
<li><strong>Version tracking</strong>: When <code>playwright</code> or <code>puppeteer</code> is upgraded in <code>package-lock.json</code>, the hash changes, and a fresh download is triggered with the correct browser version.</li>
</ul>
<p>For monorepos or workspaces with multiple lock files, point <code>hashFiles</code> to the relevant lock file:</p>
<pre><code class="language-yaml">key: playwright-${{ runner.os }}-${{ hashFiles(&#039;apps/web/package-lock.json&#039;) }}</code></pre><hr>
<h2>Install Only What You Need</h2>
<p>Both Playwright and Puppeteer support installing individual browsers. If your workflow only needs Chromium, skip the rest:</p>
<pre><code class="language-bash"># Playwright: install only Chromium (skip Firefox, WebKit)
npx playwright install chromium

# Puppeteer: install only Chrome
npx puppeteer browsers install chrome</code></pre><p>Installing all browsers downloads 400MB+ and triples the cache size. Install only what your workflow actually uses.</p>
<hr>
<h2>Combining Cache with Conditional Steps</h2>
<p>If some workflow runs do not need a browser at all, combine caching with conditional execution to skip both the cache restore and the install step:</p>
<pre><code class="language-yaml">      - name: Detect if screenshots needed
        id: changes
        run: |
          if git diff --name-only HEAD~1 -- &#039;src/content/**&#039; | grep -q .; then
            echo &quot;needs_browser=true&quot; &gt;&gt; &quot;$GITHUB_OUTPUT&quot;
          fi

      - name: Cache Playwright browsers
        if: steps.changes.outputs.needs_browser == &#039;true&#039;
        id: playwright-cache
        uses: actions/cache@v4
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ runner.os }}-${{ hashFiles(&#039;package-lock.json&#039;) }}

      - name: Install Playwright Chromium
        if: steps.changes.outputs.needs_browser == &#039;true&#039; &amp;&amp; steps.playwright-cache.outputs.cache-hit != &#039;true&#039;
        run: npx playwright install chromium</code></pre><p>This avoids restoring a 150MB cache on runs that will never use a browser.</p>
<hr>
<h2>Key Points</h2>
<ul>
<li>Browser binaries are often the largest single download in Node.js CI workflows.</li>
<li>Cache <code>~/.cache/ms-playwright</code> (Playwright) or <code>~/.cache/puppeteer</code> (Puppeteer) using <code>actions/cache</code>.</li>
<li>Include <code>package-lock.json</code> hash in the cache key so version upgrades trigger a fresh download.</li>
<li>System dependencies (<code>install-deps</code>) must run on every workflow run, even on cache hits.</li>
<li>Install only the specific browsers your workflow uses to minimize download and cache size.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Git</category>
            <category>GitHub Actions</category>
            <category>Git</category>
            <category>GitHub Actions</category>
            <category>CI/CD</category>
            <category>Playwright</category>
            <category>Puppeteer</category>
        </item>
        <item>
            <title><![CDATA[Write Files Incrementally in Build Scripts to Avoid Unnecessary Rebuilds]]></title>
            <link>https://mrpunyapal.dev/tips/github-actions-incremental-file-writing</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/github-actions-incremental-file-writing</guid>
            <pubDate>Mon, 31 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Compare generated content against existing files before writing. This prevents unnecessary git diffs, cache invalidation, and downstream rebuilds.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Compare generated content against existing files before writing. This prevents unnecessary git diffs, cache invalidation, and downstream rebuilds.</p>
</blockquote>
<p>Build scripts that generate HTML, JSON, XML, or other output files often write the result unconditionally, even when the content has not changed. This creates problems in CI pipelines: unchanged files get new timestamps, <code>git status</code> reports false modifications, auto-commit steps push empty diffs, and downstream caches are invalidated for no reason.</p>
<h2>The Problem</h2>
<p>A typical build script writes output directly:</p>
<pre><code class="language-javascript">import fs from &#039;node:fs&#039;;

const html = generatePage(data);
fs.writeFileSync(&#039;dist/index.html&#039;, html);</code></pre><p>If <code>generatePage</code> produces the same HTML as the file already on disk, the write is wasted. Worse, it updates the file&#39;s modification timestamp, which can trigger:</p>
<ul>
<li><code>git diff</code> reporting the file as changed (if line endings or encoding differ)</li>
<li>Downstream tools rebuilding dependents of that file</li>
<li>CI auto-commit steps creating empty or noise-only commits</li>
<li>CDN or deployment cache invalidation for files that did not actually change</li>
</ul>
<hr>
<h2>The Fix: Compare Before Writing</h2>
<p>Read the existing file, compare it with the new content, and skip the write when they match:</p>
<pre><code class="language-javascript">import fs from &#039;node:fs&#039;;
import path from &#039;node:path&#039;;

function writeIfChanged(filePath, content) {
  try {
    const existing = fs.readFileSync(filePath, &#039;utf-8&#039;);
    if (existing === content) return false;
  } catch {
    // File does not exist yet, proceed with write
  }

  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, content);
  return true;
}</code></pre><p>Usage in a build script:</p>
<pre><code class="language-javascript">const pages = generateAllPages(data);
let written = 0;

for (const [filePath, html] of Object.entries(pages)) {
  if (writeIfChanged(filePath, html)) {
    written++;
  }
}

console.log(`Wrote ${written}/${Object.keys(pages).length} files (rest unchanged)`);</code></pre><p>On a typical run where 3 out of 300 pages changed, you will see:</p>
<pre><code class="language-text">Wrote 3/300 files (rest unchanged)</code></pre><hr>
<h2>Apply to Every Generated Output</h2>
<p>This pattern works for any generated file type:</p>
<pre><code class="language-javascript">// HTML pages
writeIfChanged(&#039;dist/tips.html&#039;, renderTipsPage(tips));

// JSON search indexes
writeIfChanged(&#039;public/search-index.json&#039;, JSON.stringify(index));

// XML feeds and sitemaps
writeIfChanged(&#039;public/feed.xml&#039;, renderRssFeed(posts));
writeIfChanged(&#039;public/sitemap.xml&#039;, renderSitemap(routes));</code></pre><hr>
<h2>CI Auto-Commit: Only Commit When Files Actually Changed</h2>
<p>When your workflow auto-commits generated files, the incremental write pattern prevents empty commits:</p>
<pre><code class="language-yaml">- name: Generate static assets
  run: node scripts/build.js

- name: Commit generated files if changed
  run: |
    git add -A
    if git diff --cached --quiet; then
      echo &quot;No changes to commit&quot;
    else
      git commit -m &quot;chore: regenerate static assets&quot;
      git push
    fi</code></pre><p>Without incremental writes, <code>git diff --cached</code> would always detect changes because every file received a fresh write (and potentially a new timestamp or trailing whitespace difference), leading to commits that contain no meaningful content changes.</p>
<hr>
<h2>Key Points</h2>
<ul>
<li>Always compare new content with existing files before writing in build scripts.</li>
<li>Skipping unchanged writes prevents false <code>git diff</code> results, unnecessary cache invalidation, and empty auto-commits.</li>
<li>The <code>try/catch</code> pattern handles missing files gracefully on first builds.</li>
<li>This optimization compounds in projects with hundreds of generated files, where only a few change per commit.</li>
<li>Pair with <code>git diff --cached --quiet</code> in CI to avoid committing when nothing actually changed.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Git</category>
            <category>GitHub Actions</category>
            <category>Git</category>
            <category>GitHub Actions</category>
            <category>CI/CD</category>
            <category>Build Tools</category>
        </item>
        <item>
            <title><![CDATA[Skip Expensive CI Steps Using git diff Instead of File Timestamps]]></title>
            <link>https://mrpunyapal.dev/tips/github-actions-skip-steps-with-git-diff</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/github-actions-skip-steps-with-git-diff</guid>
            <pubDate>Mon, 31 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[File modification timestamps are unreliable in GitHub Actions. Use git diff to detect actual changes and conditionally skip expensive workflow steps.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>File modification timestamps are unreliable in GitHub Actions. Use <code>git diff</code> to detect actual changes and conditionally skip expensive workflow steps.</p>
</blockquote>
<p>Many build tools use file modification timestamps (<code>mtime</code>) to decide what needs rebuilding. This works on local machines because the filesystem tracks when each file was last saved.</p>
<p>In GitHub Actions, <code>actions/checkout</code> resets <strong>every file&#39;s timestamp</strong> to the moment of checkout. A project with 500 files will have all 500 stamped with the exact same time, regardless of when each was actually last modified.</p>
<p>Any script comparing timestamps to decide &quot;has this source changed since the last build?&quot; will answer &quot;yes&quot; for everything, triggering full rebuilds on every single run.</p>
<h2>The Problem in Practice</h2>
<p>Consider a build script that skips PDF generation when the output is newer than the source:</p>
<pre><code class="language-javascript">import fs from &#039;node:fs&#039;;

const srcStat = fs.statSync(&#039;template.html&#039;);
const outStat = fs.statSync(&#039;output.pdf&#039;);

if (outStat.mtimeMs &gt; srcStat.mtimeMs) {
  console.log(&#039;PDF is up to date, skipping&#039;);
  process.exit(0);
}

// ... expensive PDF generation with Puppeteer</code></pre><p>Locally, this works perfectly. In CI, both files receive the checkout timestamp, so <code>outStat.mtimeMs</code> equals <code>srcStat.mtimeMs</code>, and the skip condition fails every time.</p>
<hr>
<h2>The Fix: Use git diff</h2>
<p>Git tracks actual content changes regardless of filesystem timestamps. Replace <code>mtime</code> checks with <code>git diff</code>:</p>
<pre><code class="language-yaml"># .github/workflows/build.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2  # Need parent commit for diff

      - name: Detect changed files
        id: changes
        run: |
          if git diff --name-only HEAD~1 -- &#039;src/resume/**&#039; &#039;templates/resume.html&#039; | grep -q .; then
            echo &quot;resume_changed=true&quot; &gt;&gt; &quot;$GITHUB_OUTPUT&quot;
          fi
          if git diff --name-only HEAD~1 -- &#039;src/content/**&#039; | grep -q .; then
            echo &quot;content_changed=true&quot; &gt;&gt; &quot;$GITHUB_OUTPUT&quot;
          fi

      - name: Generate PDF Resume
        if: steps.changes.outputs.resume_changed == &#039;true&#039;
        run: node scripts/generate-pdf.js

      - name: Generate Screenshots
        if: steps.changes.outputs.content_changed == &#039;true&#039;
        run: node scripts/generate-screenshots.js</code></pre><h3>Why fetch-depth: 2?</h3>
<p>By default, <code>actions/checkout</code> performs a shallow clone with <code>fetch-depth: 1</code>, which only contains the latest commit. <code>git diff HEAD~1</code> requires the parent commit to exist. Setting <code>fetch-depth: 2</code> fetches exactly enough history for a single-commit diff.</p>
<hr>
<h2>Handling repository_dispatch and workflow_dispatch</h2>
<p>When workflows are triggered by <code>repository_dispatch</code> or <code>workflow_dispatch</code>, there is no new commit to diff against. Handle these triggers explicitly:</p>
<pre><code class="language-yaml">- name: Detect changed files
  id: changes
  run: |
    EVENT=&quot;${{ github.event_name }}&quot;

    # Dispatch events have no associated commit diff
    if [[ &quot;$EVENT&quot; == &quot;repository_dispatch&quot; || &quot;$EVENT&quot; == &quot;workflow_dispatch&quot; ]]; then
      echo &quot;content_changed=true&quot; &gt;&gt; &quot;$GITHUB_OUTPUT&quot;
      exit 0
    fi

    if git diff --name-only HEAD~1 -- &#039;src/content/**&#039; | grep -q .; then
      echo &quot;content_changed=true&quot; &gt;&gt; &quot;$GITHUB_OUTPUT&quot;
    fi</code></pre><p>For <code>repository_dispatch</code>, you often know which subsystem triggered the event based on the <code>event_type</code>. Use that to set only the relevant flags instead of enabling everything.</p>
<hr>
<h2>The Same Pattern Inside Build Scripts</h2>
<p>You can also move the <code>git diff</code> check directly into your build scripts:</p>
<pre><code class="language-javascript">import { execSync } from &#039;node:child_process&#039;;

function hasSourceChanged(paths) {
  try {
    const diff = execSync(
      `git diff --name-only HEAD~1 -- ${paths.join(&#039; &#039;)}`,
      { encoding: &#039;utf-8&#039; }
    ).trim();
    return diff.length &gt; 0;
  } catch {
    // If git diff fails (shallow clone, initial commit), assume changed
    return true;
  }
}

if (!hasSourceChanged([&#039;template.html&#039;, &#039;styles/resume.css&#039;])) {
  console.log(&#039;No source changes detected, skipping generation&#039;);
  process.exit(0);
}

// ... proceed with expensive work</code></pre><p>The <code>try/catch</code> fallback is important. On initial commits, force pushes, or misconfigured shallow clones, <code>git diff HEAD~1</code> will fail. Defaulting to &quot;changed&quot; ensures the build still runs when change detection is unavailable.</p>
<hr>
<h2>Key Points</h2>
<ul>
<li><code>actions/checkout</code> sets all file timestamps to the checkout time, making <code>mtime</code>-based incremental logic unreliable in CI.</li>
<li>Use <code>git diff --name-only HEAD~1</code> to detect files that actually changed in the latest commit.</li>
<li>Set <code>fetch-depth: 2</code> in <code>actions/checkout</code> so the parent commit is available for diffing.</li>
<li>Handle <code>repository_dispatch</code> and <code>workflow_dispatch</code> triggers separately, since they have no associated commit diff.</li>
<li>Always fall back to &quot;assume changed&quot; when <code>git diff</code> fails, so builds are never silently skipped due to detection errors.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Git</category>
            <category>GitHub Actions</category>
            <category>Git</category>
            <category>GitHub Actions</category>
            <category>CI/CD</category>
            <category>Performance</category>
        </item>
        <item>
            <title><![CDATA[Mask Query Bindings in Laravel Exception Messages]]></title>
            <link>https://mrpunyapal.dev/tips/mask-query-bindings-in-exception-messages</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/mask-query-bindings-in-exception-messages</guid>
            <pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Keep query bindings out of QueryException messages while retaining access to the actual bindings.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Keep query bindings out of <code>QueryException</code> messages while retaining access to the actual bindings.</p>
</blockquote>
<p>When a database query fails, Laravel&#39;s <code>QueryException</code> message can include the values that were bound to the query.</p>
<p>That can be useful while debugging, but it can also mean sensitive values end up in logs, error trackers, or other places where exception messages are stored.</p>
<p>Laravel 13.27 adds an option to mask those bindings in exception messages.</p>
<h2>Before</h2>
<p>A failed query can produce an exception message containing the actual value:</p>
<pre><code class="language-text">SQL: select * from `users` where `email` = &#039;john@example.com&#039;</code></pre><p>That means the query&#39;s bound values become part of the exception message.</p>
<h2>Laravel 13.27</h2>
<p>Enable binding masking on your database connection:</p>
<pre><code class="language-php">// config/database.php

&#039;connections&#039; =&gt; [
    &#039;mysql&#039; =&gt; [
        // ...

        &#039;mask_bindings_in_exception_messages&#039; =&gt; env(&#039;DB_MASK_BINDINGS&#039;, false),
    ],
];</code></pre><p>Now the exception message keeps the placeholder instead of interpolating the binding:</p>
<pre><code class="language-text">SQL: select * from `users` where `email` = ?</code></pre><h2>The bindings are still available</h2>
<p>Masking the exception message does not remove the bindings from the query.</p>
<p>You can still access them separately through:</p>
<pre><code class="language-php">$exception-&gt;getBindings();</code></pre><p>This gives you a useful separation:</p>
<pre><code class="language-text">Exception message
    ↓
SQL with ? placeholders

Exception bindings
    ↓
Actual values</code></pre><p>So your error message can avoid exposing query values while the actual bindings remain available when you explicitly need them.</p>
<h2>Why this is useful</h2>
<p>This can be especially useful when exceptions are sent to:</p>
<ul>
<li>application logs</li>
<li>error tracking services</li>
<li>APM systems</li>
<li>failed job records</li>
<li>other external monitoring systems</li>
</ul>
<p>If query values can contain sensitive or personal information, keeping them out of the exception message reduces the chance of accidentally exposing them through your error reporting pipeline.</p>
<h2>Takeaway</h2>
<p>Laravel 13.27 gives you more control over what appears in <code>QueryException</code> messages.</p>
<p>If you don&#39;t need actual query bindings embedded in your exception messages, enable:</p>
<pre><code class="language-php">&#039;mask_bindings_in_exception_messages&#039; =&gt; env(&#039;DB_MASK_BINDINGS&#039;, false),</code></pre><p>You still have access to the bindings separately through <code>getBindings()</code>.</p>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Database</category>
            <category>Laravel</category>
            <category>Database</category>
            <category>Security</category>
            <category>Exceptions</category>
        </item>
        <item>
            <title><![CDATA[Ignore Local Files Without Modifying .gitignore Using .git/info/exclude]]></title>
            <link>https://mrpunyapal.dev/tips/git-local-ignore-info-exclude</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/git-local-ignore-info-exclude</guid>
            <pubDate>Sat, 22 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Use .git/info/exclude to ignore personal scratch files, local debug scripts, and notes without polluting your team's shared .gitignore file.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use .git/info/exclude to ignore personal scratch files, local debug scripts, and notes without polluting your team&#39;s shared .gitignore file.</p>
</blockquote>
<p>When working on a shared repository, you often create temporary files for local testing: a quick scratchpad (<code>scratchpad.md</code>), a temporary debugging script (<code>debug_local.php</code>), or a folder of personal investigation notes (<code>/notes/</code>).</p>
<p>A common developer habit is adding those personal patterns to the project&#39;s root <code>.gitignore</code> file. However, <code>.gitignore</code> is committed and tracked in version control. Adding personal, one-off file patterns creates noisy pull request diffs and forces team members to adopt ignore rules they do not need.</p>
<p>Git provides a built-in repository-local ignore file: <code>.git/info/exclude</code>.</p>
<h2>Three Scopes of Git Ignore Rules</h2>
<p>Understanding where to place an ignore pattern depends on who needs the rule:</p>
<pre><code class="language-text">.gitignore
  ↓
Shared project rules (Committed and tracked by the entire team)

.git/info/exclude
  ↓
Personal rules for this clone only (Uncommitted, stored inside .git/)

Global Gitignore (~/.gitignore)
  ↓
Personal rules across all repositories on your machine (e.g. .DS_Store, .vscode/)</code></pre><h2>How to Use .git/info/exclude</h2>
<p>Every Git repository includes a <code>.git/info/exclude</code> file by default. Because it lives inside the <code>.git</code> directory, it is never committed or pushed to remote repositories.</p>
<p>Open the file in your preferred editor:</p>
<pre><code class="language-bash">code .git/info/exclude</code></pre><p>Add your personal patterns using standard <code>.gitignore</code> syntax:</p>
<pre><code class="language-text"># Personal scratchpad and experimental scripts
scratchpad.md
debug_local.php
experimental_*.php

# Local private notes
/personal-notes/

# Local custom test database dump
local_dump.sql</code></pre><p>After saving the file, running <code>git status</code> will no longer report matching files under &quot;Untracked files&quot;, and running <code>git add .</code> will not accidentally stage them.</p>
<h2>Important: .git/info/exclude Only Ignores Untracked Files</h2>
<p>Like <code>.gitignore</code>, adding a pattern to <code>.git/info/exclude</code> applies strictly to <strong>untracked files</strong>.</p>
<p>If a file is already tracked in Git history, adding it to <code>.git/info/exclude</code> will not prevent Git from tracking subsequent changes to it.</p>
<p>If you need to stop tracking a file that was previously committed while keeping your local copy on disk, you must remove it from the Git index:</p>
<pre><code class="language-bash">git rm --cached path/to/file.txt</code></pre><p><em>(Note: <code>git rm --cached</code> stages a deletion in Git history for the next commit, so only use it if the file should truly be removed from the shared repository for all contributors).</em></p>
<h2>Decision Guide: Which Ignore Scope to Use</h2>
<table>
<thead>
<tr>
<th>Situation</th>
<th>Recommended Scope</th>
<th>Location</th>
</tr>
</thead>
<tbody><tr>
<td>Project dependencies, build outputs, and shared environment templates (<code>node_modules/</code>, <code>vendor/</code>, <code>dist/</code>, <code>.env.example</code>)</td>
<td>Project <code>.gitignore</code></td>
<td><code>.gitignore</code> in repository root</td>
</tr>
<tr>
<td>Personal scratch files, debug scripts, or local test fixtures specific to this clone (<code>scratchpad.md</code>, <code>debug_test.php</code>, <code>/my-notes/</code>)</td>
<td>Local repository exclude</td>
<td><code>.git/info/exclude</code></td>
</tr>
<tr>
<td>Machine-wide artifacts and OS/IDE files across all projects on your laptop (<code>.DS_Store</code>, <code>Thumbs.db</code>, <code>.idea/</code>, <code>.vscode/</code>)</td>
<td>Global Gitignore</td>
<td><code>~/.gitignore</code> (configured via <code>git config --global core.excludesFile</code>)</td>
</tr>
</tbody></table>
<h2>Summary</h2>
<ul>
<li>Use <code>.gitignore</code> for shared project ignore rules that every team member needs.</li>
<li>Use <code>.git/info/exclude</code> to ignore personal scratch files and temporary scripts in a specific repository without modifying committed files.</li>
<li>Use a global Gitignore (<code>core.excludesFile</code>) for operating system and editor artifacts across all repositories on your machine.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Git</category>
            <category>Workflow</category>
            <category>Git</category>
            <category>DevOps</category>
            <category>Workflow</category>
            <category>Tooling</category>
        </item>
        <item>
            <title><![CDATA[Stream CSV Downloads in Laravel Without Creating Temporary Files]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-stream-csv-downloads-without-temporary-files</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-stream-csv-downloads-without-temporary-files</guid>
            <pubDate>Fri, 21 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Combine response()->streamDownload(), php://output, and query chunking to stream CSV exports directly to the browser without writing temporary files to disk.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Combine response()-&gt;streamDownload(), php://output, and query chunking to stream CSV exports directly to the browser without writing temporary files to disk.</p>
</blockquote>
<p>Generating CSV exports often involves writing a temporary file to disk (<code>storage_path(&#39;app/temp.csv&#39;)</code>), returning a download response, and cleaning up the file afterward. This introduces unnecessary disk I/O and risk of disk space exhaustion.</p>
<p>Using <code>response()-&gt;streamDownload()</code>, you can write CSV rows directly to <code>php://output</code> in chunked database batches with minimal memory consumption and zero disk usage.</p>
<hr>
<h2>Invokable Controller Example</h2>
<pre><code class="language-php">namespace App\Http\Controllers;

use App\Models\Product;
use Symfony\Component\HttpFoundation\StreamedResponse;

class ExportProductsController
{
    public function __invoke(): StreamedResponse
    {
        return response()-&gt;streamDownload(function (): void {
            $output = fopen(&#039;php://output&#039;, &#039;w&#039;);

            // Header row
            fputcsv($output, [&#039;SKU&#039;, &#039;Name&#039;, &#039;Price&#039;]);

            // Stream records in batches using primary-key pagination
            Product::query()
                -&gt;select([&#039;id&#039;, &#039;sku&#039;, &#039;name&#039;, &#039;price&#039;])
                -&gt;chunkById(500, function ($products) use ($output): void {
                    foreach ($products as $product) {
                        fputcsv($output, [
                            $product-&gt;sku,
                            $product-&gt;name,
                            $product-&gt;price,
                        ]);
                    }
                });

            fclose($output);
        }, &#039;products.csv&#039;, [
            &#039;Content-Type&#039; =&gt; &#039;text/csv&#039;,
        ]);
    }
}</code></pre><hr>
<h2>Key Technical Details</h2>
<ul>
<li><strong><code>php://output</code></strong>: A write-only output stream wrapper that writes directly into PHP&#39;s output buffer sent to the HTTP client.</li>
<li><strong><code>fputcsv()</code></strong>: Handles escaping of commas, quotes, and newlines natively in PHP.</li>
<li><strong><code>chunkById()</code></strong>: Paginates by <code>WHERE id &gt; last_seen_id</code> rather than SQL <code>OFFSET</code>, preventing duplicate or skipped records if the table receives new rows during the export.</li>
<li><strong>Selective Columns</strong>: Always pair <code>chunkById()</code> with explicit <code>select([&#39;id&#39;, ...])</code> to hydrate only necessary columns instead of full model instances.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>HTTP &amp; API</category>
            <category>Laravel</category>
            <category>HTTP</category>
            <category>Streaming</category>
            <category>CSV</category>
            <category>Database</category>
        </item>
        <item>
            <title><![CDATA[Prevent Navigation Layout Shifts and Flickering in Web Applications]]></title>
            <link>https://mrpunyapal.dev/tips/css-prevent-navigation-layout-shift-flicker</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/css-prevent-navigation-layout-shift-flicker</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Fix common navigation bar layout shifts, scrollbar jumps, font width shifts, and theme switcher flickering with clean CSS and HTML patterns.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Fix common navigation bar layout shifts, scrollbar jumps, font width shifts, and theme switcher flickering with clean CSS and HTML patterns.</p>
</blockquote>
<p>Navigation bars often suffer from subtle layout shifts during page reloads, theme toggles, and tab switches. Combining a few CSS and HTML techniques eliminates these visual jumps entirely.</p>
<h3>1. Lock Root Scrollbar Space and Clip Horizontal Overflow</h3>
<p>Navigating between long pages with scrollbars and short pages without scrollbars causes horizontal layout jumps. Use <code>scrollbar-gutter: stable</code> on <code>html</code> with <code>overflow-y: auto</code>, and clip horizontal overflow on <code>body</code> with <code>overflow-x: clip</code>.</p>
<pre><code class="language-css">/* Reserve scrollbar width globally without forcing a scrollbar track on short pages */
html {
  scrollbar-gutter: stable;
  overflow-y: auto;
}

/* Clip wide horizontal overflow without creating a second scroll container */
body {
  overflow-x: clip;
}</code></pre><p><code>scrollbar-gutter: stable</code> reserves scrollbar space on short pages so layout width remains constant across navigations, while <code>overflow-y: auto</code> avoids rendering an empty scrollbar track when scrolling is not needed.</p>
<p>Use <code>overflow-x: clip</code> instead of <code>overflow-x: hidden</code> on <code>body</code>. Traditional <code>overflow-x: hidden</code> forces browsers to evaluate <code>overflow-y</code> as <code>auto</code>, creating a secondary scroll container on <code>body</code> that causes double vertical scrollbars. <code>overflow-x: clip</code> clips horizontal overflow cleanly without creating a scrolling context.</p>
<h3>2. Prevent View Transition Scrollbar Flicker</h3>
<p>When using native cross-document View Transitions (<code>@view-transition { navigation: auto; }</code>), browsers render outgoing and incoming page snapshots simultaneously. For a single frame during the transition, the viewport height expands, triggering a temporary scrollbar track.</p>
<pre><code class="language-css">/* Prevent temporary scrollbar flicker during cross-document view transitions */
::view-transition-group(root),
::view-transition-image-pair(root),
::view-transition-old(root),
::view-transition-new(root) {
  overflow: hidden !important;
}</code></pre><p>Setting <code>overflow: hidden</code> on the root view transition pseudo-elements clips snapshot layers to viewport bounds, preventing split-second scrollbar popping during page navigations.</p>
<h3>3. Lock Navigation Tab Widths with CSS Grid</h3>
<p>Active or hovered navigation tabs often expand in width when text becomes bold or changes color contrast, pushing adjacent tabs left or right. Use CSS Grid overlay with an invisible pseudo-element to reserve bold text dimensions upfront.</p>
<pre><code class="language-html">&lt;nav class=&quot;nav-tabs&quot;&gt;
  &lt;a href=&quot;/dashboard&quot; class=&quot;nav-link active&quot;&gt;
    &lt;span class=&quot;nav-label&quot; data-text=&quot;Dashboard&quot;&gt;
      &lt;span&gt;Dashboard&lt;/span&gt;
    &lt;/span&gt;
  &lt;/a&gt;
  &lt;a href=&quot;/settings&quot; class=&quot;nav-link&quot;&gt;
    &lt;span class=&quot;nav-label&quot; data-text=&quot;Settings&quot;&gt;
      &lt;span&gt;Settings&lt;/span&gt;
    &lt;/span&gt;
  &lt;/a&gt;
&lt;/nav&gt;</code></pre><pre><code class="language-css">.nav-tabs a {
  display: inline-flex;
  align-items: center;
}

/* Grid overlay sizes container to the widest content layer */
.nav-tabs .nav-label {
  display: inline-grid;
  grid-template-areas: &quot;label&quot;;
  align-items: center;
  justify-items: center;
}

.nav-tabs .nav-label::after,
.nav-tabs .nav-label &gt; span {
  grid-area: label;
}

/* Invisible bold pseudo-element reserves max text width */
.nav-tabs .nav-label::after {
  content: attr(data-text);
  font-weight: 700;
  visibility: hidden;
  overflow: hidden;
  user-select: none;
  pointer-events: none;
}</code></pre><h3>4. Pre-render Theme Toggle Icons in HTML</h3>
<p>Swapping sun and moon icons via JavaScript after page load causes visual button flickering. Pre-render both icons directly in static HTML and toggle visibility using CSS theme classes.</p>
<pre><code class="language-html">&lt;button type=&quot;button&quot; class=&quot;theme-toggle&quot; aria-label=&quot;Toggle theme&quot;&gt;
  &lt;!-- Sun icon visible in dark mode --&gt;
  &lt;svg class=&quot;hidden dark:block&quot; viewBox=&quot;0 0 24 24&quot; width=&quot;16&quot; height=&quot;16&quot;&gt;
    &lt;circle cx=&quot;12&quot; cy=&quot;12&quot; r=&quot;5&quot; fill=&quot;currentColor&quot;/&gt;
  &lt;/svg&gt;

  &lt;!-- Moon icon visible in light mode --&gt;
  &lt;svg class=&quot;block dark:hidden&quot; viewBox=&quot;0 0 24 24&quot; width=&quot;16&quot; height=&quot;16&quot;&gt;
    &lt;path d=&quot;M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z&quot; fill=&quot;currentColor&quot;/&gt;
  &lt;/svg&gt;
&lt;/button&gt;</code></pre><h3>5. Use Opacity for Text Contrast Changes</h3>
<p>Changing text color from gray to solid black or white triggers font stem-darkening in browser rendering engines, altering glyph widths by fractions of a pixel. Use constant base colors and toggle opacity instead.</p>
<pre><code class="language-css">/* Active tab */
.nav-link.active {
  color: #0f172a; /* 100% opacity */
}

/* Inactive tab uses same base color with lower opacity */
.nav-link:not(.active) {
  color: rgba(15, 23, 42, 0.6);
}</code></pre><h3>Key Takeaways</h3>
<ul>
<li>Reserve scrollbar width on <code>html</code> with <code>scrollbar-gutter: stable</code> and clip horizontal overflow on <code>body</code> with <code>overflow-x: clip</code>.</li>
<li>Clip View Transition root pseudo-elements with <code>overflow: hidden</code> to prevent split-second scrollbar popping.</li>
<li>Reserve space for bold tab labels using <code>display: inline-grid</code> and <code>content: attr(data-text)</code>.</li>
<li>Pre-render all theme toggle states in static HTML to eliminate DOM injection flicker.</li>
<li>Adjust text opacity rather than hex colors to preserve font glyph vector calculations.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>CSS</category>
            <category>Styling</category>
            <category>CSS</category>
            <category>Layout Shift</category>
            <category>Frontend</category>
            <category>Performance</category>
        </item>
        <item>
            <title><![CDATA[Trigger a GitHub Actions Workflow Across Repositories]]></title>
            <link>https://mrpunyapal.dev/tips/github-actions-cross-repo-workflow-dispatch</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/github-actions-cross-repo-workflow-dispatch</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Use repository_dispatch to trigger a workflow in a target repository when commits or PRs land in a source repository.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use <code>repository_dispatch</code> to trigger a workflow in a target repository when commits or PRs land in a source repository.</p>
</blockquote>
<p>GitHub Actions workflows are scoped to a single repository by default. To run a workflow in another repository after merging a PR or pushing a commit, use GitHub&#39;s <code>repository_dispatch</code> API endpoint.</p>
<h2>1. Configure the Target Repository Listener</h2>
<p>Add <code>repository_dispatch</code> to the <code>on</code> block in the target repository workflow file:</p>
<pre><code class="language-yaml"># .github/workflows/build.yml (target repo)
name: Build Site

on:
  push:
    branches:
      - main
  repository_dispatch:
    types: [content-updated]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo &quot;Build triggered&quot;</code></pre><h2>2. Configure the Source Repository Dispatcher</h2>
<p>In the source repository, add a workflow step that sends a POST request to GitHub&#39;s dispatches endpoint:</p>
<pre><code class="language-yaml"># .github/workflows/notify.yml (source repo)
name: Notify Target Repo

on:
  push:
    branches:
      - main

jobs:
  notify:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger target build
        env:
          TOKEN: ${{ secrets.WEBSITE_DISPATCH_TOKEN }}
        run: |
          if [ -z &quot;$TOKEN&quot; ]; then
            echo &quot;WEBSITE_DISPATCH_TOKEN is not set&quot;
            exit 1
          fi
          curl --fail --show-error -X POST \
            -H &quot;Authorization: Bearer $TOKEN&quot; \
            -H &quot;Accept: application/vnd.github.v3+json&quot; \
            https://api.github.com/repos/OWNER/TARGET-REPO/dispatches \
            -d &#039;{&quot;event_type&quot;: &quot;content-updated&quot;}&#039;</code></pre><h2>3. Create and Assign the Access Token</h2>
<ol>
<li>Create a Fine-Grained Personal Access Token under GitHub Developer Settings.</li>
<li>Select the target repository under <strong>Repository Access</strong>.</li>
<li>Set <strong>Contents</strong> permission to <strong>Read and write</strong>.</li>
<li>Save the token as <code>WEBSITE_DISPATCH_TOKEN</code> in the source repository&#39;s Actions Secrets.</li>
</ol>
<h2>Key Considerations</h2>
<ul>
<li>The <code>event_type</code> string in the payload must match the array value under <code>types: [...]</code>.</li>
<li>Always pass <code>--fail --show-error</code> to <code>curl</code> so HTTP errors exit with code 1 instead of failing silently.</li>
<li>Fine-grained tokens limit dispatch access strictly to the targeted repository.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Git</category>
            <category>GitHub Actions</category>
            <category>Git</category>
            <category>GitHub Actions</category>
            <category>CI/CD</category>
            <category>DevOps</category>
        </item>
        <item>
            <title><![CDATA[Reusable Migration Fields with Custom Blueprint Macros]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-blueprint-macro-reusable-schema-audit-fields</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-blueprint-macro-reusable-schema-audit-fields</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Extend Laravel's Blueprint class with custom macros to standardize and reuse common schema columns across database migrations.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Extend Laravel&#39;s Blueprint class with custom macros to standardize and reuse common schema columns across database migrations.</p>
</blockquote>
<p>Database migrations frequently repeat the same cluster of audit columns (such as created/updated timestamps, soft deletes, and user foreign keys) across multiple tables.</p>
<p>Using <code>Blueprint::macro()</code>, you can bundle these columns into a single reusable helper method.</p>
<hr>
<h2>Register the Macro</h2>
<p>Define your custom macro inside <code>AppServiceProvider::boot()</code>:</p>
<pre><code class="language-php">namespace App\Providers;

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Blueprint::macro(&#039;auditFields&#039;, function (bool $softDeletes = true) {
            $this-&gt;timestamps();

            if ($softDeletes) {
                $this-&gt;softDeletes();
            }

            $this-&gt;foreignId(&#039;created_by&#039;)-&gt;nullable()-&gt;constrained(&#039;users&#039;);
            $this-&gt;foreignId(&#039;updated_by&#039;)-&gt;nullable()-&gt;constrained(&#039;users&#039;);
        });
    }
}</code></pre><hr>
<h2>Use Across Migrations</h2>
<p>Call the macro directly on <code>$table</code> in any migration:</p>
<pre><code class="language-php">use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create(&#039;posts&#039;, function (Blueprint $table) {
            $table-&gt;id();
            $table-&gt;string(&#039;title&#039;);
            $table-&gt;text(&#039;content&#039;);

            // Injects timestamps, softDeletes, and audit user IDs
            $table-&gt;auditFields();
        });
    }
};</code></pre><hr>
<h2>Key Points</h2>
<ul>
<li><strong>Consistency</strong>: Guarantees identical column types, nullability, and foreign key constraints across all audited tables.</li>
<li><strong>Execution Timing</strong>: Macros expand into standard column definitions when <code>php artisan migrate</code> runs; they do not retroactively alter already-migrated tables.</li>
<li><strong>Parametric Flexibility</strong>: Add arguments to your macro closure (like <code>$softDeletes = true</code>) to adapt column inclusion per table.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Database</category>
            <category>Migrations</category>
            <category>Macros</category>
        </item>
        <item>
            <title><![CDATA[Define Custom Macros on the Eloquent Builder]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-builder-macro-registration</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-builder-macro-registration</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Register reusable query methods directly on the Builder so every model gains access without repeating logic.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Register reusable query methods directly on the Builder so every model gains access without repeating logic.</p>
</blockquote>
<p>Laravel&#39;s <code>Macroable</code> trait lets you add methods to the Builder at runtime. Register them in a service provider&#39;s <code>boot()</code> method and they become available on any query.</p>
<pre><code class="language-php">use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;

// In AppServiceProvider::boot()
Builder::macro(&#039;useIndex&#039;, function (string $table, string $index) {
    return $this-&gt;from(DB::raw(&quot;{$table} WITH (INDEX({$index}))&quot;));
});</code></pre><p>Now any model can hint a specific index:</p>
<pre><code class="language-php">use App\Models\Order;

$orders = Order::query()
    -&gt;useIndex(&#039;orders&#039;, &#039;orders_status_created_index&#039;)
    -&gt;where(&#039;status&#039;, &#039;pending&#039;)
    -&gt;get();</code></pre><ul>
<li>Register on <code>Illuminate\Database\Eloquent\Builder</code> for Eloquent-level macros</li>
<li>The closure receives <code>$this</code> bound to the current builder instance</li>
<li>Useful for performance hints that aren&#39;t covered by the default query builder API</li>
<li><strong>Note:</strong> The <code>WITH (INDEX(...))</code> syntax used above is specific to <strong>Microsoft SQL Server (T-SQL)</strong>. MySQL uses <code>FORCE INDEX(...)</code> and PostgreSQL relies on the query planner (no direct index hints). Adjust the macro body to match your database engine.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Vasile Papuc)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Macros</category>
        </item>
        <item>
            <title><![CDATA[Never Return Statements inside Finally Blocks in PHP]]></title>
            <link>https://mrpunyapal.dev/tips/php-never-return-in-finally-block</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/php-never-return-in-finally-block</guid>
            <pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Placing a return statement inside a try-catch finally block silently overrides exceptions and previous return statements.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Placing a return statement inside a try-catch finally block silently overrides exceptions and previous return statements.</p>
</blockquote>
<p>A return statement inside a finally block executes regardless of whether an exception was thrown or caught. It silently discards pending exceptions and overwrites return values from try or catch blocks.</p>
<pre><code class="language-php">// What does guess() return? It returns &#039;finally&#039;!
function guess(): string
{
    try {
        throw new Exception(&#039;Something went wrong&#039;);
    } catch (Exception $e) {
        return &#039;catch&#039;;
    } finally {
        return &#039;finally&#039;; // Silently overrides the catch return!
    }
}

// GOOD: Use finally strictly for resource cleanup
function processOrderClean(): bool
{
    try {
        return true;
    } finally {
        $this-&gt;cleanupLocks();
    }
}</code></pre><ul>
<li>Return inside finally discards thrown exceptions without logging them</li>
<li>Overwrites return values calculated in try or catch blocks</li>
<li>Use finally strictly for resource cleanup like closing file handles or releasing locks</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>PHP</category>
            <category>Basics</category>
            <category>PHP</category>
            <category>Exceptions</category>
            <category>Best Practices</category>
        </item>
        <item>
            <title><![CDATA[Automate PHP Readonly Class Refactoring with Rector]]></title>
            <link>https://mrpunyapal.dev/tips/php-rector-automate-readonly-classes</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/php-rector-automate-readonly-classes</guid>
            <pubDate>Wed, 29 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Use Rector rules to automatically convert immutable DTOs and value objects into native PHP 8.2 readonly classes.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use Rector rules to automatically convert immutable DTOs and value objects into native PHP 8.2 readonly classes.</p>
</blockquote>
<p>Manually adding <code>readonly</code> keywords across dozens of data transfer objects (DTOs) and value objects is repetitive.</p>
<p>Rector automates upgrading class declarations across your codebase using AST refactoring.</p>
<hr>
<h2>Configuration</h2>
<p>Register <code>ReadOnlyClassRector</code> in <code>rector.php</code>:</p>
<pre><code class="language-php">// rector.php
use Rector\Config\RectorConfig;
use Rector\Php82\Rector\Class_\ReadOnlyClassRector;

return RectorConfig::configure()
    -&gt;withRules([
        ReadOnlyClassRector::class,
    ]);</code></pre><hr>
<h2>What It Refactors</h2>
<pre><code class="language-php">// Before Rector (individual readonly properties):
class UserData
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
    ) {}
}

// After Rector (PHP 8.2+ native readonly class):
readonly class UserData
{
    public function __construct(
        public string $name,
        public string $email,
    ) {}
}</code></pre><hr>
<h2>Key Benefits</h2>
<ul>
<li><strong>Compiler-Level Immutability</strong>: Enforces that all properties cannot be modified after instantiation.</li>
<li><strong>Cleaner Constructors</strong>: Removes repetitive <code>readonly</code> property declarations in favor of a single class-level keyword.</li>
<li><strong>Safe Automation</strong>: Rector verifies property usage before promoting classes, ensuring mutable entities are not broken.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>PHP</category>
            <category>Tooling</category>
            <category>PHP</category>
            <category>Rector</category>
            <category>Refactoring</category>
        </item>
        <item>
            <title><![CDATA[Use sole() Instead of firstOrFail() for Single Record Guarantees]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-sole-vs-firstorfail</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-sole-vs-firstorfail</guid>
            <pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[When you expect exactly one matching record, use sole() instead of firstOrFail(). It guards against multiple records by throwing MultipleRecordsFoundException.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>When you expect exactly one matching record, use sole() instead of firstOrFail(). It guards against multiple records by throwing MultipleRecordsFoundException.</p>
</blockquote>
<p>When querying unique records, firstOrFail() silently returns the first record even if multiple records match due to data integrity issues. sole() makes sure exactly one record exists.</p>
<pre><code class="language-php">// Throws ModelNotFoundException if 0, MultipleRecordsFoundException if 2+
$user = User::where(&#039;verification_token&#039;, $token)-&gt;sole();</code></pre><ul>
<li>Asserts that exactly one record matches criteria</li>
<li>Catches data integrity anomalies before bad states propagate</li>
<li>Throws explicit MultipleRecordsFoundException</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Database</category>
        </item>
        <item>
            <title><![CDATA[Protect Custom Artisan Commands from AI Agents with Prohibitable]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-prohibitable-custom-commands-agents</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-prohibitable-custom-commands-agents</guid>
            <pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Use Laravel's Prohibitable trait on custom Artisan commands to selectively block AI agents from running destructive domain operations.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use Laravel&#39;s Prohibitable trait on custom Artisan commands to selectively block AI agents from running destructive domain operations.</p>
</blockquote>
<p>While <code>DB::prohibitDestructiveCommands()</code> protects core database migrations, custom Artisan commands (such as purging inactive tenants or deleting old records) require their own guard so AI coding agents do not execute them automatically.</p>
<hr>
<h2>1. Add Prohibitable to the Command Class</h2>
<p>Add the <code>Illuminate\Console\Prohibitable</code> trait to your custom command:</p>
<pre><code class="language-php">namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Console\Prohibitable;

class DeleteInactiveUsersCommand extends Command
{
    use Prohibitable;

    protected $signature = &#039;users:purge-inactive&#039;;

    public function handle(): int
    {
        if ($this-&gt;isProhibited()) {
            $this-&gt;error(&#039;This command is prohibited in this environment.&#039;);
            return Command::FAILURE;
        }

        // Execution logic...
        return Command::SUCCESS;
    }
}</code></pre><hr>
<h2>2. Prohibit in AppServiceProvider</h2>
<p>Prohibit execution when an AI coding agent is detected:</p>
<pre><code class="language-php">namespace App\Providers;

use App\Console\Commands\DeleteInactiveUsersCommand;
use Illuminate\Support\ServiceProvider;
use Laravel\AgentDetector\Facades\AgentDetector;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Humans can run locally; AI agents are blocked
        DeleteInactiveUsersCommand::prohibit(AgentDetector::detect()-&gt;isAgent);
    }
}</code></pre><hr>
<h2>Key Points</h2>
<ul>
<li><strong>Trait Mechanics</strong>: Adding <code>use Prohibitable;</code> provides both the static <code>::prohibit()</code> configuration method and the instance <code>-&gt;isProhibited()</code> check.</li>
<li><strong>Granular Control</strong>: Protects destructive application-specific commands without blanket blocking all Artisan tools.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Artisan</category>
            <category>AI</category>
        </item>
        <item>
            <title><![CDATA[Detect AI Agents in Laravel with AgentDetector and PAO]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-agent-detector-pao</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-agent-detector-pao</guid>
            <pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Laravel's AgentDetector detects whether an AI coding agent is interacting with your app, and PAO optimizes CLI output for agents.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Laravel&#39;s AgentDetector detects whether an AI coding agent is interacting with your app, and PAO optimizes CLI output for agents.</p>
</blockquote>
<p>AI coding agents (Cursor, Claude Code, Devin) are now part of regular development workflows. Laravel provides first-party tools to detect agents and optimize terminal output for LLM parsing.</p>
<hr>
<h2>Agent Detection</h2>
<pre><code class="language-php">use Laravel\AgentDetector\Facades\AgentDetector;

$detection = AgentDetector::detect();

if ($detection-&gt;isAgent) {
    // Inspect agent name (e.g. &#039;claude-code&#039;, &#039;cursor&#039;, &#039;devin&#039;)
    logger()-&gt;info(&#039;AI agent session&#039;, [&#039;agent&#039; =&gt; $detection-&gt;name]);
}</code></pre><hr>
<h2>How PAO Works</h2>
<ul>
<li><strong>First-Party</strong>: Ships with PAO (<code>laravel/pao</code>) included by default in new Laravel applications.</li>
<li><strong>Output Optimization</strong>: PAO automatically switches verbose Artisan CLI output into compact JSON when an agent is detected.</li>
<li><strong>Human Invariant</strong>: Human developers see standard terminal formatting with zero workflow change.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Configuration</category>
            <category>Laravel</category>
            <category>AI</category>
            <category>DevOps</category>
        </item>
        <item>
            <title><![CDATA[Stream Large Datasets: cursor() vs lazy() in Eloquent]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-cursor-vs-lazy-collections</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-cursor-vs-lazy-collections</guid>
            <pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[cursor() hydrates single model instances sequentially using database cursors; lazy() streams records in chunks backed by LazyCollection.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>cursor() hydrates single model instances sequentially using database cursors; lazy() streams records in chunks backed by LazyCollection.</p>
</blockquote>
<p>When iterating millions of records, get() exhausts PHP memory limits. cursor() uses PDO cursors to fetch records one by one, while lazy() queries records in chunks while exposing a fluent LazyCollection.</p>
<pre><code class="language-php">use App\Models\User;

// Cursors: single query, streams 1 instance at a time (lowest memory)
foreach (User::where(&#039;active&#039;, false)-&gt;cursor() as $user) {
    $user-&gt;archive();
}

// Lazy: queries in chunks of 1000 under the hood, provides LazyCollection API
User::where(&#039;active&#039;, false)-&gt;lazy(1000)-&gt;each-&gt;archive();</code></pre><ul>
<li>cursor() uses a single database connection cursor for minimal RAM usage</li>
<li>lazy() executes chunked subqueries under the hood and allows collection chaining</li>
<li>Both prevent loading full datasets into PHP memory at once</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Performance</category>
        </item>
        <item>
            <title><![CDATA[Choose the Right Processing Method: chunk(), lazy(), chunkById(), lazyById()]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-chunk-vs-lazy-by-id</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-chunk-vs-lazy-by-id</guid>
            <pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Understand offset vs keyset pagination when processing large datasets to avoid missing records during updates.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Understand offset vs keyset pagination when processing large datasets to avoid missing records during updates.</p>
</blockquote>
<p>Updating records inside chunk() modifies the result set, causing offset pagination to skip every second chunk. Use chunkById() or lazyById() when updating query columns.</p>
<pre><code class="language-php">use App\Models\User;

// BAD when updating filtered column: skips records due to offset shift!
User::where(&#039;processed&#039;, false)-&gt;chunk(100, function ($users) {
    $users-&gt;each-&gt;update([&#039;processed&#039; =&gt; true]);
});

// GOOD: Uses primary key comparison (id &gt; last_id) to avoid skipping
User::where(&#039;processed&#039;, false)-&gt;chunkById(100, function ($users) {
    $users-&gt;each-&gt;update([&#039;processed&#039; =&gt; true]);
});</code></pre><ul>
<li>chunk() uses OFFSET pagination (vulnerable to skipping if queried fields change)</li>
<li>chunkById() uses keyset pagination (WHERE id &gt; last_id), safe for updates</li>
<li>lazyById() provides the same keyset safety wrapped in a LazyCollection</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Performance</category>
        </item>
        <item>
            <title><![CDATA[UI Controls Are Not Security Layers: Always Enforce Policies]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-security-ui-vs-policy-enforcement</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-security-ui-vs-policy-enforcement</guid>
            <pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Hiding buttons in Blade or Vue does not restrict access. Always enforce authorization policies in controller or request layers.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Hiding buttons in Blade or Vue does not restrict access. Always enforce authorization policies in controller or request layers.</p>
</blockquote>
<p>Hiding an edit button using @can or v-if only modifies visual presentation. Attackers can submit HTTP requests directly to backend endpoints. Always enforce authorization logic in backend controllers or form requests.</p>
<pre><code class="language-php">// Blade UI (Visual convenience only)
@can(&#039;update&#039;, $post)
    &lt;a href=&quot;{{ route(&#039;posts.edit&#039;, $post) }}&quot;&gt;Edit Post&lt;/a&gt;
@endcan

// Controller Action (Actual Security Layer)
public function update(UpdatePostRequest $request, Post $post)
{
    $this-&gt;authorize(&#039;update&#039;, $post); // Enforces server-side security
    $post-&gt;update($request-&gt;validated());
}</code></pre><ul>
<li>UI directive checks like @can are user-experience features, not security guards</li>
<li>Always enforce $this-&gt;authorize() or Policy checks in backend controllers</li>
<li>Prevents unauthorized HTTP request payload tampering</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Validation</category>
            <category>Laravel</category>
            <category>Security</category>
            <category>Policies</category>
        </item>
        <item>
            <title><![CDATA[Dispatch Jobs After Transaction Commit with DB::afterCommit()]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-db-after-commit-transaction-jobs</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-db-after-commit-transaction-jobs</guid>
            <pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Use DB::afterCommit() to defer job dispatching until surrounding database transactions complete successfully.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use DB::afterCommit() to defer job dispatching until surrounding database transactions complete successfully.</p>
</blockquote>
<p>Dispatching queue jobs inside active DB transactions can trigger race conditions where background workers run before the transaction commits. DB::afterCommit() delays side effects until commit finishes.</p>
<pre><code class="language-php">use Illuminate\Support\Facades\DB;
use App\Jobs\ProcessPayment;

DB::transaction(function () use ($order) {
    $order-&gt;save();
    
    // Guarantees worker receives committed database records
    DB::afterCommit(fn () =&gt; ProcessPayment::dispatch($order));
});</code></pre><ul>
<li>Prevents race conditions where queue workers query uncommitted database rows</li>
<li>Discards callbacks automatically if transaction rolls back</li>
<li>Can be set on jobs using public $afterCommit = true;</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Queue</category>
            <category>Laravel</category>
            <category>Database</category>
            <category>Queue</category>
        </item>
        <item>
            <title><![CDATA[Assert JSON Columns and Backed Enums with assertDatabaseHas]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-assert-database-has-json-enums</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-assert-database-has-json-enums</guid>
            <pubDate>Mon, 15 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In Laravel and Pest tests, assertDatabaseHas() natively queries nested JSON properties using arrow syntax and accepts backed Enum instances directly.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>In Laravel and Pest tests, assertDatabaseHas() natively queries nested JSON properties using arrow syntax and accepts backed Enum instances directly.</p>
</blockquote>
<p>Testing JSON attributes or PHP backed enums in database assertions requires no manual casting, JSON encoding, or <code>-&gt;value</code> calls.</p>
<p><code>assertDatabaseHas()</code> natively serializes backed enums and translates arrow notation into database JSON queries.</p>
<hr>
<h2>Code Examples</h2>
<pre><code class="language-php">use App\Enums\UserRole;

// In Pest tests:
assertDatabaseHas(&#039;users&#039;, [
    &#039;role&#039; =&gt; UserRole::Maintainer, // Serializes backed enum automatically
    &#039;settings-&gt;theme&#039; =&gt; &#039;dark&#039;,    // Queries nested JSON key
    &#039;settings-&gt;notifications-&gt;email&#039; =&gt; true,
]);

// In PHPUnit tests:
$this-&gt;assertDatabaseHas(&#039;users&#039;, [
    &#039;role&#039; =&gt; UserRole::Maintainer,
    &#039;settings-&gt;theme&#039; =&gt; &#039;dark&#039;,
]);</code></pre><hr>
<h2>Key Benefits</h2>
<ul>
<li><strong>Enum Serialization</strong>: Passes backed enum instances directly without manual <code>-&gt;value</code> extraction.</li>
<li><strong>Nested JSON Traversal</strong>: Arrow syntax (<code>settings-&gt;theme</code>) queries database JSON paths across MySQL, PostgreSQL, and SQLite.</li>
<li><strong>Framework Agnostic</strong>: Works identically in both Pest test functions and PHPUnit test cases.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Utilities</category>
            <category>Laravel</category>
            <category>Testing</category>
            <category>Pest</category>
            <category>Enums</category>
        </item>
        <item>
            <title><![CDATA[Streamline Testing with Pest Test Impact Analysis (TIA)]]></title>
            <link>https://mrpunyapal.dev/tips/pest-tia-plugin-workflow</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/pest-tia-plugin-workflow</guid>
            <pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Enable the Tia Engine in Pest to only re-run tests affected by your latest code changes while replaying cached results for unaffected tests.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Enable the Tia Engine in Pest to only re-run tests affected by your latest code changes while replaying cached results for unaffected tests.</p>
</blockquote>
<p>Running full test suites on every minor change slows down local feedback loops. While <code>--dirty</code> only checks if test files themselves changed, Pest&#39;s Tia Engine analyzes code coverage mapping to identify tests that execute the actual application lines you modified.</p>
<hr>
<h2>1. CLI Usage</h2>
<p>Run Pest with the <code>--tia</code> flag:</p>
<pre><code class="language-bash"># Run tests with Test Impact Analysis enabled
./vendor/bin/pest --parallel --tia</code></pre><p>On the initial run, Pest records a dependency graph of which tests touch which files. On subsequent runs, it only re-runs affected tests and replays cached results for the rest.</p>
<hr>
<h2>2. Configuration in tests/Pest.php</h2>
<p>To avoid typing <code>--tia</code> on every run, configure the Tia Engine fluently inside <code>tests/Pest.php</code>:</p>
<pre><code class="language-php">// tests/Pest.php

pest()-&gt;tia()
    -&gt;locally()   // Run TIA on every local invocation; skipped automatically in CI
    -&gt;baselined() // Fetch shared baseline from CI when no local graph exists
    -&gt;filtered(); // Narrow test runner to affected test files only</code></pre><ul>
<li><strong><code>locally()</code></strong>: Recommended. Activates TIA for every local <code>pest</code> run without requiring the <code>--tia</code> flag. In CI (or with <code>--ci</code>), TIA is skipped automatically so pipelines run the full suite.</li>
<li><strong><code>always()</code></strong>: Alternative to <code>locally()</code>. Enforces TIA across all environments including CI.</li>
<li><strong><code>baselined()</code></strong>: Downloads the shared CI baseline artifact so developers replay immediately on fresh clones.</li>
</ul>
<hr>
<h2>Key Rules &amp; Requirements</h2>
<ul>
<li><strong>Coverage Driver</strong>: Requires a code coverage driver (<strong>PCOV</strong> or <strong>Xdebug</strong>) to build the dependency graph.</li>
<li><strong>Cosmetic Invariant</strong>: Whitespace changes, comments, and docblock edits produce identical hashes, executing zero tests.</li>
<li><strong>CLI Overrides</strong>: Bypass or force TIA anytime using <code>--tia</code> or <code>--no-tia</code>.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Pest PHP</category>
            <category>Plugins</category>
            <category>Pest</category>
            <category>Testing</category>
            <category>DX</category>
        </item>
        <item>
            <title><![CDATA[Protect Production with Laravel Prohibitable Commands]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-prohibitable-destructive-commands</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-prohibitable-destructive-commands</guid>
            <pubDate>Sat, 02 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Prevent catastrophic accidents like db:wipe or migrate:fresh in production using Laravel's Prohibitable trait and DB::prohibitDestructiveCommands().]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Prevent catastrophic accidents like db:wipe or migrate:fresh in production using Laravel&#39;s Prohibitable trait and DB::prohibitDestructiveCommands().</p>
</blockquote>
<p>Accidentally running <code>migrate:fresh</code> or <code>db:wipe</code> on a production database is catastrophic.</p>
<p>Laravel provides <code>DB::prohibitDestructiveCommands()</code> to block dangerous commands when running in production.</p>
<hr>
<h2>AppServiceProvider Configuration</h2>
<p>Add the call inside your <code>AppServiceProvider::boot()</code> method:</p>
<pre><code class="language-php">namespace App\Providers;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Block destructive migration commands when in production
        DB::prohibitDestructiveCommands(
            $this-&gt;app-&gt;isProduction()
        );
    }
}</code></pre><hr>
<h2>Commands Protected</h2>
<p>When prohibited, attempts to run the following commands fail with an exit error code:</p>
<ul>
<li><code>php artisan db:wipe</code></li>
<li><code>php artisan migrate:fresh</code></li>
<li><code>php artisan migrate:refresh</code></li>
<li><code>php artisan migrate:reset</code></li>
</ul>
<hr>
<h2>Key Points</h2>
<ul>
<li><strong>Zero Accidental Overrides</strong>: Even passing the <code>--force</code> flag cannot bypass a prohibited command.</li>
<li><strong>Environment Driven</strong>: Passing <code>$this-&gt;app-&gt;isProduction()</code> keeps local, staging, and automated testing environments fully functional.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Configuration</category>
            <category>Laravel</category>
            <category>Database</category>
            <category>Security</category>
        </item>
        <item>
            <title><![CDATA[Why strip_tags() Is Not Enough for XSS Protection]]></title>
            <link>https://mrpunyapal.dev/tips/php-security-strip-tags-vs-htmlpurifier</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/php-security-strip-tags-vs-htmlpurifier</guid>
            <pubDate>Sat, 18 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[strip_tags() removes HTML elements but fails to sanitize inline attributes or malformed HTML payload vectors. Use HTMLPurifier for rich text input.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>strip_tags() removes HTML elements but fails to sanitize inline attributes or malformed HTML payload vectors. Use HTMLPurifier for rich text input.</p>
</blockquote>
<p>A common security misconception in PHP is relying on strip_tags() to sanitize user-submitted rich text. It allows attribute payloads like onload= or javascript: URIs through if allowed tags are specified.</p>
<pre><code class="language-php">use HTMLPurifier;

$purifier = new HTMLPurifier();
$cleanHtml = $purifier-&gt;purify($input);</code></pre><ul>
<li>strip_tags() does not validate tag attributes or execution vectors</li>
<li>Always escape plain text with e() or Blade&#39;s {{ $var }}</li>
<li>For user-submitted rich text, use reliable HTML sanitizers like HTMLPurifier</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>PHP</category>
            <category>Basics</category>
            <category>PHP</category>
            <category>Security</category>
            <category>XSS</category>
        </item>
        <item>
            <title><![CDATA[Clean Up Complex Multi-Step Operations with Illuminate Pipeline]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-pipeline-pattern-complex-workflows</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-pipeline-pattern-complex-workflows</guid>
            <pubDate>Sun, 05 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Process complex data sequences or multi-stage order checks through Laravel's built-in Pipeline facade to replace massive controller methods.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Process complex data sequences or multi-stage order checks through Laravel&#39;s built-in Pipeline facade to replace massive controller methods.</p>
</blockquote>
<p>When processing multi-stage workflows (such as order checkout validation or user onboarding steps), controller actions frequently accumulate deeply nested <code>if</code> statements.</p>
<p>Laravel&#39;s <code>Pipeline</code> facade passes an object sequentially through dedicated pipe classes.</p>
<hr>
<h2>The Pipeline Runner</h2>
<pre><code class="language-php">use App\Models\Order;
use App\Pipes\ApplyCoupon;
use App\Pipes\CalculateTax;
use App\Pipes\VerifyStock;
use Illuminate\Support\Facades\Pipeline;

$order = Pipeline::send($draftOrder)
    -&gt;through([
        VerifyStock::class,
        ApplyCoupon::class,
        CalculateTax::class,
    ])
    -&gt;thenReturn();</code></pre><hr>
<h2>Anatomy of a Pipe Class</h2>
<p>Each pipe implements a <code>handle()</code> method accepting the passable object and a <code>$next</code> closure:</p>
<pre><code class="language-php">namespace App\Pipes;

use App\Models\Order;
use Closure;

class ApplyCoupon
{
    public function handle(Order $order, Closure $next)
    {
        if ($order-&gt;coupon_code) {
            $order-&gt;discount = 15.00;
        }

        // Pass the modified object to the next pipe in sequence
        return $next($order);
    }
}</code></pre><hr>
<h2>Key Benefits</h2>
<ul>
<li><strong>Single Responsibility</strong>: Each step is an isolated class with one clear focus.</li>
<li><strong>Easy Reordering</strong>: Add, remove, or reorder pipeline stages without touching adjacent logic.</li>
<li><strong>Testability</strong>: Test individual pipeline steps independently with unit tests.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Architecture</category>
            <category>Design Patterns</category>
        </item>
        <item>
            <title><![CDATA[Clean Up Redundant Code Comments with a Custom Rector Rule]]></title>
            <link>https://mrpunyapal.dev/tips/php-rector-remove-unnecessary-comments</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/php-rector-remove-unnecessary-comments</guid>
            <pubDate>Wed, 11 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Use a custom Rector rule to automatically strip noisy inline comments and empty docblocks across your codebase while preserving essential PHPDoc annotations.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use a custom Rector rule to automatically strip noisy inline comments and empty docblocks across your codebase while preserving essential PHPDoc annotations.</p>
</blockquote>
<p>Codebases often accumulate redundant comments over time: commented-out legacy code, obvious explanations (<code>// set user name</code>), or empty boilerplate docblocks generated by older IDE templates.</p>
<p>Manually reviewing and removing thousands of stale comments across hundreds of files is time-consuming. Using Rector, you can define an AbstractRector rule that strips non-essential comments while retaining critical PHPDoc annotations such as <code>@param</code>, <code>@return</code>, and <code>@throws</code>.</p>
<h2>The Rector Implementation</h2>
<pre><code class="language-php">namespace App\Rector;

use PhpParser\Comment;
use PhpParser\Comment\Doc;
use PhpParser\Node;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

final class RemoveUnnecessaryCommentsRector extends AbstractRector
{
    // Whitelist specific comment strings or tags you want to keep
    private const ALLOWED_PATTERNS = [
        &#039;@todo&#039;,
        &#039;@var&#039;,
    ];

    public function getNodeTypes(): array
    {
        return [Node::class];
    }

    public function refactor(Node $node): ?Node
    {
        $comments = $node-&gt;getComments();

        if ($comments === []) {
            return null;
        }

        $filtered = [];
        $changed = false;

        foreach ($comments as $comment) {
            if ($comment instanceof Doc) {
                $cleaned = $this-&gt;cleanDocBlock($comment);

                if ($cleaned !== $comment-&gt;getText()) {
                    if ($cleaned !== &#039;&#039;) {
                        $filtered[] = new Doc($cleaned);
                    }
                    $changed = true;
                } else {
                    $filtered[] = $comment;
                }
                continue;
            }

            // Check if regular comment contains allowed keywords
            if ($this-&gt;isAllowed($comment-&gt;getText())) {
                $filtered[] = $comment;
            } else {
                $changed = true;
            }
        }

        if (! $changed) {
            return null;
        }

        $node-&gt;setAttribute(&#039;comments&#039;, $filtered);

        return $node;
    }

    private function cleanDocBlock(Doc $doc): string
    {
        $lines = preg_split(&#039;/\R/&#039;, $doc-&gt;getText());
        $tags = [];

        foreach ($lines as $line) {
            $line = trim(preg_replace(&#039;/^\s*\*\s?/&#039;, &#039;&#039;, $line));

            if ($line === &#039;&#039;) {
                continue;
            }

            // Preserve standard PHPDoc annotations (@param, @return, @throws, etc.)
            if (str_starts_with($line, &#039;@&#039;)) {
                $tags[] = $line;
            }
        }

        if ($tags === []) {
            return &#039;&#039;;
        }

        return &quot;/**\n * &quot; . implode(&quot;\n * &quot;, $tags) . &quot;\n */&quot;;
    }

    private function isAllowed(string $text): bool
    {
        foreach (self::ALLOWED_PATTERNS as $allowed) {
            if (str_contains($text, $allowed)) {
                return true;
            }
        }

        return false;
    }

    public function getRuleDefinition(): RuleDefinition
    {
        return new RuleDefinition(&#039;Remove redundant inline comments while retaining essential PHPDoc tags&#039;, [
            new CodeSample(
                &lt;&lt;&lt;&#039;CODE&#039;
                // Initialize user model
                $user = new User();
                CODE
                ,
                &lt;&lt;&lt;&#039;CODE&#039;
                $user = new User();
                CODE
            ),
        ]);
    }
}</code></pre><h2>Registering in <code>rector.php</code></h2>
<p>Add the custom rule to your project&#39;s Rector configuration:</p>
<pre><code class="language-php">use App\Rector\RemoveUnnecessaryCommentsRector;
use Rector\Config\RectorConfig;

return RectorConfig::configure()
    -&gt;withRules([
        RemoveUnnecessaryCommentsRector::class,
    ]);</code></pre><p>Run the refactoring dry-run to preview changes:</p>
<pre><code class="language-bash">vendor/bin/rector process --dry-run</code></pre><h2>Summary</h2>
<ul>
<li>Use AST-based comment manipulation in Rector to systematically clean comment noise across large codebases.</li>
<li>The rule strips redundant plain comments (<code>// comment</code>) while keeping meaningful docblock tags (<code>@param</code>, <code>@return</code>, <code>@throws</code>).</li>
<li>Easily extensible to whitelist specific tags or maintenance markers such as <code>@todo</code>.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>PHP</category>
            <category>Tooling</category>
            <category>PHP</category>
            <category>Rector</category>
            <category>Refactoring</category>
            <category>Clean Code</category>
            <category>Tooling</category>
        </item>
        <item>
            <title><![CDATA[Automatically Discard Orphaned Queue Jobs with deleteWhenMissingModels]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-queue-delete-when-missing-models</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-queue-delete-when-missing-models</guid>
            <pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Use the $deleteWhenMissingModels property on queued jobs to silently discard jobs if their referenced database records were deleted before execution.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use the $deleteWhenMissingModels property on queued jobs to silently discard jobs if their referenced database records were deleted before execution.</p>
</blockquote>
<p>When an Eloquent model is passed to a queued job and a user deletes that record before the worker picks up the job, Laravel throws a <code>ModelNotFoundException</code>, causing the job to fail and populate the <code>failed_jobs</code> table.</p>
<p>Setting <code>$deleteWhenMissingModels = true</code> instructs the worker to delete the job silently.</p>
<h2>Enabling on Job Classes</h2>
<pre><code class="language-php">namespace App\Jobs;

use App\Models\Post;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class GeneratePostThumbnailJob implements ShouldQueue
{
    use InteractsWithQueue, Queueable, SerializesModels;

    // Silently delete job if the Post model was deleted from database
    public bool $deleteWhenMissingModels = true;

    public function __construct(public Post $post) {}

    public function handle(): void
    {
        // Process thumbnail
    }
}</code></pre><h2>Summary</h2>
<ul>
<li>Prevents false-alarm errors in <code>failed_jobs</code> and error tracking tools (Sentry, Bugsnag).</li>
<li>Silently purges queued tasks when parent models are deleted.</li>
<li>Standard property provided by the <code>SerializesModels</code> trait.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Queue</category>
            <category>Laravel</category>
            <category>Queue</category>
            <category>Error Handling</category>
            <category>Clean Code</category>
        </item>
        <item>
            <title><![CDATA[Prevent Queue Payload Bloat with withoutRelations()]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-queue-without-relations-serialization</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-queue-without-relations-serialization</guid>
            <pubDate>Wed, 17 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Use withoutRelations() when passing models to queue jobs to prevent large in-memory relationship graphs from serializing into queue storage.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use withoutRelations() when passing models to queue jobs to prevent large in-memory relationship graphs from serializing into queue storage.</p>
</blockquote>
<p>When dispatching queue jobs that accept an Eloquent model instance, Laravel serializes the model. If the model had dozens of nested relationships eager-loaded in the controller, all those related models are serialized into the queue payload (Redis / Database), causing massive payload bloat.</p>
<p>Calling <code>withoutRelations()</code> strips loaded relations before dispatching.</p>
<h2>Basic Usage</h2>
<pre><code class="language-php">use App\Jobs\GenerateUserInvoiceJob;

public function checkout(Order $order)
{
    // $order has heavy loaded relations (items, customer, history)

    // Strips loaded relations so only the primary key is serialized
    GenerateUserInvoiceJob::dispatch($order-&gt;withoutRelations());
}</code></pre><h2>In Job Class Constructors</h2>
<p>Alternatively, clean relations in the constructor:</p>
<pre><code class="language-php">namespace App\Jobs;

use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\SerializesModels;

class ProcessOrderJob implements ShouldQueue
{
    use Queueable, SerializesModels;

    public Order $order;

    public function __construct(Order $order)
    {
        $this-&gt;order = $order-&gt;withoutRelations();
    }
}</code></pre><h2>Summary</h2>
<ul>
<li>Reduces serialized queue payload sizes from megabytes to bytes.</li>
<li>Prevents <code>Redis command size exceeded</code> or long text column limits in <code>jobs</code> tables.</li>
<li>Queue workers re-hydrate fresh relationships when needed.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Queue</category>
            <category>Laravel</category>
            <category>Queue</category>
            <category>Performance</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Optimize Queue Worker Polling Overheads in Production]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-queue-worker-polling-optimization</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-queue-worker-polling-optimization</guid>
            <pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Use queue:work with appropriate sleep configuration or Redis blocking pops to reduce database CPU polling overheads.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use queue:work with appropriate sleep configuration or Redis blocking pops to reduce database CPU polling overheads.</p>
</blockquote>
<p>Queue workers set to poll databases without sleep configs execute continuous SELECT queries, driving database CPU usage up. Configure appropriate sleep intervals or use Redis queue drivers.</p>
<pre><code class="language-bash"># Wait 3 seconds when queue is empty before polling again
php artisan queue:work --sleep=3 --tries=3 --timeout=90</code></pre><ul>
<li>--sleep=3 pauses worker polling when no jobs are available</li>
<li>Reduces CPU usage and database query loads on idle queue workers</li>
<li>Redis queue driver uses blocking pop operations for instant zero-polling dispatch</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Queue</category>
            <category>Laravel</category>
            <category>Queue</category>
            <category>DevOps</category>
        </item>
        <item>
            <title><![CDATA[Auto-Scale Queue Workers with --stop-when-empty-for in Laravel]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-queue-worker-stop-when-empty-for</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-queue-worker-stop-when-empty-for</guid>
            <pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[The --stop-when-empty-for option keeps queue workers alive for a specific grace period after the queue empties, preventing rapid process churn during bursty workloads.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>The --stop-when-empty-for option keeps queue workers alive for a specific grace period after the queue empties, preventing rapid process churn during bursty workloads.</p>
</blockquote>
<p>Running <code>php artisan queue:work --stop-when-empty</code> in serverless environments or container auto-scalers terminates the worker process immediately when no jobs remain. If new jobs arrive seconds later, new processes must spin up continuously.</p>
<p>The <code>--stop-when-empty-for</code> option adds a configurable idle timeout:</p>
<pre><code class="language-bash"># Stops worker immediately once queue is empty
php artisan queue:work --stop-when-empty

# Keeps worker running for 60 idle seconds before exiting
php artisan queue:work --stop-when-empty-for=60</code></pre><ul>
<li>Prevents process start/stop churn during intermittent job spikes</li>
<li>Ideal for AWS ECS, Kubernetes HPA, and Serverless worker instances</li>
<li>Retains database connection pooling while waiting for trailing jobs</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Queue</category>
            <category>Laravel</category>
            <category>Queue</category>
            <category>DevOps</category>
        </item>
        <item>
            <title><![CDATA[Optimize Date Queries by Replacing whereYear() with whereBetween()]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-eloquent-where-year-vs-date-between-index</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-eloquent-where-year-vs-date-between-index</guid>
            <pubDate>Wed, 19 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Replace whereYear() and whereMonth() on large database tables with whereBetween() date ranges to enable SQL index lookups.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Replace whereYear() and whereMonth() on large database tables with whereBetween() date ranges to enable SQL index lookups.</p>
</blockquote>
<p>Using <code>whereYear(&#39;created_at&#39;, 2026)</code> forces database engines (MySQL, PostgreSQL) to wrap the column in an SQL function (<code>WHERE YEAR(created_at) = 2026</code>).</p>
<p>Wrapping indexed columns in functions invalidates database B-Tree indexes, causing full table scans.</p>
<h2>The Problem with whereYear()</h2>
<pre><code class="language-php">// ❌ SLOW: SQL function YEAR(created_at) disables database index!
$orders = Order::whereYear(&#039;created_at&#039;, 2026)-&gt;get();</code></pre><h2>The High-Performance Solution: whereBetween()</h2>
<p>Using explicit timestamp boundaries enables the database engine to perform fast range index lookups:</p>
<pre><code class="language-php">use Carbon\Carbon;

$startOfYear = Carbon::create(2026, 1, 1)-&gt;startOfDay();
$endOfYear = Carbon::create(2026, 12, 31)-&gt;endOfDay();

// ✅ FAST: Uses standard B-Tree range index lookup
$orders = Order::whereBetween(&#039;created_at&#039;, [$startOfYear, $endOfYear])-&gt;get();</code></pre><h2>Monthly Queries Example</h2>
<pre><code class="language-php">// Querying a specific month (e.g. October 2026)
$orders = Order::whereBetween(&#039;created_at&#039;, [
    now()-&gt;startOfMonth(),
    now()-&gt;endOfMonth(),
])-&gt;get();</code></pre><h2>Summary</h2>
<ul>
<li>SQL date functions (<code>YEAR()</code>, <code>MONTH()</code>) prevent query optimizers from using column indexes.</li>
<li><code>whereBetween(&#39;created_at&#39;, [$start, $end])</code> utilizes B-Tree indexes for fast range scans.</li>
<li>Essential optimization for tables with hundreds of thousands of records.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Performance</category>
            <category>Database</category>
            <category>Indexes</category>
        </item>
        <item>
            <title><![CDATA[Prevent Duplicate Redis and Database Lookups with Cache::memo()]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-cache-memo-driver-in-memory</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-cache-memo-driver-in-memory</guid>
            <pubDate>Wed, 15 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Use Cache::memo() to combine persistent cache stores with per-request memory caching, preventing repetitive network roundtrips during a single HTTP request.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use Cache::memo() to combine persistent cache stores with per-request memory caching, preventing repetitive network roundtrips during a single HTTP request.</p>
</blockquote>
<p>Calling <code>Cache::get(&#39;key&#39;)</code> multiple times in a single request still incurs a Redis network roundtrip or database query each time.</p>
<p><code>Cache::memo()</code> wraps your configured cache store with an in-memory array cache for the duration of the current request:</p>
<pre><code class="language-php">use Illuminate\Support\Facades\Cache;

// Standard Cache: 3 Redis network roundtrips
$permissions = Cache::get(&#039;user.permissions&#039;); // Redis query
$permissions = Cache::get(&#039;user.permissions&#039;); // Redis query
$permissions = Cache::get(&#039;user.permissions&#039;); // Redis query

// Cache::memo(): 1 Redis query, subsequent calls read from memory
$permissions = Cache::memo()-&gt;get(&#039;user.permissions&#039;); // Redis query
$permissions = Cache::memo()-&gt;get(&#039;user.permissions&#039;); // In-memory hit
$permissions = Cache::memo()-&gt;get(&#039;user.permissions&#039;); // In-memory hit

// Mutations automatically sync the persistent store and invalidate local memory
Cache::memo()-&gt;put(&#039;user.status&#039;, &#039;active&#039;);
Cache::memo()-&gt;increment(&#039;page.views&#039;);

// Works with specific cache stores
Cache::memo(&#039;redis&#039;)-&gt;remember(&#039;expensive-report&#039;, 3600, fn () =&gt;
    $this-&gt;buildReport()
);</code></pre><ul>
<li>Eliminates redundant cache store queries during heavy request lifecycles</li>
<li>In-memory values reset automatically at the end of the HTTP request</li>
<li>Mutation methods (<code>put</code>, <code>increment</code>, <code>forget</code>) keep memory and storage in sync</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>HTTP &amp; API</category>
            <category>Laravel</category>
            <category>Cache</category>
            <category>Performance</category>
        </item>
        <item>
            <title><![CDATA[Index Dynamic JSON Attributes with Generated Virtual Columns]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-database-generated-virtual-columns</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-database-generated-virtual-columns</guid>
            <pubDate>Wed, 27 Aug 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Use virtualAs() and storedAs() in migrations to create generated database columns for high-speed indexing on JSON attributes.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use virtualAs() and storedAs() in migrations to create generated database columns for high-speed indexing on JSON attributes.</p>
</blockquote>
<p>Querying deep JSON attributes (such as <code>WHERE JSON_EXTRACT(metadata, &#39;$.country&#39;) = &#39;US&#39;</code>) cannot use standard B-Tree column indexes, resulting in slow full table scans on large tables.</p>
<p>Generated columns extract values from JSON or string expressions and allow standard indexing.</p>
<h2>Creating Generated Columns in Migrations</h2>
<pre><code class="language-php">use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create(&#039;orders&#039;, function (Blueprint $table) {
            $table-&gt;id();
            $table-&gt;json(&#039;payload&#039;);

            // Virtual column computed on read (MySQL / PostgreSQL / SQLite)
            $table-&gt;string(&#039;currency&#039;)-&gt;virtualAs(&quot;payload-&gt;&gt;&#039;$.currency&#039;&quot;);

            // Stored column saved physically on disk for indexing
            $table-&gt;decimal(&#039;total_amount&#039;, 10, 2)-&gt;storedAs(&quot;payload-&gt;&gt;&#039;$.total&#039;&quot;);

            // Add standard fast B-Tree index on the generated column!
            $table-&gt;index(&#039;total_amount&#039;);

            $table-&gt;timestamps();
        });
    }
};</code></pre><h2>Querying Naturally in Eloquent</h2>
<pre><code class="language-php">// Fast indexed query on the generated column!
$largeOrders = Order::where(&#039;total_amount&#039;, &#039;&gt;&#039;, 500)-&gt;get();</code></pre><h2>Summary</h2>
<ul>
<li><code>virtualAs()</code>: Computes value dynamically during reads without disk storage.</li>
<li><code>storedAs()</code>: Computes value on insert/update and stores on disk, allowing standard B-Tree index creation.</li>
<li>Accelerates heavy queries on JSON payloads by 10x–100x.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Database</category>
            <category>Laravel</category>
            <category>Database</category>
            <category>Migrations</category>
            <category>Performance</category>
        </item>
        <item>
            <title><![CDATA[Tick-Based Execution Timeout Wrapper in PHP Without PCNTL]]></title>
            <link>https://mrpunyapal.dev/tips/php-timeout-guard-ticks-without-pcntl</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/php-timeout-guard-ticks-without-pcntl</guid>
            <pubDate>Fri, 13 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Interrupt long-running synchronous PHP callables on any operating system using PHP tick declarations and tick callback functions without requiring the pcntl extension.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Interrupt long-running synchronous PHP callables on any operating system using PHP tick declarations and tick callback functions without requiring the pcntl extension.</p>
</blockquote>
<p>In command-line scripts, background jobs, or plugin execution engines, you may need to impose a hard execution timeout on a closure or callable. The standard Unix approach uses <code>pcntl_alarm()</code> or <code>pcntl_fork()</code>, but the <code>pcntl</code> extension is unavailable on Windows environments and in certain shared hosting setups.</p>
<p>Using PHP ticks (<code>declare(ticks=1)</code>), you can implement a cross-platform execution watchdog that monitors elapsed execution time and interrupts runaway code.</p>
<h2>The TimeoutGuard Implementation</h2>
<pre><code class="language-php">declare(ticks=1);

class TimeoutException extends Exception {}

class TimeoutGuard
{
    public static function run(callable $callback, float $seconds): mixed
    {
        $start = microtime(true);

        // Tick handler: checks elapsed time on every low-level tick event
        $watchdog = function () use ($start, $seconds): void {
            if ((microtime(true) - $start) &gt; $seconds) {
                throw new TimeoutException(&quot;Execution interrupted: timeout of {$seconds}s exceeded&quot;);
            }
        };

        register_tick_function($watchdog);

        try {
            return $callback();
        } finally {
            unregister_tick_function($watchdog);
        }
    }
}</code></pre><h2>Usage Example</h2>
<p>Wrap any synchronous task inside <code>TimeoutGuard::run()</code>:</p>
<pre><code class="language-php">try {
    $result = TimeoutGuard::run(function () {
        // Heavy computational task or complex regex matching
        for ($i = 0; $i &lt; 1e7; $i++) {
            sqrt($i);
        }
        return &#039;Completed&#039;;
    }, 0.5); // 500ms limit

    echo &quot;Task finished: {$result}
&quot;;
} catch (TimeoutException $e) {
    echo &quot;Execution timed out: &quot; . $e-&gt;getMessage() . &quot;
&quot;;
}</code></pre><h2>How It Works</h2>
<ol>
<li><strong><code>declare(ticks=1)</code></strong>: Tells the PHP parser to emit a tick event after every single low-level statement execution.</li>
<li><strong><code>register_tick_function()</code></strong>: Registers a lightweight watchdog callback evaluated on each tick.</li>
<li><strong><code>microtime(true)</code> Check</strong>: Compares the current timestamp against the initial start time. If the elapsed time exceeds the threshold, it throws a <code>TimeoutException</code>.</li>
<li><strong><code>finally</code> Cleanup</strong>: Always unregisters the tick handler when execution finishes (or errors) to avoid overhead in subsequent operations.</li>
</ol>
<h2>When to Use This Pattern</h2>
<ul>
<li><strong>User-submitted Script Sandboxing</strong>: Enforcing computation budgets on dynamic expressions or formula evaluation.</li>
<li><strong>Cross-Platform CLI Tools</strong>: Running timeout-protected commands on both Windows and Linux without conditional <code>pcntl</code> checks.</li>
<li><strong>Complex Regex Matching</strong>: Preventing catastrophic backtracking (ReDoS) from hanging PHP worker processes.</li>
</ul>
<h2>Summary</h2>
<ul>
<li>Use <code>declare(ticks=1)</code> and <code>register_tick_function()</code> to create a zero-dependency execution timeout watchdog.</li>
<li>Works across all operating systems including Windows where <code>pcntl</code> is unavailable.</li>
<li>Always unregister tick callbacks inside a <code>finally</code> block to prevent memory leaks and unwanted background invocation.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>PHP</category>
            <category>Tooling</category>
            <category>PHP</category>
            <category>Performance</category>
            <category>CLI</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Detect Cumulative Database Query Spikes with whenQueryingForLongerThan()]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-db-when-querying-for-longer-than</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-db-when-querying-for-longer-than</guid>
            <pubDate>Wed, 12 Mar 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Use DB::whenQueryingForLongerThan() to monitor the cumulative time spent in database queries per HTTP request and notify developers of slow requests.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use DB::whenQueryingForLongerThan() to monitor the cumulative time spent in database queries per HTTP request and notify developers of slow requests.</p>
</blockquote>
<p>While <code>DB::listen()</code> measures individual query duration, an endpoint that executes 100 fast 5ms queries spends a massive 500ms in SQL without triggering single-query slow thresholds.</p>
<p><code>DB::whenQueryingForLongerThan()</code> measures the cumulative total database time across an entire request.</p>
<h2>Setting Cumulative Query Thresholds</h2>
<p>In <code>AppServiceProvider::boot()</code>:</p>
<pre><code class="language-php">namespace App\Providers;

use Carbon\CarbonInterval;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Trigger alert if a single request spends more than 500ms in total database queries
        DB::whenQueryingForLongerThan(CarbonInterval::milliseconds(500), function ($connection) {
            Log::warning(&quot;Database query threshold exceeded [{$connection-&gt;totalQueryDuration()}ms]&quot;, [
                &#039;url&#039; =&gt; request()-&gt;fullUrl(),
                &#039;user_id&#039; =&gt; auth()-&gt;id(),
            ]);
        });
    }
}</code></pre><h2>Summary</h2>
<ul>
<li>Measures total aggregate SQL duration per HTTP request lifecycle.</li>
<li>Catches cumulative N+1 query regressions that individual query thresholds miss.</li>
<li>Accepts <code>CarbonInterval</code> instances for clean threshold definitions.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Database</category>
            <category>Laravel</category>
            <category>Database</category>
            <category>Performance</category>
            <category>Monitoring</category>
        </item>
        <item>
            <title><![CDATA[Reusable Tappable Scopes for Eloquent and Laravel Scout Queries]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-tappable-scopes-eloquent-scout</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-tappable-scopes-eloquent-scout</guid>
            <pubDate>Fri, 07 Feb 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Extract query filtering logic into invokable scope classes that work interchangeably across Eloquent query builders and Laravel Scout search instances.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Extract query filtering logic into invokable scope classes that work interchangeably across Eloquent query builders and Laravel Scout search instances.</p>
</blockquote>
<p>Standard Eloquent local scopes (<code>scopeActive()</code>, <code>scopeFilter()</code>) work well for database queries, but they are tightly coupled to the Eloquent <code>Builder</code> instance and cannot be directly invoked on a Laravel Scout search query builder (<code>Post::search()-&gt;...</code>).</p>
<p>By creating invokable &quot;Tappable Scope&quot; classes, you can share identical query constraints across both database queries and full-text search pipelines without duplicating logic.</p>
<h2>1. Create the Invokable Scope Class</h2>
<p>Create a dedicated scope class that accepts either an Eloquent <code>Builder</code> or a Scout <code>Builder</code>:</p>
<pre><code class="language-php">namespace App\Models\Scopes;

use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use LaravelScoutBuilder as ScoutBuilder;

final class PublishedScope
{
    public function __construct(
        private ?User $user = null
    ) {}

    public function __invoke(Builder|ScoutBuilder $query): Builder|ScoutBuilder
    {
        return $query
            -&gt;where(&#039;is_published&#039;, true)
            -&gt;when($this-&gt;user, fn ($q) =&gt; $q-&gt;where(&#039;author_id&#039;, $this-&gt;user-&gt;id));
    }
}</code></pre><h2>2. Using in Model Local Scopes</h2>
<p>Apply the scope class inside your model&#39;s local scope using <code>$query-&gt;tap()</code>:</p>
<pre><code class="language-php">namespace App\Models;

use App\Models\Scopes\PublishedScope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;

class Post extends Model
{
    use Searchable;

    public function scopePublished(Builder $query, ?User $user = null): Builder
    {
        return $query-&gt;tap(new PublishedScope($user));
    }
}</code></pre><h2>3. Using in Laravel Scout Search Queries</h2>
<p>The same scope class can be piped directly into Scout search queries via <code>-&gt;tap()</code>:</p>
<pre><code class="language-php">use App\Models\Post;
use App\Models\Scopes\PublishedScope;

// Search posts with the same publishing constraints applied
$results = Post::search($searchTerm)
    -&gt;tap(new PublishedScope(auth()-&gt;user()))
    -&gt;get();</code></pre><h2>Why Use Tappable Scopes?</h2>
<ul>
<li><strong>Dual Compatibility</strong>: Operates directly across both standard Eloquent queries and Laravel Scout search pipelines.</li>
<li><strong>Cross-Model Reuse</strong>: Share identical filtering rules across multiple models (e.g. <code>Post</code>, <code>Article</code>, <code>Video</code>) without bloated traits.</li>
<li><strong>Cleaner Models</strong>: Keeps model files lightweight by extracting complex filtering criteria into dedicated, testable classes.</li>
</ul>
<h2>Summary</h2>
<ul>
<li>Use invokable scope classes paired with <code>-&gt;tap()</code> to share query logic between Eloquent and Scout.</li>
<li>Keeps search filtering synchronized with database query scopes.</li>
<li>Eliminates duplicate query logic and keeps model classes lean.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Scout</category>
            <category>Architecture</category>
            <category>DRY</category>
        </item>
        <item>
            <title><![CDATA[Ditch sleep() in Tests: Use Laravel Time Travel Helpers]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-test-time-travel-freeze</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-test-time-travel-freeze</guid>
            <pubDate>Thu, 12 Sep 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Use $this->travelTo() or freezeTime() in test suites to test date-sensitive logic without slowing down test execution with sleep().]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use $this-&gt;travelTo() or freezeTime() in test suites to test date-sensitive logic without slowing down test execution with sleep().</p>
</blockquote>
<p>Using sleep() in tests slows down execution suites significantly. Laravel&#39;s time travel helpers let you manipulate Carbon&#39;s internal clock instantaneously without real-world delays.</p>
<pre><code class="language-php">test(&#039;trial expires after 14 days&#039;, function () {
    $user = User::factory()-&gt;create([&#039;trial_ends_at&#039; =&gt; now()-&gt;addDays(14)]);

    // Instantly jump 15 days into the future
    $this-&gt;travel(15)-&gt;days();

    expect($user-&gt;fresh()-&gt;hasExpiredTrial())-&gt;toBeTrue();
});</code></pre><ul>
<li>Replaces slow real-time sleep() calls with instant mock clock jumps</li>
<li>travel(15)-&gt;days() moves time forward dynamically</li>
<li>freezeTime() locks current time to prevent test flickering</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Configuration</category>
            <category>Laravel</category>
            <category>Testing</category>
            <category>Time</category>
        </item>
        <item>
            <title><![CDATA[Integer Enums: Start Indexing from 1, Avoid 0]]></title>
            <link>https://mrpunyapal.dev/tips/php-backed-integer-enums-indexing</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/php-backed-integer-enums-indexing</guid>
            <pubDate>Sat, 31 Aug 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[When creating integer-backed PHP Enums, index starting from 1 to avoid false-y evaluation bugs in loose comparisons.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>When creating integer-backed PHP Enums, index starting from 1 to avoid false-y evaluation bugs in loose comparisons.</p>
</blockquote>
<p>In PHP, <code>0</code> evaluates as false-y in loose condition checks like <code>empty()</code> or <code>if ($enum-&gt;value)</code>. Starting integer enum values at <code>1</code> prevents accidental false-y evaluation bugs.</p>
<hr>
<h2>Before &amp; After</h2>
<pre><code class="language-php">// ❌ Dangerous: 0 evaluates to false in loose checks
enum Priority: int
{
    case Low = 0;
    case Medium = 1;
    case High = 2;
}

// empty(Priority::Low-&gt;value) is true!

// ✅ Recommended: Start indexing at 1
enum Priority: int
{
    case Low = 1;
    case Medium = 2;
    case High = 3;
}

// empty(Priority::Low-&gt;value) is false!</code></pre><hr>
<h2>Key Benefits</h2>
<ul>
<li><strong>Prevents False-y Pitfalls</strong>: Avoids unexpected truthy/falsy evaluation bugs when inspecting raw enum integer values.</li>
<li><strong>Database Consistency</strong>: Aligns naturally with standard 1-based auto-incrementing database ID conventions.</li>
<li><strong>Predictable Empty Checks</strong>: Ensures <code>empty()</code> or <code>blank()</code> checks on enum values behave intuitively without silent failures.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>PHP</category>
            <category>Basics</category>
            <category>PHP</category>
            <category>Enums</category>
            <category>Best Practices</category>
        </item>
        <item>
            <title><![CDATA[Typed Arrays and Array Shapes in PHP with PHPDoc]]></title>
            <link>https://mrpunyapal.dev/tips/php-array-shapes-phpdoc</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/php-array-shapes-phpdoc</guid>
            <pubDate>Thu, 25 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Document and enforce structured array schemas using PHPDoc array shapes, lists, and non-empty array annotations for static analysis in PHPStan and Psalm.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Document and enforce structured array schemas using PHPDoc array shapes, lists, and non-empty array annotations for static analysis in PHPStan and Psalm.</p>
</blockquote>
<p>PHP arrays are versatile: they function as zero-indexed lists, associative maps, and nested dictionary structures. However, using native typehints like <code>function process(array $data)</code> leaves the contents of the array untyped, leading to potential missing key errors or unexpected values at runtime.</p>
<p>By using PHPDoc array shapes and pseudo-types, static analysis tools (like PHPStan and Psalm) and modern IDEs (such as PhpStorm and VS Code) can strictly validate array keys and types without requiring dedicated Data Transfer Objects (DTOs) for every small payload.</p>
<hr>
<h2>1. Defining Exact Array Shapes (<code>array{...}</code>)</h2>
<p>Array shapes define the exact dictionary schema of an associative array, including expected keys and their individual types:</p>
<pre><code class="language-php">/**
 * @param array{id: int, name: string, email: string, is_active?: bool} $user
 * @return array{status: string, timestamp: int}
 */
function registerUser(array $user): array
{
    // PHPStan ensures $user[&#039;id&#039;], $user[&#039;name&#039;], and $user[&#039;email&#039;] exist
    $status = $user[&#039;is_active&#039;] ?? true ? &#039;active&#039; : &#039;pending&#039;;

    return [
        &#039;status&#039; =&gt; $status,
        &#039;timestamp&#039; =&gt; time(),
    ];
}</code></pre><ul>
<li><strong>Required Keys</strong>: <code>id: int, name: string</code></li>
<li><strong>Optional Keys (<code>?</code>)</strong>: <code>is_active?: bool</code> (the key may or may not be present in the array)</li>
</ul>
<hr>
<h2>2. Zero-Indexed Lists (<code>list&lt;T&gt;</code>)</h2>
<p>Standard PHP arrays can have arbitrary or sparse integer keys. When an array is guaranteed to be a contiguous, zero-indexed numerical list (<code>0, 1, 2, ...</code>), use <code>list&lt;T&gt;</code>:</p>
<pre><code class="language-php">/**
 * @param list&lt;int&gt; $scores
 * @return float
 */
function calculateAverage(array $scores): float
{
    if ($scores === []) {
        return 0.0;
    }

    return array_sum($scores) / count($scores);
}

// Valid list
calculateAverage([90, 85, 95]);

// PHPStan flags non-list associative arrays
calculateAverage([&#039;first&#039; =&gt; 90, &#039;second&#039; =&gt; 85]); // Error</code></pre><hr>
<h2>3. Associative Key-Value Maps (<code>array&lt;K, V&gt;</code>)</h2>
<p>When working with dynamic key-value maps (such as configuration options or lookup tables):</p>
<pre><code class="language-php">/**
 * @param array&lt;string, mixed&gt; $payload
 * @return string
 */
function serializePayload(array $payload): string
{
    return json_encode($payload, JSON_THROW_ON_ERROR);
}

/**
 * @param array&lt;int, User&gt; $usersById
 */
function notifyUsers(array $usersById): void
{
    foreach ($usersById as $id =&gt; $user) {
        // $id is typed as int, $user is typed as User
        $user-&gt;sendNotification();
    }
}</code></pre><hr>
<h2>4. Non-Empty Arrays (<code>non-empty-array&lt;T&gt;</code>)</h2>
<p>If a function requires at least one element to prevent division-by-zero or empty-set errors, declare it with <code>non-empty-array</code>:</p>
<pre><code class="language-php">/**
 * @param non-empty-array&lt;string&gt; $recipients
 * @return string
 */
function getPrimaryRecipient(array $recipients): string
{
    // Guaranteed to contain at least 1 element; safe from undefined offset
    return $recipients[0];
}</code></pre><hr>
<h2>5. Typed Callback Arrays (<code>array&lt;callable&gt;</code>)</h2>
<p>When storing or dispatching collections of closures or invokable handlers:</p>
<pre><code class="language-php">/**
 * @param list&lt;callable(): void&gt; $listeners
 */
function triggerListeners(array $listeners): void
{
    foreach ($listeners as $listener) {
        $listener();
    }
}

triggerListeners([
    fn () =&gt; logger()-&gt;info(&#039;First listener executed&#039;),
    fn () =&gt; logger()-&gt;info(&#039;Second listener executed&#039;),
]);</code></pre><hr>
<h2>Summary</h2>
<ul>
<li>Use <code>array{key: type}</code> to define explicit key requirements and prevent missing array index bugs.</li>
<li>Use <code>list&lt;T&gt;</code> for contiguous, zero-indexed sequential lists.</li>
<li>Use <code>array&lt;KeyType, ValueType&gt;</code> for generic maps and associative containers.</li>
<li>Use <code>non-empty-array&lt;T&gt;</code> to statically assert that an array contains at least one item.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>PHP</category>
            <category>Tooling</category>
            <category>PHP</category>
            <category>PHPStan</category>
            <category>Type Safety</category>
            <category>Arrays</category>
            <category>Tooling</category>
        </item>
        <item>
            <title><![CDATA[Generic Classes and Functions in PHP Using PHPDoc @template]]></title>
            <link>https://mrpunyapal.dev/tips/php-generics-phpdoc-template</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/php-generics-phpdoc-template</guid>
            <pubDate>Thu, 25 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Implement type-safe generic collections, wrapper classes, and utility functions in PHP using PHPDoc template annotations for static analysis engines.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Implement type-safe generic collections, wrapper classes, and utility functions in PHP using PHPDoc template annotations for static analysis engines.</p>
</blockquote>
<p>While PHP does not have native language-level generic syntax (like <code>class Collection&lt;T&gt;</code>), static analysis tools such as PHPStan and Psalm provide full generic support via <code>@template</code> annotations.</p>
<p>By annotating wrapper classes and helper functions with <code>@template</code>, you preserve exact object types across transformations and collections without resorting to ambiguous <code>mixed</code> or <code>object</code> returns.</p>
<hr>
<h2>1. Generic Collections and Repositories</h2>
<p>Annotate a class with <code>@template T</code> so the items stored inside match the items returned:</p>
<pre><code class="language-php">/**
 * @template T
 */
class Collection
{
    /** @var list&lt;T&gt; */
    private array $items = [];

    /**
     * @param T $item
     */
    public function add(mixed $item): void
    {
        $this-&gt;items[] = $item;
    }

    /**
     * @return list&lt;T&gt;
     */
    public function all(): array
    {
        return $this-&gt;items;
    }

    /**
     * @return T|null
     */
    public function first(): mixed
    {
        return $this-&gt;items[0] ?? null;
    }
}</code></pre><h3>Usage with Type Preservation</h3>
<pre><code class="language-php">/** @var Collection&lt;User&gt; $userCollection */
$userCollection = new Collection();
$userCollection-&gt;add(new User(&#039;Alice&#039;));

// IDE and PHPStan know $user is an instance of User
$user = $userCollection-&gt;first();
$userName = $user?-&gt;name;</code></pre><hr>
<h2>2. Generic Type Constraints (<code>@template T of BaseClass</code>)</h2>
<p>You can restrict a generic type parameter to a specific class, interface, or union type using the <code>of</code> keyword:</p>
<pre><code class="language-php">abstract class Animal
{
    abstract public function speak(): string;
}

class Dog extends Animal
{
    public function speak(): string { return &#039;Woof&#039;; }
}

class Cat extends Animal
{
    public function speak(): string { return &#039;Meow&#039;; }
}

/**
 * @template T of Animal
 */
class Shelter
{
    /** @var list&lt;T&gt; */
    private array $residents = [];

    /**
     * @param T $animal
     */
    public function admit(Animal $animal): void
    {
        $this-&gt;residents[] = $animal;
    }

    /**
     * @return list&lt;T&gt;
     */
    public function getResidents(): array
    {
        return $this-&gt;residents;
    }
}</code></pre><hr>
<h2>3. Generic Functions and Utility Methods</h2>
<p>Generic functions capture the type passed in arguments and return that exact same type:</p>
<pre><code class="language-php">/**
 * @template T
 * @param non-empty-array&lt;T&gt; $items
 * @return T
 */
function getFirstItem(array $items): mixed
{
    return $items[array_key_first($items)];
}

// Inferred return type: int
$number = getFirstItem([10, 20, 30]);

// Inferred return type: Product
$product = getFirstItem([new Product(&#039;Keyboard&#039;), new Product(&#039;Mouse&#039;)]);</code></pre><hr>
<h2>4. Generic Factory Methods with <code>class-string&lt;T&gt;</code></h2>
<p>When instantiating or resolving classes dynamically by class name, combine <code>@template T</code> with <code>class-string&lt;T&gt;</code>:</p>
<pre><code class="language-php">/**
 * @template T of object
 * @param class-string&lt;T&gt; $className
 * @return T
 */
function createInstance(string $className): object
{
    return new $className();
}

// $service is statically typed as PaymentGateway, with full autocomplete
$service = createInstance(PaymentGateway::class);</code></pre><hr>
<h2>Summary</h2>
<ul>
<li>Use <code>@template T</code> on classes and functions to capture dynamic types and prevent type degradation to <code>mixed</code>.</li>
<li>Use <code>@template T of Constraint</code> to enforce class hierarchy or interface boundaries on generic parameters.</li>
<li>Combine <code>@template T</code> with <code>class-string&lt;T&gt;</code> for strongly typed factory and dependency injection methods.</li>
<li>Enables compile-time type safety across complex collections and architectures.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>PHP</category>
            <category>Tooling</category>
            <category>PHP</category>
            <category>Generics</category>
            <category>PHPStan</category>
            <category>Type Safety</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Complete Guide to PHPStan Static Analysis Levels (0 to 10)]]></title>
            <link>https://mrpunyapal.dev/tips/phpstan-static-analysis-levels-guide</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/phpstan-static-analysis-levels-guide</guid>
            <pubDate>Thu, 25 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Understand what each rule level in PHPStan inspects, from basic syntax errors at Level 0 to strict mixed-type enforcement at Level 9 and Level 10.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Understand what each rule level in PHPStan inspects, from basic syntax errors at Level 0 to strict mixed-type enforcement at Level 9 and Level 10.</p>
</blockquote>
<p>PHPStan inspects your PHP code without executing it, discovering subtle type mismatches, dead code, and unhandled edge cases. PHPStan organizes its rule sets into numeric levels ranging from <strong>0 (most lenient)</strong> to <strong>10 (maximum strictness)</strong>.</p>
<p>When adopting PHPStan in existing or greenfield projects, understanding what each level verifies helps you establish progressive static analysis milestones.</p>
<h2>Overview of PHPStan Levels</h2>
<table>
<thead>
<tr>
<th>Level</th>
<th>Focus Area</th>
<th>What It Detects</th>
</tr>
</thead>
<tbody><tr>
<td><strong>0</strong></td>
<td>Basic Checks</td>
<td>Unknown classes, non-existent methods, wrong argument counts</td>
</tr>
<tr>
<td><strong>1</strong></td>
<td>Undefined Variables</td>
<td>Variables that might be undefined in certain code branches</td>
</tr>
<tr>
<td><strong>2</strong></td>
<td>Unknown Methods &amp; Properties</td>
<td>Validates methods/properties on all expressions and PHPDoc types</td>
</tr>
<tr>
<td><strong>3</strong></td>
<td>Return &amp; Property Types</td>
<td>Ensures return types and property assignments match declared types</td>
</tr>
<tr>
<td><strong>4</strong></td>
<td>Dead Code Checks</td>
<td>Unreachable <code>else</code> branches, redundant <code>instanceof</code> checks</td>
</tr>
<tr>
<td><strong>5</strong></td>
<td>Function &amp; Method Arguments</td>
<td>Verifies argument types passed to methods against signatures</td>
</tr>
<tr>
<td><strong>6</strong></td>
<td>Missing Typehints</td>
<td>Reports missing parameter and return type declarations</td>
</tr>
<tr>
<td><strong>7</strong></td>
<td>Union Type Accuracy</td>
<td>Detects partially wrong union types (calling a method only on one variant)</td>
</tr>
<tr>
<td><strong>8</strong></td>
<td>Nullable Type Safety</td>
<td>Flags method calls and property accesses on potentially null values</td>
</tr>
<tr>
<td><strong>9</strong></td>
<td>Strict <code>mixed</code> Evaluation</td>
<td>Prohibits calling methods or accessing properties on <code>mixed</code> without narrowing</td>
</tr>
<tr>
<td><strong>10</strong></td>
<td>Strict Explicit <code>mixed</code> Types</td>
<td>Prohibits passing or returning <code>mixed</code> where specific types are expected</td>
</tr>
</tbody></table>
<hr>
<h2>Level-by-Level Code Examples</h2>
<h3>Level 0: Basic Syntax &amp; Function Calls</h3>
<p>Verifies that called classes, functions, and methods exist, and that required parameter counts are satisfied:</p>
<pre><code class="language-php">class Example
{
    public function run(): void
    {
        $this-&gt;unknownMethod(); // Error: Call to an undefined method
    }
}

function calculate(int $a, int $b): int
{
    return $a + $b;
}

calculate(1); // Error: Function calculate() called with 1 parameter, 2 required.</code></pre><h3>Level 1: Possibly Undefined Variables</h3>
<p>Ensures every variable accessed is guaranteed to be defined in all branching paths:</p>
<pre><code class="language-php">function process(bool $flag): void
{
    if ($flag) {
        $data = &#039;ready&#039;;
    }

    echo $data; // Error: Variable $data might not be defined.
}</code></pre><h3>Level 2: Unknown Methods on All Expressions</h3>
<p>Checks methods and properties on expressions where types are known via annotations or reflection:</p>
<pre><code class="language-php">class User
{
    public function getName(): string { return &#039;Alex&#039;; }
}

$user = new User();
$user-&gt;getProfilePhoto(); // Error: Call to an undefined method User::getProfilePhoto()</code></pre><h3>Level 3: Return Types and Property Assignments</h3>
<p>Validates that returned values and assigned properties match their type definitions:</p>
<pre><code class="language-php">class Order
{
    private int $total;

    public function setTotal(string $amount): void
    {
        $this-&gt;total = $amount; // Error: Property Order::$total (int) does not accept string.
    }

    public function getInvoiceNumber(): string
    {
        return 12345; // Error: Method getInvoiceNumber() should return string but returns int.
    }
}</code></pre><h3>Level 4: Dead Code and Redundant Logic</h3>
<p>Detects unreachable statements and conditionals that always evaluate to true or false:</p>
<pre><code class="language-php">function check(User $user): void
{
    if ($user instanceof User) { // Always true
        echo &#039;Valid&#039;;
    }

    return;
    echo &#039;Unreachable&#039;; // Error: Unreachable statement - code above always returns.
}</code></pre><h3>Level 5: Argument Type Validation</h3>
<p>Verifies that arguments passed to functions match the declared parameter types:</p>
<pre><code class="language-php">function setDiscount(int $percentage): void {}

setDiscount(&#039;20&#039;); // Error: Parameter #1 expects int, string given.</code></pre><h3>Level 6: Missing Typehints</h3>
<p>Requires explicit typehints on function parameters, return types, and properties:</p>
<pre><code class="language-php">class Report
{
    // Error: Property Report::$records has no type specified.
    private $records;

    // Error: Method generate() has parameter $options with no type specified.
    public function generate($options) {}
}</code></pre><h3>Level 7: Partially Wrong Union Types</h3>
<p>Flags method calls on union types where only some union members implement the method:</p>
<pre><code class="language-php">class Customer { public function sendInvoice(): void {} }
class Guest {}

function notify(Customer|Guest $recipient): void
{
    // Error: Call to method sendInvoice() on Customer|Guest (Guest does not have method).
    $recipient-&gt;sendInvoice();
}</code></pre><h3>Level 8: Nullable Type Access</h3>
<p>Catches potential &quot;Call to a member function on null&quot; errors:</p>
<pre><code class="language-php">function printLength(?string $input): int
{
    // Error: Cannot call method strlen() with parameter ?string (requires string).
    return strlen($input);
}</code></pre><h3>Level 9: Strict <code>mixed</code> Evaluation</h3>
<p>At Level 9, <code>mixed</code> is treated strictly. You cannot perform operations on <code>mixed</code> values without explicit type narrowing:</p>
<pre><code class="language-php">function formatData(mixed $value): string
{
    // Error: Cannot call method trim() on mixed. Narrow type with is_string() first.
    return trim($value);
}</code></pre><h3>Level 10: Strict Explicit <code>mixed</code> Types (PHPStan 2.0+)</h3>
<p>PHPStan 2.0 introduces Level 10 (or <code>level: max</code>), which goes beyond Level 9 by prohibiting passing <code>mixed</code> values or returning <code>mixed</code> where specific types are expected, ensuring full end-to-end type safety throughout generic containers and arrays:</p>
<pre><code class="language-php">/**
 * @param array&lt;string, mixed&gt; $payload
 */
function processPayload(array $payload): string
{
    // Level 10 Error: Parameter #1 $name of function greet() expects string, mixed given.
    return greet($payload[&#039;name&#039;]);
}

function greet(string $name): string
{
    return &quot;Hello, {$name}!&quot;;
}</code></pre><p>To satisfy Level 10, explicitly assert or validate the type:</p>
<pre><code class="language-php">function processPayload(array $payload): string
{
    if (! isset($payload[&#039;name&#039;]) || ! is_string($payload[&#039;name&#039;])) {
        throw new InvalidArgumentException(&#039;Expected name to be a string.&#039;);
    }

    return greet($payload[&#039;name&#039;]); // Valid at Level 10
}</code></pre><h2>Recommended Adoption Strategy</h2>
<ol>
<li><strong>Start at Level 1 or 2</strong>: Fix baseline undefined variables and non-existent methods in legacy codebases.</li>
<li><strong>Advance to Level 5</strong>: Add argument type verification to prevent runtime TypeError exceptions.</li>
<li><strong>Target Level 8 or Level 10</strong>: Ideal for modern, strongly typed Laravel and PHP applications using Larastan.</li>
</ol>
<h2>Summary</h2>
<ul>
<li>PHPStan levels range progressively from 0 (lenient) to 10 (maximum strictness).</li>
<li>Levels 1 to 5 catch common runtime bugs without requiring exhaustive PHPDoc annotations.</li>
<li>Levels 6 to 8 enforce complete type coverage and nullable safety.</li>
<li>Levels 9 and 10 enforce strict type narrowing on <code>mixed</code> values and array payloads.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>PHP</category>
            <category>Tooling</category>
            <category>PHP</category>
            <category>PHPStan</category>
            <category>Tooling</category>
            <category>Static Analysis</category>
            <category>Type Safety</category>
        </item>
        <item>
            <title><![CDATA[Prevent Information Disclosure with Gate::denyAsNotFound()]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-gate-deny-as-not-found-security</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-gate-deny-as-not-found-security</guid>
            <pubDate>Wed, 17 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Use Response::denyAsNotFound() in policies to return a 404 Not Found response instead of 403 Forbidden, concealing the existence of private resources.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use Response::denyAsNotFound() in policies to return a 404 Not Found response instead of 403 Forbidden, concealing the existence of private resources.</p>
</blockquote>
<p>Returning HTTP 403 Forbidden on confidential resources (such as private repositories, draft invoices, or secret project boards) tells attackers that the resource exists, leaking information.</p>
<p>Laravel policies support <code>Response::denyAsNotFound()</code> to return a 404 response directly from authorization checks.</p>
<h2>Policy Implementation</h2>
<pre><code class="language-php">namespace App\Policies;

use App\Models\Project;
use App\Models\User;
use Illuminate\Auth\Access\Response;

class ProjectPolicy
{
    public function view(User $user, Project $project): Response
    {
        if ($project-&gt;is_confidential &amp;&amp; $project-&gt;owner_id !== $user-&gt;id) {
            // Returns HTTP 404 instead of 403 Forbidden
            return Response::denyAsNotFound(&#039;Project not found.&#039;);
        }

        return Response::allow();
    }
}</code></pre><h2>Summary</h2>
<ul>
<li>Throws a <code>NotFoundHttpException</code> (404) rather than <code>AuthorizationException</code> (403).</li>
<li>Prevents resource enumeration attacks on private URL identifiers.</li>
<li>Keeps authorization logic centralized inside Policy classes.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Authorization</category>
            <category>Security</category>
            <category>HTTP</category>
        </item>
        <item>
            <title><![CDATA[Execute Multi-Step Business Workflows with the Pipeline Facade]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-pipeline-send-multi-step-workflows</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-pipeline-send-multi-step-workflows</guid>
            <pubDate>Wed, 26 Jun 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Use Laravel's Pipeline facade to pass objects through sequential pipe classes, keeping complex order processing and data transformations modular.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use Laravel&#39;s Pipeline facade to pass objects through sequential pipe classes, keeping complex order processing and data transformations modular.</p>
</blockquote>
<p>When an application workflow involves multiple distinct stages (such as validating payment, applying coupons, calculating tax, and generating invoices), stuffing all logic into a single service method creates monolithic, hard-to-test code.</p>
<p>Laravel&#39;s <code>Pipeline</code> pattern executes sequential pipes cleanly.</p>
<h2>Defining Pipe Classes</h2>
<p>Each pipe receives the payload and a <code>$next</code> closure:</p>
<pre><code class="language-php">namespace App\Pipelines\Orders;

use Closure;

class ApplyDiscountCoupon
{
    public function handle(Order $order, Closure $next)
    {
        if ($order-&gt;coupon_code) {
            $order-&gt;discount = CouponService::calculate($order);
        }

        return $next($order);
    }
}</code></pre><h2>Executing the Pipeline</h2>
<pre><code class="language-php">use App\Pipelines\Orders\ApplyDiscountCoupon;
use App\Pipelines\Orders\CalculateTax;
use App\Pipelines\Orders\ProcessStripePayment;
use Illuminate\Support\Facades\Pipeline;

$processedOrder = Pipeline::send($order)
    -&gt;through([
        ApplyDiscountCoupon::class,
        CalculateTax::class,
        ProcessStripePayment::class,
    ])
    -&gt;then(fn ($order) =&gt; $order-&gt;finalize());</code></pre><h2>Summary</h2>
<ul>
<li>Deconstructs complex operations into focused, single-responsibility pipe classes.</li>
<li>Enables easy reordering, adding, or removing of business steps.</li>
<li>Uses standard middleware-style <code>handle($passable, Closure $next)</code> signatures.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Architecture</category>
            <category>Laravel</category>
            <category>Architecture</category>
            <category>Design Patterns</category>
            <category>Clean Code</category>
        </item>
        <item>
            <title><![CDATA[Detect and Eliminate N+1 Queries in Development with preventLazyLoading()]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-eloquent-prevent-lazy-loading-strict-mode</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-eloquent-prevent-lazy-loading-strict-mode</guid>
            <pubDate>Wed, 29 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Enable Model::preventLazyLoading() in local development to automatically throw exceptions whenever a relationship is lazy loaded.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Enable Model::preventLazyLoading() in local development to automatically throw exceptions whenever a relationship is lazy loaded.</p>
</blockquote>
<p>Accidentally accessing relationships inside loops (like <code>$user-&gt;posts</code> without <code>User::with(&#39;posts&#39;)</code>) causes silent N+1 query performance degradation that often goes unnoticed until production traffic spikes.</p>
<p><code>Model::preventLazyLoading()</code> forces Eloquent to throw an exception when lazy loading occurs.</p>
<h2>Enabling in AppServiceProvider</h2>
<pre><code class="language-php">namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Throw exceptions locally; log warnings in production
        Model::preventLazyLoading(! $this-&gt;app-&gt;isProduction());

        // Handle violations in production gracefully
        if ($this-&gt;app-&gt;isProduction()) {
            Model::handleLazyLoadingViolationsUsing(function ($model, $relation) {
                logger()-&gt;warning(&quot;Lazy loading [{$relation}] on [{$model::class}] in production.&quot;);
            });
        }
    }
}</code></pre><h2>What Happens When a Relation is Lazy Loaded</h2>
<pre><code class="language-text">Attempted to lazy load [comments] on model [AppModelsPost] but lazy loading is disabled.</code></pre><h2>Summary</h2>
<ul>
<li>Catches N+1 query performance bugs during development and test suites.</li>
<li>Fails fast with descriptive stack traces pointing directly to the offending blade view or loop.</li>
<li>Supports custom violation handlers for production telemetry.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Performance</category>
            <category>Debugging</category>
        </item>
        <item>
            <title><![CDATA[Refactor PHP Enum Cases from snake_case to PascalCase with Rector]]></title>
            <link>https://mrpunyapal.dev/tips/php-rector-refactor-enums-pascal-case</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/php-rector-refactor-enums-pascal-case</guid>
            <pubDate>Mon, 20 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Automate renaming PHP 8.1+ enum cases from SNAKE_CASE to PascalCase across PHP classes and Blade templates using Rector and an Artisan refactoring script.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Automate renaming PHP 8.1+ enum cases from SNAKE_CASE to PascalCase across PHP classes and Blade templates using Rector and an Artisan refactoring script.</p>
</blockquote>
<p>When PHP 8.1 backed enums were introduced, many projects originally named enum cases in uppercase snake case (<code>case PENDING_REVIEW;</code>), matching older class constant conventions. Modern PHP and Laravel coding style standards (including PER-CS and Laravel Pint) recommend PascalCase for enum cases (<code>case PendingReview;</code>).</p>
<p>Renaming enum cases across a large production application manually is error-prone. By combining a custom Rector rule with an Artisan Blade updater, you can refactor all enum declarations, references, and Blade view usages automatically.</p>
<h2>1. Custom Rector Rule for Enum Cases</h2>
<pre><code class="language-php">namespace App\Rector;

use PhpParser\Node;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt\EnumCase;
use PHPStan\Reflection\ReflectionProvider;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

final class RenameEnumCasesToPascalCaseRector extends AbstractRector
{
    public function __construct(
        private ReflectionProvider $reflectionProvider
    ) {}

    public function getNodeTypes(): array
    {
        return [EnumCase::class, ClassConstFetch::class];
    }

    public function refactor(Node $node): ?Node
    {
        if ($node instanceof EnumCase) {
            return $this-&gt;refactorEnumCase($node);
        }

        if ($node instanceof ClassConstFetch) {
            return $this-&gt;refactorEnumCaseFetch($node);
        }

        return null;
    }

    private function refactorEnumCase(EnumCase $node): ?Node
    {
        $oldName = $this-&gt;getName($node-&gt;name);
        $newName = $this-&gt;convertSnakeToPascalCase($oldName);

        if ($oldName !== $newName) {
            $node-&gt;name = new Identifier($newName);
            return $node;
        }

        return null;
    }

    private function refactorEnumCaseFetch(ClassConstFetch $node): ?Node
    {
        $objectType = $this-&gt;getType($node-&gt;class);

        if ($objectType-&gt;isEnum()-&gt;yes()) {
            $oldName = $this-&gt;getName($node-&gt;name);
            if ($oldName === &#039;class&#039; || $oldName === null) {
                return null;
            }

            $newName = $this-&gt;convertSnakeToPascalCase($oldName);
            if ($oldName !== $newName) {
                $node-&gt;name = new Identifier($newName);
                return $node;
            }
        }

        return null;
    }

    private function convertSnakeToPascalCase(string $name): string
    {
        return str_replace(&#039; &#039;, &#039;&#039;, ucwords(str_replace(&#039;_&#039;, &#039; &#039;, mb_strtolower($name))));
    }

    public function getRuleDefinition(): RuleDefinition
    {
        return new RuleDefinition(&#039;Convert SNAKE_CASE enum cases to PascalCase&#039;, [
            new CodeSample(
                &#039;enum Status { case PENDING_PAYMENT; }&#039;,
                &#039;enum Status { case PendingPayment; }&#039;
            ),
        ]);
    }
}</code></pre><h2>2. Refactoring Blade Templates</h2>
<p>Because Rector parses PHP files rather than Blade templates, use an Artisan command to update enum case usages inside <code>resources/views/**/*.blade.php</code>:</p>
<pre><code class="language-php">namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;

class RefactorBladeEnumsCommand extends Command
{
    protected $signature = &#039;refactor:blade-enums&#039;;
    protected $description = &#039;Refactor enum cases in Blade views from SNAKE_CASE to PascalCase&#039;;

    public function handle(): int
    {
        $views = File::allFiles(resource_path(&#039;views&#039;));

        foreach ($views as $file) {
            if ($file-&gt;getExtension() !== &#039;php&#039;) {
                continue;
            }

            $content = File::get($file-&gt;getRealPath());

            // Match patterns like Status::PENDING_PAYMENT
            $updated = preg_replace_callback(&#039;/([A-Z][A-Za-z0-9]+)::([A-Z0-9_]+)/&#039;, function ($matches) {
                $class = $matches[1];
                $case = $matches[2];

                if ($case === &#039;class&#039;) {
                    return $matches[0];
                }

                $pascalCase = str_replace(&#039; &#039;, &#039;&#039;, ucwords(str_replace(&#039;_&#039;, &#039; &#039;, strtolower($case))));
                return &quot;{$class}::{$pascalCase}&quot;;
            }, $content);

            if ($content !== $updated) {
                File::put($file-&gt;getRealPath(), $updated);
                $this-&gt;line(&quot;Updated view: &quot; . $file-&gt;getRelativePathname());
            }
        }

        $this-&gt;info(&#039;Blade enum refactoring completed.&#039;);
        return Command::SUCCESS;
    }
}</code></pre><h2>Summary</h2>
<ul>
<li>Use Rector to safely refactor Enum definitions and PHP references via AST inspection and PHPStan static reflection.</li>
<li>Run a targeted Blade regex migration script to update template references without manual search-and-replace.</li>
<li>Standardizes your enum architecture to PascalCase conventions across the entire application.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>PHP</category>
            <category>Tooling</category>
            <category>PHP</category>
            <category>Rector</category>
            <category>Enums</category>
            <category>Refactoring</category>
            <category>Blade</category>
            <category>Tooling</category>
        </item>
        <item>
            <title><![CDATA[Automate Laravel 11 casts() Method Upgrade with Rector]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-rector-automate-casts-method-upgrade</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-rector-automate-casts-method-upgrade</guid>
            <pubDate>Sat, 30 Mar 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Use Rector to automatically refactor legacy protected $casts array properties into the modern casts() method across your entire Laravel codebase.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use Rector to automatically refactor legacy protected $casts array properties into the modern casts() method across your entire Laravel codebase.</p>
</blockquote>
<p>Laravel 11 introduced the <code>casts()</code> method on Eloquent models, allowing fluent cast definitions, class references, and method calls inside model classes.</p>
<p>Rector automates converting legacy <code>protected $casts</code> properties to the new method:</p>
<pre><code class="language-diff">1) app/Models/Post.php

- protected $casts = [
-     &#039;tags&#039; =&gt; &#039;array&#039;,
-     &#039;published_at&#039; =&gt; &#039;datetime&#039;,
-     &#039;is_featured&#039; =&gt; FeaturedStatus::class,
- ];

+ protected function casts(): array
+ {
+     return [
+         &#039;tags&#039; =&gt; &#039;array&#039;,
+         &#039;published_at&#039; =&gt; &#039;datetime&#039;,
+         &#039;is_featured&#039; =&gt; FeaturedStatus::class,
+     ];
+ }</code></pre><ul>
<li>Runs across hundreds of models in seconds during framework upgrades</li>
<li>Enables calling static methods directly inside cast definitions (e.g. <code>AsEnumCollection::of(...)</code>)</li>
<li>Eliminates typos in array property names</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Rector</category>
            <category>Tooling</category>
        </item>
        <item>
            <title><![CDATA[Cast Enums Inside JSON and Array Column Keys in Eloquent]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-cast-enum-json-array-key</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-cast-enum-json-array-key</guid>
            <pubDate>Tue, 30 Jan 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Cast enum instances nested within JSON or array database columns using Eloquent Attribute accessors and mutators.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Cast enum instances nested within JSON or array database columns using Eloquent Attribute accessors and mutators.</p>
</blockquote>
<p>Laravel provides first-party enum casting for standard model columns:</p>
<pre><code class="language-php">protected $casts = [
    &#039;status&#039; =&gt; PostStatus::class,
];</code></pre><p>However, when an enum value is stored inside a nested JSON object or array column (such as <code>options-&gt;status</code> or <code>settings[&#39;status&#39;]</code>), Laravel&#39;s default column casting casts the entire column to an array, leaving the nested enum value as a raw string or integer.</p>
<p>Using Eloquent&#39;s <code>Attribute</code> accessor and mutator, you can automatically hydrate and serialize nested enum instances.</p>
<h2>Implementing the Nested Enum Cast</h2>
<pre><code class="language-php">namespace App\Models;

use App\Enums\PostStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected function options(): Attribute
    {
        return Attribute::make(
            get: function ($value): array {
                $options = json_decode($value ?? &#039;[]&#039;, true) ?? [];

                // Hydrate string value to Backed Enum instance
                if (isset($options[&#039;status&#039;])) {
                    $options[&#039;status&#039;] = PostStatus::tryFrom($options[&#039;status&#039;]) ?? $options[&#039;status&#039;];
                }

                return $options;
            },
            set: function ($value): string {
                $options = is_array($value) ? $value : (json_decode($value, true) ?? []);

                // Serialize Backed Enum instance to scalar value before storing
                if (isset($options[&#039;status&#039;]) &amp;&amp; $options[&#039;status&#039;] instanceof PostStatus) {
                    $options[&#039;status&#039;] = $options[&#039;status&#039;]-&gt;value;
                }

                return json_encode($options);
            }
        );
    }
}</code></pre><h2>Practical Usage</h2>
<p>You can now interact with nested enum properties as strongly typed objects:</p>
<pre><code class="language-php">$post = Post::find(1);

// Accessing the nested enum
if ($post-&gt;options[&#039;status&#039;] === PostStatus::Draft) {
    // Strongly typed enum comparison
}

// Updating the nested enum
$options = $post-&gt;options;
$options[&#039;status&#039;] = PostStatus::Published;
$post-&gt;options = $options;
$post-&gt;save();</code></pre><h2>Summary</h2>
<ul>
<li>Use Eloquent <code>Attribute::make()</code> to hydrate and serialize enums nested inside JSON attributes.</li>
<li>Use <code>PostStatus::tryFrom()</code> in the getter to gracefully handle unexpected values without throwing exceptions.</li>
<li>Ensures strongly typed enum consistency even for schemaless or flexible JSON payloads.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Enums</category>
            <category>Casts</category>
            <category>JSON</category>
        </item>
        <item>
            <title><![CDATA[Combine Custom Casts and Enums for Complex Attributes]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-custom-casts-enum-json</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-custom-casts-enum-json</guid>
            <pubDate>Tue, 30 Jan 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Use Eloquent custom casts (CastsAttributes) to handle complex JSON serialization and Backed Enum arrays cleanly.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use Eloquent custom casts (CastsAttributes) to handle complex JSON serialization and Backed Enum arrays cleanly.</p>
</blockquote>
<p>When model attributes contain complex JSON structures or collections of Enums, implement CastsAttributes to handle custom database transformation and object hydration.</p>
<pre><code class="language-php">namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use App\Enums\Permission;

class PermissionCast implements CastsAttributes
{
    public function get($model, string $key, $value, array $attributes): array
    {
        return array_map(fn ($val) =&gt; Permission::from($val), json_decode($value, true) ?? []);
    }

    public function set($model, string $key, $value, array $attributes): string
    {
        return json_encode(array_map(fn ($enum) =&gt; $enum-&gt;value, $value));
    }
}</code></pre><ul>
<li>Implements CastsAttributes with get() and set() transformation signatures</li>
<li>Handles custom JSON encoding and object hydration transparently</li>
<li>Keeps model classes free of manual JSON encoding logic</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Casts</category>
        </item>
        <item>
            <title><![CDATA[Fix Blade View Cache Compilation for Teleport Directives in Livewire]]></title>
            <link>https://mrpunyapal.dev/tips/livewire-teleport-blade-view-cache-directive-fix</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/livewire-teleport-blade-view-cache-directive-fix</guid>
            <pubDate>Sun, 14 Jan 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Prevent view compilation glitches when running php artisan view:cache on templates using @teleport by registering explicit Blade directives.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Prevent view compilation glitches when running php artisan view:cache on templates using @teleport by registering explicit Blade directives.</p>
</blockquote>
<p>When using Livewire or Alpine.js <code>@teleport</code> directives inside Blade templates, running <code>php artisan view:cache</code> in production or CI/CD pipelines can occasionally cause compilation errors or unparsed tags if custom directive compilation is missing from the service container.</p>
<p>You can ensure consistent, error-free view compilation by registering custom Blade compiler directives for <code>@teleport</code> and <code>@endteleport</code>.</p>
<h2>The Fix in AppServiceProvider</h2>
<p>Add the custom directive registration inside your <code>AppServiceProvider::register()</code> or <code>boot()</code> method:</p>
<pre><code class="language-php">namespace App\Providers;

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Blade::directive(&#039;teleport&#039;, function (string $expression): string {
            return &quot;&lt;template x-teleport=&quot;&lt;?php echo e({$expression}); ?&gt;&quot;&gt;&quot;;
        });

        Blade::directive(&#039;endteleport&#039;, function (): string {
            return &quot;&lt;/template&gt;&quot;;
        });
    }
}</code></pre><h2>Usage in Blade Views</h2>
<p>You can now use <code>@teleport</code> cleanly across cached views:</p>
<pre><code class="language-blade">&lt;div x-data=&quot;{ open: false }&quot;&gt;
    &lt;button @click=&quot;open = true&quot;&gt;Open Modal&lt;/button&gt;

    @teleport(&#039;body&#039;)
        &lt;div x-show=&quot;open&quot; class=&quot;modal-backdrop&quot;&gt;
            &lt;div class=&quot;modal-content&quot;&gt;
                &lt;h3&gt;Modal Title&lt;/h3&gt;
                &lt;button @click=&quot;open = false&quot;&gt;Close&lt;/button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    @endteleport
&lt;/div&gt;</code></pre><h2>Testing View Caching</h2>
<p>Run view cache compilation to verify:</p>
<pre><code class="language-bash">php artisan view:clear
php artisan view:cache</code></pre><p>All views compile cleanly into cached PHP templates without syntax exceptions.</p>
<h2>Summary</h2>
<ul>
<li>Register explicit <code>@teleport</code> and <code>@endteleport</code> Blade directives in <code>AppServiceProvider</code> to resolve compilation glitches during <code>view:cache</code>.</li>
<li>Translates cleanly into standard Alpine.js <code>&lt;template x-teleport=&quot;...&quot;&gt;</code> elements.</li>
<li>Prevents production deployment build failures when caching views.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Livewire</category>
            <category>Components</category>
            <category>Laravel</category>
            <category>Livewire</category>
            <category>Blade</category>
            <category>Alpine.js</category>
            <category>Tooling</category>
        </item>
        <item>
            <title><![CDATA[Centralized Application Activity Logging with Laravel Event Subscribers]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-activity-log-event-subscriber</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-activity-log-event-subscriber</guid>
            <pubDate>Fri, 01 Dec 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Capture HTTP requests, Artisan CLI commands, and outgoing API responses into a unified audit trail using a single Laravel Event Subscriber.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Capture HTTP requests, Artisan CLI commands, and outgoing API responses into a unified audit trail using a single Laravel Event Subscriber.</p>
</blockquote>
<p>Applications frequently need to log key lifecycle actions for debugging and compliance. Instead of scattering logging logic across various middleware, commands, and service classes, Laravel Event Subscribers let you listen to multiple framework events from a single centralized class.</p>
<h2>The ActivityLogEventSubscriber</h2>
<pre><code class="language-php">namespace App\Listeners;

use Illuminate\Console\Events\CommandFinished;
use Illuminate\Events\Dispatcher;
use Illuminate\Foundation\Http\Events\RequestHandled;
use Illuminate\Http\Client\Events\ResponseReceived;
use Illuminate\Support\Facades\Log;

class ActivityLogEventSubscriber
{
    public function handleHttpRequest(RequestHandled $event): void
    {
        $this-&gt;logActivity(&#039;RequestHandled&#039;, [
            &#039;method&#039; =&gt; $event-&gt;request-&gt;method(),
            &#039;url&#039; =&gt; $event-&gt;request-&gt;fullUrl(),
            &#039;status&#039; =&gt; $event-&gt;response-&gt;getStatusCode(),
            &#039;user_id&#039; =&gt; $event-&gt;request-&gt;user()?-&gt;id,
        ]);
    }

    public function handleConsoleCommand(CommandFinished $event): void
    {
        $this-&gt;logActivity(&#039;CommandFinished&#039;, [
            &#039;command&#039; =&gt; $event-&gt;command,
            &#039;exit_code&#039; =&gt; $event-&gt;exitCode,
        ]);
    }

    public function handleHttpClientResponse(ResponseReceived $event): void
    {
        $this-&gt;logActivity(&#039;HttpClientResponse&#039;, [
            &#039;url&#039; =&gt; (string) $event-&gt;request-&gt;url(),
            &#039;status&#039; =&gt; $event-&gt;response-&gt;status(),
        ]);
    }

    private function logActivity(string $eventType, array $payload): void
    {
        Log::channel(&#039;activity&#039;)-&gt;info(&quot;Activity [{$eventType}]&quot;, $payload);
    }

    public function subscribe(Dispatcher $events): array
    {
        return [
            RequestHandled::class =&gt; &#039;handleHttpRequest&#039;,
            CommandFinished::class =&gt; &#039;handleConsoleCommand&#039;,
            ResponseReceived::class =&gt; &#039;handleHttpClientResponse&#039;,
        ];
    }
}</code></pre><h2>Registering the Subscriber</h2>
<p>In Laravel 11+, register the subscriber inside your <code>AppServiceProvider::boot()</code> method (or <code>EventServiceProvider</code> in earlier versions):</p>
<pre><code class="language-php">namespace App\Providers;

use App\Listeners\ActivityLogEventSubscriber;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Event::subscribe(ActivityLogEventSubscriber::class);
    }
}</code></pre><h2>Summary</h2>
<ul>
<li>Use an Event Subscriber to group related event handlers into a single cohesive class.</li>
<li>Listen to built-in framework events like <code>RequestHandled</code>, <code>CommandFinished</code>, and <code>ResponseReceived</code> for comprehensive audit logging.</li>
<li>Decouples monitoring and logging logic from core HTTP controllers and console commands.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Events</category>
            <category>Laravel</category>
            <category>Events</category>
            <category>Logging</category>
            <category>Architecture</category>
            <category>Audit</category>
        </item>
        <item>
            <title><![CDATA[Efficient Unique Slug Generation in Eloquent Without Looping Queries]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-generate-unique-slug-without-looping</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-generate-unique-slug-without-looping</guid>
            <pubDate>Wed, 29 Nov 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Generate unique database slugs in Eloquent by querying the maximum existing numerical suffix directly instead of executing repeated queries in a while loop.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Generate unique database slugs in Eloquent by querying the maximum existing numerical suffix directly instead of executing repeated queries in a while loop.</p>
</blockquote>
<p>When generating URL slugs for blog posts, products, or articles, collisions occur when two records share the same title (<code>&quot;My First Post&quot;</code>).</p>
<p>A frequent implementation uses a <code>while</code> loop that repeatedly queries the database (<code>post-1</code>, <code>post-2</code>, <code>post-3</code>, ...) until an available slug is found. On busy applications with many duplicate titles, this pattern generates dozens of sequential database roundtrips for a single record creation.</p>
<p>You can determine the next unique slug in a single query by inspecting the maximum numeric suffix using SQL functions.</p>
<h2>The Efficient Implementation</h2>
<pre><code class="language-php">namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

class Article extends Model
{
    protected static function booted(): void
    {
        static::creating(function (Article $article): void {
            if (empty($article-&gt;slug)) {
                $article-&gt;slug = static::generateUniqueSlug($article-&gt;title);
            }
        });
    }

    public static function generateUniqueSlug(string $title): string
    {
        $baseSlug = str($title)-&gt;slug()-&gt;value();

        // Check if base slug is already available
        if (static::where(&#039;slug&#039;, $baseSlug)-&gt;doesntExist()) {
            return $baseSlug;
        }

        // Query the maximum existing numeric suffix in a single query
        $maxSuffix = static::query()
            -&gt;where(&#039;slug&#039;, &#039;LIKE&#039;, &quot;{$baseSlug}-%&quot;)
            -&gt;max(DB::raw(&#039;CAST(SUBSTRING_INDEX(slug, &quot;-&quot;, -1) AS SIGNED)&#039;));

        if ($maxSuffix === null || $maxSuffix &lt;= 0) {
            return &quot;{$baseSlug}-2&quot;;
        }

        return &quot;{$baseSlug}-&quot; . ($maxSuffix + 1);
    }
}</code></pre><h2>How It Works</h2>
<ol>
<li><strong>Initial Availability Check</strong>: Checks if the clean base slug (<code>&quot;laravel-tips&quot;</code>) exists. If available, it returns immediately with 1 simple indexed lookup.</li>
<li><strong><code>SUBSTRING_INDEX</code> Extraction</strong>: For duplicate titles, MySQL&#39;s <code>SUBSTRING_INDEX(slug, &quot;-&quot;, -1)</code> extracts the trailing characters after the last hyphen.</li>
<li><strong><code>CAST(... AS SIGNED)</code></strong>: Converts the extracted substring into an integer so the <code>MAX()</code> aggregate computes the numerical maximum rather than alphabetical sorting.</li>
<li><strong>Calculated Next Suffix</strong>: Increments the maximum found suffix (<code>max + 1</code>), guaranteeing uniqueness without sequential trial-and-error queries.</li>
</ol>
<h2>Performance Comparison</h2>
<ul>
<li><strong>While Loop Approach</strong>: Runs (N) database queries (where (N) is the number of existing collisions).</li>
<li><strong>Direct Max Calculation</strong>: Always runs exactly 2 queries regardless of whether 2 or 2,000 colliding records exist.</li>
</ul>
<h2>Summary</h2>
<ul>
<li>Avoid <code>while (Model::whereSlug(...)-&gt;exists())</code> loops that create unpredictable N+1 database roundtrips.</li>
<li>Use SQL string extraction and integer casting to retrieve the maximum numerical suffix in a single lookup.</li>
<li>Guarantees fast, predictable execution time during bulk imports and high-traffic record creation.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Database</category>
            <category>Performance</category>
            <category>Clean Code</category>
        </item>
        <item>
            <title><![CDATA[Freeze and Inspect Application Time in Tests with Carbon setTestNow()]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-carbon-set-test-now-time-manipulation</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-carbon-set-test-now-time-manipulation</guid>
            <pubDate>Thu, 23 Nov 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Control system time during testing and development using Carbon::setTestNow() and inspect active time mocks with Carbon::hasTestNow().]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Control system time during testing and development using Carbon::setTestNow() and inspect active time mocks with Carbon::hasTestNow().</p>
</blockquote>
<p>When writing unit tests or developing features that rely on temporal calculations (such as subscription renewals, birthday promotions, trial expirations, or grace periods), relying on real system time creates flaky tests that fail depending on the time of day or month.</p>
<p>Carbon provides <code>setTestNow()</code> to lock time to a specific instant, alongside <code>hasTestNow()</code> to verify whether the temporal state is currently mocked.</p>
<h2>Freezing Time in Feature Logic</h2>
<pre><code class="language-php">use Carbon\Carbon;

function isBirthdayOfferActive(Carbon $birthday): bool
{
    return Carbon::now()-&gt;isSameDay($birthday);
}

// Check without mock time
$userBirthday = Carbon::create(1995, 11, 23);
isBirthdayOfferActive($userBirthday); // Evaluates against real current date

// Freeze application time to a specific target date
Carbon::setTestNow(Carbon::create(2026, 11, 23));

isBirthdayOfferActive($userBirthday); // Returns true!

// Reset back to real system time
Carbon::setTestNow(null);</code></pre><h2>Checking if Mock Time is Active with <code>hasTestNow()</code></h2>
<p>In custom debug bars, health checks, or safety middleware, you can inspect whether the application is running under a simulated timestamp:</p>
<pre><code class="language-php">use Carbon\Carbon;

if (Carbon::hasTestNow()) {
    logger()-&gt;warning(&#039;Application running with mocked Carbon time: &#039; . Carbon::now()-&gt;toDateTimeString());
}</code></pre><h2>Laravel Test Helpers Under the Hood</h2>
<p>Laravel&#39;s built-in testing helpers (<code>$this-&gt;travelTo()</code>, <code>$this-&gt;freezeTime()</code>) interact directly with <code>Carbon::setTestNow()</code>:</p>
<pre><code class="language-php">test(&#039;trial expires after 14 days&#039;, function () {
    $this-&gt;freezeTime();

    $user = User::factory()-&gt;create([&#039;trial_ends_at&#039; =&gt; now()-&gt;addDays(14)]);
    expect($user-&gt;hasExpiredTrial())-&gt;toBeFalse();

    // Travel forward 15 days
    $this-&gt;travel(15)-&gt;days();

    expect($user-&gt;hasExpiredTrial())-&gt;toBeTrue();
});</code></pre><h2>Summary</h2>
<ul>
<li>Use <code>Carbon::setTestNow()</code> to freeze time during test executions and verify time-dependent edge cases reliably.</li>
<li>Always reset mock time with <code>Carbon::setTestNow(null)</code> in test teardown hooks if not using Laravel&#39;s test time helpers.</li>
<li>Use <code>Carbon::hasTestNow()</code> to check if time manipulation is currently active.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Testing</category>
            <category>Laravel</category>
            <category>Testing</category>
            <category>Carbon</category>
            <category>PHP</category>
        </item>
        <item>
            <title><![CDATA[Stream CSV Report Downloads with Renderless Livewire Component Actions]]></title>
            <link>https://mrpunyapal.dev/tips/livewire-renderless-export-csv-reports</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/livewire-renderless-export-csv-reports</guid>
            <pubDate>Mon, 06 Nov 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Use Livewire 3's #[Renderless] attribute to stream file downloads directly from component actions without triggering unnecessary view re-renders or database queries.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use Livewire 3&#39;s #[Renderless] attribute to stream file downloads directly from component actions without triggering unnecessary view re-renders or database queries.</p>
</blockquote>
<p>When triggering a file download or CSV export from a Livewire component, Livewire&#39;s default lifecycle runs the component&#39;s <code>render()</code> method after executing the action.</p>
<p>If your <code>render()</code> method executes complex database queries or chart calculations, running <code>render()</code> during a binary file download wastes server CPU and memory since the HTML response is discarded in favor of the download stream.</p>
<p>Livewire 3 provides the <code>#[Renderless]</code> attribute to execute an action without invoking <code>render()</code>.</p>
<h2>The Livewire Component</h2>
<pre><code class="language-php">namespace App\Livewire;

use App\Models\Report;
use Livewire\Attributes\Renderless;
use Livewire\Component;
use Symfony\Component\HttpFoundation\StreamedResponse;

class TransactionReports extends Component
{
    public string $startDate = &#039;&#039;;
    public string $endDate = &#039;&#039;;
    public array $selectedColumns = [&#039;id&#039;, &#039;reference&#039;, &#039;amount&#039;, &#039;created_at&#039;];

    #[Renderless]
    public function exportCsv(): StreamedResponse
    {
        return response()-&gt;streamDownload(function (): void {
            $output = fopen(&#039;php://output&#039;, &#039;w&#039;);

            // Write CSV header
            fputcsv($output, $this-&gt;selectedColumns);

            // Stream records in chunks
            Report::query()
                -&gt;when($this-&gt;startDate, fn ($q) =&gt; $q-&gt;where(&#039;created_at&#039;, &#039;&gt;=&#039;, $this-&gt;startDate))
                -&gt;when($this-&gt;endDate, fn ($q) =&gt; $q-&gt;where(&#039;created_at&#039;, &#039;&lt;=&#039;, $this-&gt;endDate))
                -&gt;select($this-&gt;selectedColumns)
                -&gt;chunk(500, function ($rows) use ($output): void {
                    foreach ($rows as $row) {
                        fputcsv($output, $row-&gt;toArray());
                    }
                });

            fclose($output);
        }, &#039;reports.csv&#039;, [
            &#039;Content-Type&#039; =&gt; &#039;text/csv&#039;,
        ]);
    }

    public function render()
    {
        // Heavy query executed ONLY when rendering HTML UI, NOT during CSV export
        return view(&#039;livewire.transaction-reports&#039;, [
            &#039;reports&#039; =&gt; Report::latest()-&gt;paginate(25),
        ]);
    }
}</code></pre><h2>In the Blade Template</h2>
<pre><code class="language-blade">&lt;div&gt;
    &lt;button wire:click=&quot;exportCsv&quot; class=&quot;btn btn-secondary&quot;&gt;
        Export CSV
    &lt;/button&gt;
&lt;/div&gt;</code></pre><h2>Summary</h2>
<ul>
<li>Apply <code>#[Renderless]</code> (or <code>$this-&gt;skipRender()</code> in Livewire 2/3) to file export methods in Livewire components.</li>
<li>Skips the <code>render()</code> lifecycle entirely, preventing redundant database queries and HTML rendering during downloads.</li>
<li>Combines with <code>response()-&gt;streamDownload()</code> for memory-efficient exports directly to the browser.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Livewire</category>
            <category>Performance</category>
            <category>Laravel</category>
            <category>Livewire</category>
            <category>HTTP</category>
            <category>Streaming</category>
            <category>CSV</category>
            <category>Performance</category>
        </item>
        <item>
            <title><![CDATA[Multi-Tenant Team Scoping with a Reusable BelongsToTeam Model Trait]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-belongs-to-team-multitenancy-trait</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-belongs-to-team-multitenancy-trait</guid>
            <pubDate>Sun, 05 Nov 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Automatically assign tenant IDs and restrict Eloquent queries using a reusable BelongsToTeam model trait with model booting and global scopes.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Automatically assign tenant IDs and restrict Eloquent queries using a reusable BelongsToTeam model trait with model booting and global scopes.</p>
</blockquote>
<p>In multi-tenant or team-based SaaS applications, models (such as projects, invoices, and documents) must belong to a specific team, and users must only access records belonging to their active team.</p>
<p>Instead of writing repetitive query scopes and <code>$model-&gt;team_id = auth()-&gt;user()-&gt;team_id</code> assignments in every controller, you can encapsulate multi-tenant behavior in a reusable model trait.</p>
<h2>The BelongsToTeam Trait</h2>
<pre><code class="language-php">namespace App\Traits\Models;

use App\Models\Team;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

trait BelongsToTeam
{
    public static function bootBelongsToTeam(): void
    {
        // Automatically set the team_id when creating a new record
        static::creating(function ($model): void {
            if (auth()-&gt;check() &amp;&amp; empty($model-&gt;team_id)) {
                $model-&gt;team_id = auth()-&gt;user()-&gt;team_id;
            }
        });
    }

    protected static function booted(): void
    {
        parent::booted();

        // Apply a global scope to filter queries by active team
        if (auth()-&gt;check()) {
            static::addGlobalScope(&#039;team&#039;, function (Builder $query): void {
                $query-&gt;team();
            });
        }
    }

    public function scopeTeam(Builder $query): Builder
    {
        return $query-&gt;when(
            auth()-&gt;user()?-&gt;team_id,
            function (Builder $query, int $teamId): Builder {
                // Prefix column with table name to prevent SQL ambiguity in joins
                return $query-&gt;where($this-&gt;getTable() . &#039;.team_id&#039;, $teamId);
            },
            function (Builder $query): Builder {
                // If the user has no team, prevent data leakage
                abort(403, &#039;User does not belong to an active team.&#039;);
            }
        );
    }

    public function team(): BelongsTo
    {
        return $this-&gt;belongsTo(Team::class);
    }
}</code></pre><h2>Using the Trait on Models</h2>
<pre><code class="language-php">namespace App\Models;

use App\Traits\Models\BelongsToTeam;
use Illuminate\Database\Eloquent\Model;

class Project extends Model
{
    use BelongsToTeam;

    protected $fillable = [&#039;name&#039;, &#039;description&#039;];
}</code></pre><h2>What This Automates</h2>
<ol>
<li><strong>Automatic Assignment</strong>: Calling <code>Project::create([&#39;name&#39; =&gt; &#39;API v2&#39;])</code> automatically populates <code>team_id</code> from the authenticated user.</li>
<li><strong>Automatic Query Scoping</strong>: Calling <code>Project::all()</code> automatically generates <code>WHERE projects.team_id = ?</code>.</li>
<li><strong>Join Safety</strong>: Using <code>$this-&gt;getTable() . &#39;.team_id&#39;</code> ensures joins and <code>hasManyThrough</code> relationships do not encounter &quot;ambiguous column&quot; SQL errors.</li>
</ol>
<h2>Summary</h2>
<ul>
<li>Use a <code>BelongsToTeam</code> trait to automate tenant ID assignment on creation and filter queries via global scopes.</li>
<li>Table-prefix column references in query scopes to prevent SQL errors in joins.</li>
<li>Eliminates manual scoping across controllers and guarantees tenant isolation.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Architecture</category>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Multi-Tenancy</category>
            <category>Architecture</category>
            <category>Security</category>
        </item>
        <item>
            <title><![CDATA[Reusable whereLike Macro with Relationship and Expression Support]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-custom-wherelike-macro</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-custom-wherelike-macro</guid>
            <pubDate>Fri, 27 Oct 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Simplify multi-column wildcard searches across model columns, relationships, and raw SQL expressions using a powerful whereLike macro on the Eloquent Builder.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Simplify multi-column wildcard searches across model columns, relationships, and raw SQL expressions using a powerful whereLike macro on the Eloquent Builder.</p>
</blockquote>
<p>Searching across multiple attributes frequently leads to verbose and repetitive <code>orWhere</code> query chains in controller code.</p>
<p>By registering a custom <code>whereLike</code> macro on Eloquent&#39;s <code>Builder</code>, you can search across model columns, dot-notation relationships (<code>user.name</code>), and custom database expressions (<code>DB::raw()</code>) in a single readable call.</p>
<h2>Registering the Macro in AppServiceProvider</h2>
<pre><code class="language-php">namespace App\Providers;

use Illuminate\Contracts\Database\Query\Expression;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Builder::macro(&#039;whereLike&#039;, function ($attributes, string $searchTerm) {
            return $this-&gt;where(function (Builder $query) use ($attributes, $searchTerm) {
                foreach (Arr::wrap($attributes) as $attribute) {
                    $query-&gt;when(
                        // Check if attribute is a relationship dot-notation string (e.g. &#039;user.name&#039;)
                        ! ($attribute instanceof Expression) &amp;&amp; str_contains((string) $attribute, &#039;.&#039;),
                        function (Builder $query) use ($attribute, $searchTerm) {
                            [$relation, $relatedAttribute] = explode(&#039;.&#039;, (string) $attribute);

                            $query-&gt;orWhereHas($relation, function (Builder $query) use ($relatedAttribute, $searchTerm) {
                                $query-&gt;where($relatedAttribute, &#039;LIKE&#039;, &quot;%{$searchTerm}%&quot;);
                            });
                        },
                        function (Builder $query) use ($attribute, $searchTerm) {
                            // Search on local column or DB::raw expression
                            $query-&gt;orWhere($attribute, &#039;LIKE&#039;, &quot;%{$searchTerm}%&quot;);
                        }
                    );
                }
            });
        });
    }
}</code></pre><h2>Usage Example</h2>
<p>You can pass single columns, related model fields using dot notation, and formatted raw SQL expressions:</p>
<pre><code class="language-php">use App\Models\Post;
use Illuminate\Support\Facades\DB;

$search = request(&#039;search&#039;);

$posts = Post::query()
    -&gt;whereLike([
        &#039;title&#039;,
        &#039;description&#039;,
        &#039;user.name&#039;,
        &#039;user.email&#039;,
        DB::raw(&#039;CONCAT(user.first_name, &quot; &quot;, user.last_name)&#039;),
        DB::raw(&#039;DATE_FORMAT(created_at, &quot;%d/%m/%Y&quot;)&#039;),
    ], $search)
    -&gt;with(&#039;user&#039;)
    -&gt;paginate(15);</code></pre><h2>How It Works</h2>
<ol>
<li><strong>Encapsulated Scope</strong>: Wraps the entire search clause in a single <code>$this-&gt;where(function ($query) ...)</code> closure so that boolean <code>AND</code> / <code>OR</code> operator precedence is respected when chained with other query filters.</li>
<li><strong>Relationship Detection</strong>: Automatically detects dot notation (<code>&#39;user.name&#39;</code>) and applies <code>orWhereHas</code> subqueries against the related model.</li>
<li><strong>Expression Compatibility</strong>: Supports <code>DB::raw()</code> expressions without throwing type errors, enabling searches on computed columns and SQL date formats.</li>
</ol>
<h2>Summary</h2>
<ul>
<li>Use <code>Builder::macro(&#39;whereLike&#39;)</code> to consolidate multi-attribute wildcard searches into a single expressive method.</li>
<li>Automatically handles both local table columns and related model attributes via <code>orWhereHas</code>.</li>
<li>Respects SQL operator grouping and supports raw database expressions.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Macros</category>
            <category>Search</category>
            <category>Database</category>
        </item>
        <item>
            <title><![CDATA[Speed Up Test Suites with the LazilyRefreshDatabase Trait]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-testing-lazily-refresh-database</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-testing-lazily-refresh-database</guid>
            <pubDate>Wed, 11 Oct 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Use LazilyRefreshDatabase instead of RefreshDatabase to run database transactions only for tests that actually touch the database.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use LazilyRefreshDatabase instead of RefreshDatabase to run database transactions only for tests that actually touch the database.</p>
</blockquote>
<p>When running large test suites, the traditional <code>RefreshDatabase</code> trait migrates and resets database transactions before every test, even for pure unit tests that only test formatting or calculations without touching SQL.</p>
<p><code>LazilyRefreshDatabase</code> defers database initialization until a query is executed.</p>
<h2>Using LazilyRefreshDatabase in Tests</h2>
<pre><code class="language-php">namespace Tests\Feature;

use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Tests\TestCase;

class UserFeatureTest extends TestCase
{
    use LazilyRefreshDatabase;

    test(&#039;calculates user discount percentage&#039;, function () {
        // Pure calculation: No database query executed, database setup is skipped!
        $discount = (new Calculator)-&gt;discount(100, 20);
        expect($discount)-&gt;toBe(80);
    });

    test(&#039;creates user record in database&#039;, function () {
        // First database query triggers lazy transaction setup automatically
        $user = User::factory()-&gt;create();
        $this-&gt;assertModelExists($user);
    });
}</code></pre><h2>Summary</h2>
<ul>
<li>Defer database connection setup until the first SQL query executes.</li>
<li>Significantly accelerates test suite runtime in hybrid unit/feature suites.</li>
<li>Drop-in replacement for <code>RefreshDatabase</code>.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Testing</category>
            <category>Laravel</category>
            <category>Testing</category>
            <category>Performance</category>
            <category>Database</category>
        </item>
        <item>
            <title><![CDATA[Reuse Model Instances Across Nested Factories with Factory recycle()]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-factory-recycle-existing-models</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-factory-recycle-existing-models</guid>
            <pubDate>Wed, 27 Sep 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Use Factory::recycle() to pass existing parent models to child factories, avoiding duplicate database record creation in test fixtures.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use Factory::recycle() to pass existing parent models to child factories, avoiding duplicate database record creation in test fixtures.</p>
</blockquote>
<p>When generating complex test hierarchies (such as creating 10 Posts, each belonging to the same User, or multiple Orders for an existing Customer), child factories create duplicate parent models by default.</p>
<p><code>recycle()</code> instructs factories to reuse existing model instances rather than creating new ones.</p>
<h2>Reusing Existing Models</h2>
<pre><code class="language-php">use App\Models\Post;
use App\Models\User;

$user = User::factory()-&gt;create();

// All 5 posts will be assigned to the existing $user instance
$posts = Post::factory()
    -&gt;count(5)
    -&gt;recycle($user)
    -&gt;create();</code></pre><h2>Recycling Collections of Models</h2>
<p>Pass a collection of models to distribute them across child instances:</p>
<pre><code class="language-php">$users = User::factory()-&gt;count(3)-&gt;create();

// 20 posts randomly distributed among the 3 existing users
$posts = Post::factory()
    -&gt;count(20)
    -&gt;recycle($users)
    -&gt;create();</code></pre><h2>Summary</h2>
<ul>
<li>Prevents duplicate parent records from cluttering test databases.</li>
<li>Speeds up test execution times by reducing total database insertions.</li>
<li>Accepts single model instances or model collections.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Testing</category>
            <category>Laravel</category>
            <category>Testing</category>
            <category>Factories</category>
            <category>Performance</category>
        </item>
        <item>
            <title><![CDATA[Work Around MySQL Subquery Restrictions on Target Tables]]></title>
            <link>https://mrpunyapal.dev/tips/mysql-subquery-same-table-update-restriction</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/mysql-subquery-same-table-update-restriction</guid>
            <pubDate>Fri, 22 Sep 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[MySQL prevents updating a table while selecting from it in a subquery. Wrap subqueries in an intermediate alias table.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>MySQL prevents updating a table while selecting from it in a subquery. Wrap subqueries in an intermediate alias table.</p>
</blockquote>
<p>Executing UPDATE table WHERE id IN (SELECT id FROM table) throws MySQL Error 1093. Work around this by wrapping the subquery in an intermediate derived table alias.</p>
<pre><code class="language-sql">-- ❌ FAILS in MySQL: Error 1093
-- UPDATE users SET status = &#039;inactive&#039; WHERE id IN (SELECT id FROM users WHERE last_login &lt; &#039;2023-01-01&#039;);

-- ✅ WORKS: Intermediate alias subquery
UPDATE users SET status = &#039;inactive&#039;
WHERE id IN (
    SELECT id FROM (
        SELECT id FROM users WHERE last_login &lt; &#039;2023-01-01&#039;
    ) AS temp_users
);</code></pre><ul>
<li>MySQL forbids modifying a target table used directly in a subquery clause</li>
<li>Wrapping subquery in SELECT * FROM (...) AS alias resolves Error 1093</li>
<li>Alternative: Use JOIN syntax for multi-table updates</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>MySQL</category>
            <category>Queries</category>
            <category>MySQL</category>
            <category>Database</category>
            <category>SQL</category>
        </item>
        <item>
            <title><![CDATA[Build Conditional Transformation Pipelines with Collection::when() and whenEmpty()]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-collection-when-conditional-pipelines</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-collection-when-conditional-pipelines</guid>
            <pubDate>Wed, 20 Sep 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Use when(), unless(), and whenEmpty() on Laravel Collections to conditionally apply transformations without breaking method chains.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use when(), unless(), and whenEmpty() on Laravel Collections to conditionally apply transformations without breaking method chains.</p>
</blockquote>
<p>When processing collections (such as applying search filters or sorting parameters), writing intermediate <code>if</code> blocks breaks the fluent chain and requires temporary variables.</p>
<p>Laravel Collections provide <code>when()</code> and <code>unless()</code> to conditionally execute closures.</p>
<h2>Conditional Transformations with when()</h2>
<pre><code class="language-php">$sortBy = request(&#039;sort&#039;); // &#039;price&#039;, &#039;rating&#039;, or null
$filterActive = request()-&gt;boolean(&#039;active_only&#039;);

$products = collect($rawProducts)
    -&gt;when($filterActive, function ($collection) {
        return $collection-&gt;where(&#039;is_active&#039;, true);
    })
    -&gt;when($sortBy === &#039;price&#039;, function ($collection) {
        return $collection-&gt;sortBy(&#039;price&#039;);
    })
    -&gt;when($sortBy === &#039;rating&#039;, function ($collection) {
        return $collection-&gt;sortByDesc(&#039;rating&#039;);
    });</code></pre><h2>Fallback Data with whenEmpty()</h2>
<p>When a collection contains no items, <code>whenEmpty()</code> provides a clean fallback closure:</p>
<pre><code class="language-php">$recipients = collect($users)
    -&gt;whenEmpty(function ($collection) {
        // Fallback to default administrator if no recipients found
        return collect([User::getSystemAdmin()]);
    });</code></pre><h2>Summary</h2>
<ul>
<li>Keeps collection transformation logic inside a single readable fluent chain.</li>
<li>Accepts an optional default callback as the third argument if the condition is false.</li>
<li><code>whenEmpty()</code> cleanly handles empty dataset fallbacks.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Collections</category>
            <category>Laravel</category>
            <category>Collections</category>
            <category>Clean Code</category>
        </item>
        <item>
            <title><![CDATA[Catch Unmocked External API Calls with Http::preventStrayRequests()]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-http-fake-prevent-stray-requests</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-http-fake-prevent-stray-requests</guid>
            <pubDate>Wed, 13 Sep 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Enable Http::preventStrayRequests() in test suites to throw exceptions whenever HTTP requests are made without explicit mocks.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Enable Http::preventStrayRequests() in test suites to throw exceptions whenever HTTP requests are made without explicit mocks.</p>
</blockquote>
<p>In test suites, accidental outbound HTTP requests to real third-party APIs (such as payment gateways, SMS providers, or webhooks) can trigger unwanted real-world charges or fail unpredictably without internet access.</p>
<p>Laravel provides <code>Http::preventStrayRequests()</code> to block all unmocked HTTP traffic.</p>
<h2>Enabling in Base TestCase</h2>
<pre><code class="language-php">namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Http;

abstract class TestCase extends BaseTestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        // Throw an exception if any HTTP request is unhandled by Http::fake()
        Http::preventStrayRequests();
    }
}</code></pre><h2>What Happens on Stray Requests</h2>
<p>If code executes an un-faked HTTP request during a test, Laravel immediately fails with a descriptive <code>RuntimeException</code>:</p>
<pre><code class="language-text">Attempted request to [https://api.stripe.com/v1/charges] without a matching fake.</code></pre><h2>Summary</h2>
<ul>
<li>Guarantees 100% mocked external HTTP interactions in CI/CD pipelines.</li>
<li>Prevents accidental calls to real production API endpoints during testing.</li>
<li>Forces developers to declare explicit <code>Http::fake()</code> definitions for all outbound traffic.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Testing</category>
            <category>Laravel</category>
            <category>Testing</category>
            <category>HTTP</category>
            <category>Security</category>
        </item>
        <item>
            <title><![CDATA[Query Relationships Across Multiple Database Connections]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-where-has-multiple-database-connections</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-where-has-multiple-database-connections</guid>
            <pubDate>Wed, 16 Aug 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Query related models located on different database connections using whereHas() without cross-database join errors.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Query related models located on different database connections using whereHas() without cross-database join errors.</p>
</blockquote>
<p>When your application partitions data across multiple database connections (such as separating analytics logs, multi-tenant tenants, or payment microservices onto different database servers), standard SQL joins between connection tables fail.</p>
<p>Eloquent&#39;s <code>whereHas()</code> directly handles relations across different database connections.</p>
<h2>Defining Models on Separate Connections</h2>
<pre><code class="language-php">namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Tenant extends Model
{
    protected $connection = &#039;main_db&#039;;

    public function activityLogs(): HasMany
    {
        return $this-&gt;hasMany(ActivityLog::class);
    }
}

class ActivityLog extends Model
{
    // Stored on a separate logging database server
    protected $connection = &#039;logging_db&#039;;
}</code></pre><h2>Querying Across Connections with whereHas()</h2>
<pre><code class="language-php">use App\Models\Tenant;

// Eloquent handles the cross-connection query correctly
$activeTenants = Tenant::whereHas(&#039;activityLogs&#039;, function ($query) {
    $query-&gt;where(&#039;created_at&#039;, &#039;&gt;=&#039;, now()-&gt;subDays(7));
})-&gt;get();</code></pre><h2>Summary</h2>
<ul>
<li>Enables relational querying even when models reside on distinct database servers.</li>
<li>Uses <code>WHERE EXISTS</code> queries scoped by database name.</li>
<li>Eliminates manual cross-database ID querying loops.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Database</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Query JSON and Array ID Columns with Array-Column Relationships]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-array-column-relationships-hasmanyarraycolumn</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-array-column-relationships-hasmanyarraycolumn</guid>
            <pubDate>Mon, 19 Jun 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Define relationships on models where foreign keys are stored as JSON arrays or comma-separated lists rather than traditional single-id foreign keys.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Define relationships on models where foreign keys are stored as JSON arrays or comma-separated lists rather than traditional single-id foreign keys.</p>
</blockquote>
<p>When dealing with legacy schemas or denormalized database designs where a model stores multiple related IDs inside a JSON array column (e.g. <code>[1, 2, 5]</code>), standard Eloquent relationships fail.</p>
<p>Using array column relationship packages or custom query scope join helpers allows direct querying:</p>
<pre><code class="language-php">use App\Models\Tag;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $casts = [
        &#039;tag_ids&#039; =&gt; &#039;array&#039;,
    ];

    // Query products that contain specific tag IDs inside JSON array
    public function scopeWithTag($query, int $tagId)
    {
        return $query-&gt;whereJsonContains(&#039;tag_ids&#039;, $tagId);
    }
}

// Fetch products matching tag array
$products = Product::withTag(5)-&gt;get();</code></pre><ul>
<li>Enables relational queries on denormalized JSON array fields</li>
<li>Uses database-native <code>whereJsonContains()</code> for optimized index searching</li>
<li>Ideal for tag lists, permission arrays, and multi-category selections</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Laravel</category>
            <category>Eloquent</category>
            <category>Database</category>
        </item>
        <item>
            <title><![CDATA[Test Time Delays Without Slowing Down Tests Using the Sleep Helper]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-sleep-helper-testable-delays</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-sleep-helper-testable-delays</guid>
            <pubDate>Wed, 01 Mar 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Replace PHP's native sleep() and usleep() with Laravel's Sleep facade to write testable, fakeable time pauses.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Replace PHP&#39;s native sleep() and usleep() with Laravel&#39;s Sleep facade to write testable, fakeable time pauses.</p>
</blockquote>
<p>Using native <code>sleep(5)</code> in queue jobs or API polling loops pauses real system execution, causing test suites to run slowly.</p>
<p>Laravel provides the <code>Illuminate\Support\Sleep</code> facade, which supports inspection and faking during tests.</p>
<h2>Using Sleep in Application Code</h2>
<pre><code class="language-php">use Illuminate\Support\Sleep;

// Pause for 5 seconds
Sleep::for(5)-&gt;seconds();

// Pause for 500 milliseconds
Sleep::for(500)-&gt;milliseconds();

// Pause until a specific Carbon timestamp
Sleep::until(now()-&gt;addMinutes(2));</code></pre><h2>Faking Delays in Tests</h2>
<p>In unit and feature tests, call <code>Sleep::fake()</code> to bypass real sleep delays and assert that pauses occurred:</p>
<pre><code class="language-php">use Illuminate\Support\Sleep;

test(&#039;it pauses before retrying failed requests&#039;, function () {
    // Prevent real sleep delays in tests
    Sleep::fake();

    $service = new PaymentGateway();
    $service-&gt;chargeWithRetry($order);

    // Verify sleep occurred without waiting 10 seconds!
    Sleep::assertSlept(fn ($duration) =&gt; $duration-&gt;totalSeconds === 10);
});</code></pre><h2>Summary</h2>
<ul>
<li>Replaces native <code>sleep()</code> and <code>usleep()</code> with expressive fluent units (<code>seconds()</code>, <code>milliseconds()</code>).</li>
<li><code>Sleep::fake()</code> eliminates execution pauses in test suites.</li>
<li>Provides assertions like <code>assertSlept()</code> and <code>assertNeverSlept()</code>.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Testing</category>
            <category>Laravel</category>
            <category>Testing</category>
            <category>Utilities</category>
            <category>Clean Code</category>
        </item>
        <item>
            <title><![CDATA[Use CSS Container Queries for Modular Component Layouts]]></title>
            <link>https://mrpunyapal.dev/tips/css-container-queries-responsive-components</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/css-container-queries-responsive-components</guid>
            <pubDate>Fri, 24 Feb 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Use CSS Container Queries (@container) to adjust component layouts based on parent container width rather than viewport size.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use CSS Container Queries (@container) to adjust component layouts based on parent container width rather than viewport size.</p>
</blockquote>
<p>Media queries (@media) check total screen width, which breaks when a component is placed inside narrow sidebars versus wide main content areas. Container queries adjust styles based on element parent container width.</p>
<pre><code class="language-css">/* Define container context on parent */
.card-container {
  container-type: inline-size;
}

/* Adjust card layout based on container width */
@container (min-width: 400px) {
  .card {
    display: flex;
    flex-direction: row;
  }
}</code></pre><ul>
<li>Adapts component layouts based on parent container dimensions instead of viewport width</li>
<li>Allows building truly self-contained, context-aware UI components</li>
<li>Supported natively in all modern web browsers</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>CSS</category>
            <category>Styling</category>
            <category>CSS</category>
            <category>Responsive</category>
            <category>Frontend</category>
        </item>
        <item>
            <title><![CDATA[Log and Audit Slow Database Queries with DB::listen()]]></title>
            <link>https://mrpunyapal.dev/tips/laravel-db-listen-slow-query-logging</link>
            <guid isPermaLink="true">https://mrpunyapal.dev/tips/laravel-db-listen-slow-query-logging</guid>
            <pubDate>Wed, 01 Feb 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Use DB::listen() in AppServiceProvider to monitor executed queries, log slow operations, and inspect raw SQL bindings during local development.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Use DB::listen() in AppServiceProvider to monitor executed queries, log slow operations, and inspect raw SQL bindings during local development.</p>
</blockquote>
<p>Debugging database performance bottlenecks requires visibility into every executed query, its execution time, and bound parameters.</p>
<p>Laravel provides <code>DB::listen()</code> to register a callback executed after every database query.</p>
<h2>Logging Slow Queries in Development</h2>
<p>Add the listener inside <code>AppServiceProvider::boot()</code>:</p>
<pre><code class="language-php">namespace App\Providers;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        DB::listen(function ($query) {
            // Log any query that takes longer than 100ms
            if ($query-&gt;time &gt; 100) {
                Log::warning(&quot;Slow Query [{$query-&gt;time}ms]: {$query-&gt;sql}&quot;, [
                    &#039;bindings&#039;   =&gt; $query-&gt;bindings,
                    &#039;connection&#039; =&gt; $query-&gt;connectionName,
                ]);
            }
        });
    }
}</code></pre><h2>Inspecting Query Properties</h2>
<ul>
<li><strong><code>$query-&gt;sql</code></strong>: The prepared SQL statement (e.g. <code>select * from users where email = ?</code>).</li>
<li><strong><code>$query-&gt;bindings</code></strong>: Array of bound parameters.</li>
<li><strong><code>$query-&gt;time</code></strong>: Execution time in milliseconds.</li>
<li><strong><code>$query-&gt;connectionName</code></strong>: Database connection identifier (e.g. <code>mysql</code>, <code>pgsql</code>).</li>
</ul>
<h2>Summary</h2>
<ul>
<li>Intercepts all database queries application-wide.</li>
<li>Enables threshold-based slow query logging.</li>
<li>Essential tool for identifying missing indexes and unoptimized joins during development.</li>
</ul>
]]></content:encoded>
            <author>contact@mrpunyapal.dev (Punyapal Shah)</author>
            <category>Laravel</category>
            <category>Database</category>
            <category>Laravel</category>
            <category>Database</category>
            <category>Performance</category>
            <category>Debugging</category>
        </item>
    </channel>
</rss>
