<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mesak</title>
    <description>The latest articles on DEV Community by Mesak (@mesak).</description>
    <link>https://dev.to/mesak</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F775424%2Fde7f7ef0-2842-4e29-9a27-bb3c8145f4cc.png</url>
      <title>DEV Community: Mesak</title>
      <link>https://dev.to/mesak</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mesak"/>
    <language>en</language>
    <item>
      <title>Building a Composer Proxy Cache with OpenResty</title>
      <dc:creator>Mesak</dc:creator>
      <pubDate>Tue, 23 Dec 2025 06:51:38 +0000</pubDate>
      <link>https://dev.to/mesak/building-a-composer-proxy-cache-with-openresty-fjl</link>
      <guid>https://dev.to/mesak/building-a-composer-proxy-cache-with-openresty-fjl</guid>
      <description>&lt;h1&gt;
  
  
  Building a Composer Proxy Cache with OpenResty
&lt;/h1&gt;

&lt;p&gt;In enterprise environments or CI/CD workflows, frequent execution of &lt;code&gt;composer install&lt;/code&gt; or &lt;code&gt;composer update&lt;/code&gt; generates numerous external network requests. This article explains how to build an intelligent Composer proxy cache service using OpenResty (Nginx + Lua), while integrating with private Satis package repositories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Goals and Motivation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problems Solved
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Redundant Downloads&lt;/strong&gt;: Every CI/CD run re-downloads the same packages from Packagist and GitHub&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network Latency&lt;/strong&gt;: Cross-region requests to Packagist (Netherlands) and GitHub (USA)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External Dependencies&lt;/strong&gt;: Deployment pipeline breaks when GitHub or Packagist experiences downtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private Package Integration&lt;/strong&gt;: Need to use both public packages and internal private packages&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why Nginx Cache?
&lt;/h3&gt;

&lt;p&gt;Compared to other caching solutions (Redis, Varnish), Nginx proxy cache offers these advantages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Nginx Cache&lt;/th&gt;
&lt;th&gt;Other Solutions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zero Extra Dependencies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only Nginx needed&lt;/td&gt;
&lt;td&gt;Requires additional services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Filesystem Cache&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stored on disk, survives restarts&lt;/td&gt;
&lt;td&gt;Memory cache needs warm-up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Automatic Expiration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built-in inactive cleanup&lt;/td&gt;
&lt;td&gt;Requires custom implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Large File Handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native streaming support&lt;/td&gt;
&lt;td&gt;Potential memory issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operational Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium to High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Using OpenResty extends Nginx with Lua, enabling complex URL rewriting logic while maintaining high performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Build Your Own? Limitations of Existing Solutions
&lt;/h3&gt;

&lt;p&gt;Several Composer proxy solutions exist, but each has limitations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;th&gt;Limitations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/Webysther/packagist-mirror" rel="noopener noreferrer"&gt;Packagist Mirror&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full mirror requires TB-scale storage, long sync times&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://toranproxy.com/" rel="noopener noreferrer"&gt;Toran Proxy&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Commercial software, requires paid license&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/composer/satis" rel="noopener noreferrer"&gt;Satis&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only supports private packages, cannot proxy public Packagist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://jfrog.com/artifactory/" rel="noopener noreferrer"&gt;Artifactory&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enterprise solution, complex architecture, expensive licensing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pure Nginx Proxy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cannot modify response body, dist URLs still point externally&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Advantages of This Approach:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;On-Demand Caching&lt;/strong&gt;: Only cache packages actually used, not the entire Packagist (300,000+ packages)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero License Fees&lt;/strong&gt;: Entirely based on open-source software&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight Deployment&lt;/strong&gt;: Single Docker container, low resource requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complete Proxy&lt;/strong&gt;: Both metadata and dist files are proxied and cached&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private Integration&lt;/strong&gt;: Seamless integration with Satis private repositories&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────┐     ┌──────────────────────┐     ┌────────────────┐
│   Composer      │────▶│   OpenResty Proxy    │────▶│ Packagist      │
│   Client        │     │                      │     │ (repo.packagist.org)
└─────────────────┘     └──────────────────────┘     └────────────────┘
                               │
                               ├─▶ Local Satis Static Files
                               └─▶ GitHub/GitLab Download Proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Request Flow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Composer sends metadata request to proxy&lt;/li&gt;
&lt;li&gt;Proxy checks: Does local Satis have this package?

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Yes&lt;/strong&gt; → Return local file directly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No&lt;/strong&gt; → Proxy to Packagist, cache result&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Composer parses metadata, requests actual dist files&lt;/li&gt;
&lt;li&gt;Proxy forwards dist requests to GitHub/GitLab, caches result&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Satis Integration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Satis?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/composer/satis" rel="noopener noreferrer"&gt;Satis&lt;/a&gt; is the official static package repository generator for Composer, creating Packagist-compatible JSON format for private packages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Standard Satis Workflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Configure satis.json&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"My Private Packages"&lt;/span&gt;,
    &lt;span class="s2"&gt;"homepage"&lt;/span&gt;: &lt;span class="s2"&gt;"https://packages.example.com"&lt;/span&gt;,
    &lt;span class="s2"&gt;"repositories"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;
        &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"type"&lt;/span&gt;: &lt;span class="s2"&gt;"vcs"&lt;/span&gt;, &lt;span class="s2"&gt;"url"&lt;/span&gt;: &lt;span class="s2"&gt;"git@gitlab.example.com:team/package-a.git"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;,
        &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"type"&lt;/span&gt;: &lt;span class="s2"&gt;"vcs"&lt;/span&gt;, &lt;span class="s2"&gt;"url"&lt;/span&gt;: &lt;span class="s2"&gt;"git@gitlab.example.com:team/package-b.git"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;,
    &lt;span class="s2"&gt;"require-all"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# 2. Run Satis build&lt;/span&gt;
php bin/satis build satis.json ./output

&lt;span class="c"&gt;# 3. Output structure&lt;/span&gt;
output/
├── packages.json           &lt;span class="c"&gt;# Main entry point&lt;/span&gt;
├── include/                &lt;span class="c"&gt;# Split metadata&lt;/span&gt;
│   └── all&lt;span class="nv"&gt;$xxx&lt;/span&gt;.json
└── dist/                   &lt;span class="c"&gt;# Package archives&lt;/span&gt;
    └── vendor/
        └── package/
            └── package-version-hash.tar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How This Project Integrates Satis
&lt;/h3&gt;

&lt;p&gt;Mount the Satis output directory to &lt;code&gt;/var/www/packagist&lt;/code&gt;. The proxy will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prioritize Local Files&lt;/strong&gt;: Serve private packages directly from Satis output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback to Packagist&lt;/strong&gt;: Proxy public packages to official Packagist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unified Entry Point&lt;/strong&gt;: Composer only needs one repository URL&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Core Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Smart Routing Logic
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- proxy_logic.lua&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;root_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/var/www/packagist"&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;file_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;io.open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"r"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;~=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="nb"&gt;io.close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root_path&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;string.sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="n"&gt;file_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;"index.html"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;file_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="c1"&gt;-- Local file exists, serve directly&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"@satis_local"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="c1"&gt;-- Proxy to Packagist&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"@packagist"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Dist URL Rewriting
&lt;/h3&gt;

&lt;p&gt;JSON metadata from Packagist contains download URLs pointing to GitHub. To cache these downloads, we rewrite URLs to point to our proxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- body_filter.lua (core logic)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http_x_forwarded_proto&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http_host&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;proxy_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;"://"&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;"/dist_proxy/"&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;new_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="sr"&gt;"url"&lt;/span&gt;&lt;span class="err"&gt;\\s*:\\s*&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;https&lt;/span&gt;&lt;span class="se"&gt;?)&lt;/span&gt;&lt;span class="sr"&gt;://&lt;/span&gt;&lt;span class="se"&gt;([^&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="err"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;proto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;-- "http" or "https"&lt;/span&gt;
        &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;-- domain/path&lt;/span&gt;

        &lt;span class="c1"&gt;-- Only process domains we want to proxy&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;string.find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"^api%.github%.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; 
           &lt;span class="nb"&gt;string.find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"^codeload%.github%.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
           &lt;span class="nb"&gt;string.find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"^gitlab%.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;

           &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'"url": "'&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;proxy_base&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;proto&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;-- Keep other URLs unchanged&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"jo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rewrite Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original: https://api.github.com/repos/vendor/package/zipball/abc123
Proxied:  https://proxy.example.com/dist_proxy/https/api.github.com/repos/vendor/package/zipball/abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Removing available-packages
&lt;/h3&gt;

&lt;p&gt;Satis generates &lt;code&gt;packages.json&lt;/code&gt; with an &lt;code&gt;available-packages&lt;/code&gt; field, which limits Composer to only query listed packages. Remove this field to allow the proxy to handle all packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="sr"&gt;"available-packages"&lt;/span&gt;&lt;span class="err"&gt;\\s*:\\s*\\[.*?\\]\\s*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;"jos"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Nginx Configuration Highlights
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Cache configuration&lt;/span&gt;
&lt;span class="k"&gt;proxy_cache_path&lt;/span&gt; &lt;span class="n"&gt;/var/cache/nginx&lt;/span&gt; 
    &lt;span class="s"&gt;levels=1:2&lt;/span&gt; 
    &lt;span class="s"&gt;keys_zone=composer_cache:50m&lt;/span&gt; 
    &lt;span class="s"&gt;max_size=10g&lt;/span&gt; 
    &lt;span class="s"&gt;inactive=7d&lt;/span&gt; 
    &lt;span class="s"&gt;use_temp_path=off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Dist file proxy&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;^/dist_proxy/(https?)/(.*)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;$target_proto&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;$target_url&lt;/span&gt; &lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="nv"&gt;$target_proto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;//&lt;/span&gt;&lt;span class="nv"&gt;$target_url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Long-term cache (versioned files are immutable)&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_cache&lt;/span&gt; &lt;span class="s"&gt;composer_cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_cache_valid&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="s"&gt;1y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Handle GitHub API 302 redirects&lt;/span&gt;
    &lt;span class="kn"&gt;header_filter_by_lua_block&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;local&lt;/span&gt; &lt;span class="s"&gt;loc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ngx.header.Location&lt;/span&gt;
        &lt;span class="s"&gt;if&lt;/span&gt; &lt;span class="s"&gt;loc&lt;/span&gt; &lt;span class="s"&gt;then&lt;/span&gt;
            &lt;span class="s"&gt;local&lt;/span&gt; &lt;span class="s"&gt;proto,&lt;/span&gt; &lt;span class="s"&gt;rest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;string.match(loc,&lt;/span&gt; &lt;span class="s"&gt;"^(https?)://(.*)")&lt;/span&gt;
            &lt;span class="s"&gt;if&lt;/span&gt; &lt;span class="s"&gt;proto&lt;/span&gt; &lt;span class="s"&gt;then&lt;/span&gt;
                &lt;span class="s"&gt;ngx.header.Location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;scheme&lt;/span&gt; &lt;span class="s"&gt;..&lt;/span&gt; &lt;span class="s"&gt;"://"&lt;/span&gt; &lt;span class="s"&gt;..&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt; &lt;span class="s"&gt;..&lt;/span&gt; &lt;span class="s"&gt;"/dist_proxy/"&lt;/span&gt; &lt;span class="s"&gt;..&lt;/span&gt; &lt;span class="s"&gt;proto&lt;/span&gt; &lt;span class="s"&gt;..&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt; &lt;span class="s"&gt;..&lt;/span&gt; &lt;span class="s"&gt;rest&lt;/span&gt;
            &lt;span class="s"&gt;end&lt;/span&gt;
        &lt;span class="s"&gt;end&lt;/span&gt;
    &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. SSL Termination Support
&lt;/h3&gt;

&lt;p&gt;When deployed behind a reverse proxy, correctly detect the original protocol to satisfy Composer's &lt;code&gt;secure-http&lt;/code&gt; requirement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http_x_forwarded_proto&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Frontend Nginx must pass this header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Caching Strategy
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Cache Duration&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Metadata (JSON)&lt;/td&gt;
&lt;td&gt;10 minutes&lt;/td&gt;
&lt;td&gt;Balance freshness and cache benefits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dist files (zip/tar)&lt;/td&gt;
&lt;td&gt;1 year&lt;/td&gt;
&lt;td&gt;Versioned files are immutable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max cache size&lt;/td&gt;
&lt;td&gt;10 GB&lt;/td&gt;
&lt;td&gt;Adjust based on disk space&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inactive cleanup&lt;/td&gt;
&lt;td&gt;7 days&lt;/td&gt;
&lt;td&gt;Free space from unused files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Docker Compose Configuration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;composer-proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openresty/openresty:alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer-proxy&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:80"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./conf/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./cache:/var/cache/nginx&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./satis-output:/var/www/packagist:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./lua:/usr/local/openresty/nginx/lua:ro&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Host Directory Permissions
&lt;/h2&gt;

&lt;p&gt;OpenResty container runs as &lt;code&gt;nobody&lt;/code&gt; user (UID 65534). Ensure host directory permissions are correct:&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache Directory
&lt;/h3&gt;

&lt;p&gt;Nginx needs write access to &lt;code&gt;/var/cache/nginx&lt;/code&gt; (maps to host &lt;code&gt;./cache&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create directory and set permissions&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ./cache
&lt;span class="nb"&gt;chmod &lt;/span&gt;777 ./cache

&lt;span class="c"&gt;# Or more secure: set owner to container's nobody user&lt;/span&gt;
&lt;span class="nb"&gt;chown &lt;/span&gt;65534:65534 ./cache
&lt;span class="nb"&gt;chmod &lt;/span&gt;755 ./cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Satis Output Directory
&lt;/h3&gt;

&lt;p&gt;Satis outputs to &lt;code&gt;/var/www/packagist&lt;/code&gt; (maps to host &lt;code&gt;./satis-output&lt;/code&gt;), container only needs read access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ensure Satis output directory exists&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ./satis-output

&lt;span class="c"&gt;# Run Satis build to this directory&lt;/span&gt;
php bin/satis build satis.json ./satis-output

&lt;span class="c"&gt;# Ensure files are readable&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 755 ./satis-output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Directory Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./
├── cache/              # Nginx cache (requires write permission)
├── satis-output/       # Satis output (read-only mount)
│   ├── packages.json
│   ├── include/
│   └── dist/
├── conf/
│   └── nginx.conf
└── lua/
    ├── proxy_logic.lua
    └── body_filter.lua
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Configure Composer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Global configuration&lt;/span&gt;
composer config &lt;span class="nt"&gt;-g&lt;/span&gt; repo.packagist composer http://localhost:8000

&lt;span class="c"&gt;# Revert&lt;/span&gt;
composer config &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nt"&gt;--unset&lt;/span&gt; repo.packagist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Clear Cache
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; composer-proxy &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/cache/nginx/&lt;span class="k"&gt;*&lt;/span&gt;
docker-compose restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Performance Improvements
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Without Cache&lt;/th&gt;
&lt;th&gt;With Cache&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First Laravel install&lt;/td&gt;
&lt;td&gt;~45s&lt;/td&gt;
&lt;td&gt;~45s&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second install&lt;/td&gt;
&lt;td&gt;~45s&lt;/td&gt;
&lt;td&gt;~12s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;73%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 CI runners simultaneously&lt;/td&gt;
&lt;td&gt;450s total requests&lt;/td&gt;
&lt;td&gt;~50s total requests&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;89%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Important Notes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Accept-Encoding&lt;/strong&gt;: Must be set to empty string, otherwise upstream returns gzip content that cannot be modified in Lua&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content-Length&lt;/strong&gt;: Must clear this header after modifying body, use chunked encoding&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  GitHub Rate Limit &amp;amp; Token Authentication
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem: 504 Timeout Errors
&lt;/h3&gt;

&lt;p&gt;When CI/CD runs frequently, you may encounter GitHub API 504 timeout errors. The main cause is &lt;strong&gt;GitHub API Rate Limit&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Authentication&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Requests per Hour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unauthenticated&lt;/td&gt;
&lt;td&gt;Per IP&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personal Access Token&lt;/td&gt;
&lt;td&gt;Per Token&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub App&lt;/td&gt;
&lt;td&gt;Per App&lt;/td&gt;
&lt;td&gt;15,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How Proxy Caching Helps
&lt;/h3&gt;

&lt;p&gt;Using this proxy service significantly reduces GitHub requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Without Cache:
  10 CI runners executing composer install simultaneously
  → 10 x 50 packages = 500 GitHub requests
  → Exceeds 60 limit → Rate limit triggered → 504 errors

With Cache:
  First CI runner executes, caches 50 packages
  CI runners 2-10 → Read directly from cache
  → Actual GitHub requests: only 50
  → No rate limit triggered ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Best Solution: Cache + Token
&lt;/h3&gt;

&lt;p&gt;Even with caching, setting a GitHub Token is recommended as a safeguard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cache Miss&lt;/strong&gt;: New packages or expired cache still require GitHub requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Initial Cache Build&lt;/strong&gt;: Large projects may exceed 60 requests on first run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic Spikes&lt;/strong&gt;: Multiple projects updating simultaneously&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Token Authentication Implementation
&lt;/h3&gt;

&lt;p&gt;Add authentication logic to the &lt;code&gt;dist_proxy&lt;/code&gt; location in &lt;code&gt;nginx.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="o"&gt;#&lt;/span&gt; &lt;span class="n"&gt;GitHub&lt;/span&gt; &lt;span class="n"&gt;Token&lt;/span&gt; &lt;span class="n"&gt;Authentication&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;increases&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;set_by_lua_block&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;github_auth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_host&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;string.find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"github.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nb"&gt;string.find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"githubusercontent.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;os.getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"GITHUB_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;~=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"token "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;proxy_set_header&lt;/span&gt; &lt;span class="n"&gt;Authorization&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;github_auth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Workflow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────┐     ┌──────────────────────────────────────┐     ┌─────────────────┐
│   Composer      │────▶│   OpenResty Proxy                    │────▶│   GitHub API    │
│   Client        │     │                                      │     │                 │
└─────────────────┘     │  1. Receive /dist_proxy/https/api... │     │                 │
                        │  2. Check target_host                 │     │                 │
                        │  3. Is github.com?                    │     │                 │
                        │     ├─ Yes → Add Authorization header │     │                 │
                        │     └─ No  → No header                │     │                 │
                        │  4. Proxy request to GitHub           │────▶│ Rate Limit:     │
                        └──────────────────────────────────────┘     │ 5000/hr (w/token)│
                                                                     │ 60/hr (no token) │
                                                                     └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Docker Compose:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;composer-proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GITHUB_TOKEN=${GITHUB_TOKEN:-}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create Token:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://github.com/settings/tokens?type=beta" rel="noopener noreferrer"&gt;GitHub Settings → Tokens&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Create Fine-grained token with &lt;code&gt;Public repositories (read-only)&lt;/code&gt; permission only&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By leveraging OpenResty's Lua extension capabilities, we implement complex proxy logic at the Nginx layer, maintaining high performance while gaining flexibility. This solution is particularly suitable for enterprise environments requiring private package integration, and can significantly improve CI/CD pipeline performance.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>php</category>
      <category>packagist</category>
      <category>proxy</category>
    </item>
    <item>
      <title>Docker Environment Composer Local Package Development Tips</title>
      <dc:creator>Mesak</dc:creator>
      <pubDate>Mon, 22 Dec 2025 06:27:04 +0000</pubDate>
      <link>https://dev.to/mesak/docker-environment-composer-local-package-development-tips-13a7</link>
      <guid>https://dev.to/mesak/docker-environment-composer-local-package-development-tips-13a7</guid>
      <description>&lt;p&gt;When developing Laravel applications in a Docker environment, you often need to simultaneously develop custom Composer packages. This article shares an elegant solution using symbolic links.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problems
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Docker containers cannot access host machine absolute paths&lt;/li&gt;
&lt;li&gt;Packages scattered across projects, difficult to manage centrally&lt;/li&gt;
&lt;li&gt;Package modifications require reinstallation to take effect&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Create Unified Package Development Directory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/developer/packages/php
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/developer/project/my-laravel-app/plugins

&lt;span class="c"&gt;# Create symbolic links&lt;/span&gt;
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /home/developer/packages/php/my-custom-package /home/developer/project/my-laravel-app/plugins/my-custom-package
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Docker Volume Mounting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./:/var/www/html&lt;/span&gt;  &lt;span class="c1"&gt;# Laravel project mounted to /var/www/html, NGINX usually points to /var/www/html&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/home/developer/packages/php:/home/developer/packages/php&lt;/span&gt;  &lt;span class="c1"&gt;# Key: Make the same path available inside container&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Composer Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"repositories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"plugins/my-custom-package"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"symlink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Independent Version Control for Packages
&lt;/h3&gt;

&lt;p&gt;Each package should have its own Git repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /home/developer/packages/php/my-custom-package
git init
git remote add origin https://github.com/your-org/my-custom-package.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why Independent Git is Needed?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version Management&lt;/strong&gt;: Packages have their own evolution history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-project Sharing&lt;/strong&gt;: Multiple projects can reference different versions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release Process&lt;/strong&gt;: Easy to tag and publish to Packagist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission Control&lt;/strong&gt;: Different packages can have different developer permissions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD&lt;/strong&gt;: Packages can have independent testing and deployment processes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main project's &lt;code&gt;.gitignore&lt;/code&gt; should exclude the symbolic link directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugins/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Advantages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Centralized Management: All packages developed in one place&lt;/li&gt;
&lt;li&gt;Real-time Updates: Changes take effect immediately, no reinstallation needed&lt;/li&gt;
&lt;li&gt;Multi-project Sharing: One codebase serves multiple projects&lt;/li&gt;
&lt;li&gt;Container Friendly: Perfect for Docker environments&lt;/li&gt;
&lt;li&gt;Independent Publishing: Each package can have independent version control and publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Core Principle
&lt;/h2&gt;

&lt;p&gt;Through &lt;strong&gt;dual mounting&lt;/strong&gt; strategy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Host machine creates symbolic links pointing to unified package directory&lt;/li&gt;
&lt;li&gt;Docker mounts the same path, making symbolic links work inside containers&lt;/li&gt;
&lt;li&gt;Composer uses relative paths, ensuring consistency between host and container&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Restoration Strategy After Development
&lt;/h2&gt;

&lt;p&gt;After package development is complete and published, switch back to official package sources:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Publish Package to Official Repository
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In package directory&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /home/developer/packages/php/my-custom-package
git tag v1.0.0
git push origin v1.0.0
&lt;span class="c"&gt;# Publish to Packagist or private Composer repository&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Update Project's composer.json
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"repositories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Remove&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;comment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;path&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"plugins/my-custom-package"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nl"&gt;"symlink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"vendor/my-custom-package"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.0"&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Change&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;official&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;number&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Clean Up Local Symbolic Links
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove symbolic links&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; /home/developer/project/my-laravel-app/plugins/my-custom-package

&lt;span class="c"&gt;# Reinstall official version&lt;/span&gt;
composer update vendor/my-custom-package
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Restore Docker Configuration (Optional)
&lt;/h3&gt;

&lt;p&gt;If no longer developing other packages, you can remove the package directory mounting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./:/var/www/html&lt;/span&gt;
    &lt;span class="c1"&gt;# Remove package mounting&lt;/span&gt;
    &lt;span class="c1"&gt;# - /home/developer/packages/php:/home/developer/packages/php&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This completes the transition from development mode to official release mode, ensuring the project uses stable official versions!&lt;/p&gt;

&lt;p&gt;This way you can enjoy the convenience of local package development in a Docker environment!&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>gRPC Transmission Optimization: An Efficient Solution Based on Flattening and Bitset</title>
      <dc:creator>Mesak</dc:creator>
      <pubDate>Wed, 17 Dec 2025 03:17:05 +0000</pubDate>
      <link>https://dev.to/mesak/grpc-transmission-optimization-an-efficient-solution-based-on-flattening-and-bitset-en-44eg</link>
      <guid>https://dev.to/mesak/grpc-transmission-optimization-an-efficient-solution-based-on-flattening-and-bitset-en-44eg</guid>
      <description>&lt;h1&gt;
  
  
  [AI Collaboration Note] gRPC Transmission Optimization: An Efficient Solution Based on Flattening and Bitset
&lt;/h1&gt;

&lt;p&gt;This article documents an optimization strategy adopted during the development of a high-performance database middleware (&lt;code&gt;hypool&lt;/code&gt;) to address gRPC transmission efficiency and type limitations. The solution, proposed with the assistance of AI, draws inspiration from low-level database storage principles to resolve two major challenges faced when transmitting large database result sets via traditional gRPC.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Background and Technical Challenges
&lt;/h2&gt;

&lt;p&gt;When designing APIs for database middleware, we typically need to return multi-row query results. Using traditional gRPC definitions can lead to the following performance and implementation issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1 Payload Redundancy (Key Repetition)
&lt;/h3&gt;

&lt;p&gt;The most intuitive Protobuf definition is to map each row of data to a Map or Object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;Row&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;repeated&lt;/span&gt; &lt;span class="n"&gt;Row&lt;/span&gt; &lt;span class="na"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problem Analysis&lt;/strong&gt;:&lt;br&gt;
This structure leads to severe payload bloating. Assuming a query result has 10,000 records containing fields &lt;code&gt;customer_id&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;, and &lt;code&gt;status&lt;/code&gt;, the field names (Keys) are transmitted repeatedly 10,000 times, consuming significant bandwidth resources.&lt;/p&gt;
&lt;h3&gt;
  
  
  1.2 Protobuf NULL Value Limitations
&lt;/h3&gt;

&lt;p&gt;The design philosophy of Protobuf (proto3) treats scalar types as non-nullable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If a &lt;code&gt;string&lt;/code&gt; field is &lt;code&gt;NULL&lt;/code&gt;, it is serialized as an empty string &lt;code&gt;""&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Client cannot distinguish between an "empty value" and a "NULL value in the original database".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Although Wrapper types like &lt;code&gt;google.protobuf.StringValue&lt;/code&gt; can solve this, they add extra message nesting levels and processing overhead.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Solution: Database-Like Low-Level Architecture
&lt;/h2&gt;

&lt;p&gt;Addressing these challenges, the AI suggested breaking away from traditional API object thinking and referencing &lt;strong&gt;Database Columnar Storage&lt;/strong&gt; or &lt;strong&gt;ODBC/JDBC driver&lt;/strong&gt; implementations.&lt;/p&gt;

&lt;p&gt;The core optimization strategy consists of two parts:&lt;/p&gt;
&lt;h3&gt;
  
  
  A. Flattening
&lt;/h3&gt;

&lt;p&gt;Primarily used to solve the Key-Value redundancy problem.&lt;/p&gt;

&lt;p&gt;Instead of transmitting an "Array of Objects", we &lt;strong&gt;flatten&lt;/strong&gt; the data structure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Header (Metadata)&lt;/strong&gt;: Transmit field definitions (&lt;code&gt;columns&lt;/code&gt;) only once.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Body (Values)&lt;/strong&gt;: Flatten all data values into a massive one-dimensional array (&lt;code&gt;values&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This design completely removes Key transmission from each row, significantly reducing payload size.&lt;/p&gt;
&lt;h3&gt;
  
  
  B. Bitset (Bitmap) Mechanism
&lt;/h3&gt;

&lt;p&gt;Primarily used to solve the NULL value marking problem.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;string&lt;/code&gt; is used to transmit "Values", we introduce an extra binary field (&lt;code&gt;bytes&lt;/code&gt;) to specifically record "State":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a &lt;strong&gt;Bitset&lt;/strong&gt; to mark whether each value is NULL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 bit corresponds to 1 value&lt;/strong&gt;.

&lt;ul&gt;
&lt;li&gt;Bit = &lt;code&gt;1&lt;/code&gt;: Indicates the value is NULL.&lt;/li&gt;
&lt;li&gt;Bit = &lt;code&gt;0&lt;/code&gt;: Indicates the value is valid.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Space Efficiency&lt;/strong&gt;:&lt;br&gt;
Every 8 field values consume only 1 byte of extra space. For 1,000 rows x 8 columns, only about 1 KB of overhead is needed to accurately record all NULL states.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Implementation Essentials
&lt;/h2&gt;

&lt;p&gt;This section demonstrates key code for "Compression" on the Server side and "Restoration" on the Client side.&lt;/p&gt;
&lt;h3&gt;
  
  
  3.1 Protobuf Definition (&lt;code&gt;proto/query.proto&lt;/code&gt;)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;QueryResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;repeated&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// Flattened values&lt;/span&gt;
    &lt;span class="k"&gt;repeated&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt; &lt;span class="na"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Field definitions&lt;/span&gt;
    &lt;span class="kt"&gt;bytes&lt;/span&gt; &lt;span class="na"&gt;null_bitmap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// NULL marker bitstream&lt;/span&gt;
    &lt;span class="kt"&gt;int32&lt;/span&gt; &lt;span class="na"&gt;row_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  3.2 Server Side: Encoding and Compression
&lt;/h3&gt;

&lt;p&gt;The Server's task is to iterate through database results once, simultaneously completing "Value Flattening" and "Bitmap Generation".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// $result['rows'] is the 2D array returned by the database&lt;/span&gt;
&lt;span class="nv"&gt;$values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="nv"&gt;$packedBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$currentByte&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$bitIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'rows'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$values&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// Placeholder for empty string&lt;/span&gt;
            &lt;span class="nv"&gt;$currentByte&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$bitIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Mark as NULL (Set bit to 1)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$values&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Store actual value&lt;/span&gt;
            &lt;span class="c1"&gt;// Non-NULL, Bit remains 0&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Write a byte every 8 bits&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nv"&gt;$bitIndex&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$packedBytes&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;chr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$currentByte&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$currentByte&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nv"&gt;$bitIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Write remaining bits&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bitIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$packedBytes&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;chr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$currentByte&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.3 Client Side: Decoding and Restoration
&lt;/h3&gt;

&lt;p&gt;Upon receiving data, the Client needs to slice it according to the number of &lt;code&gt;columns&lt;/code&gt; and refer to the &lt;code&gt;null_bitmap&lt;/code&gt; to restore NULLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$fetchedRows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="nv"&gt;$columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getColumns&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$colCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getValues&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// Get flattened array&lt;/span&gt;
&lt;span class="nv"&gt;$bitmap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getNullBitmap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Get Bitmap string&lt;/span&gt;

&lt;span class="nv"&gt;$rowCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRowCount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$rowCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$r&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$colCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Calculate absolute index in the 1D array&lt;/span&gt;
        &lt;span class="nv"&gt;$flatIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$colCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// [Core] Bitset Lookup&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. Find corresponding Byte position&lt;/span&gt;
        &lt;span class="nv"&gt;$bytePos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nv"&gt;$flatIndex&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
        &lt;span class="c1"&gt;// 2. Find Bit position within the Byte&lt;/span&gt;
        &lt;span class="nv"&gt;$bitPos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$flatIndex&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// 3. Check if the Bit is 1&lt;/span&gt;
        &lt;span class="nv"&gt;$isNull&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bitmap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$bytePos&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$bitPos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$isNull&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Accurately restore NULL&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$flatIndex&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$fetchedRows&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Through the symmetric logic above, we achieve compression and restoration of data with extremely low computational cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Optimization Benefits Analysis
&lt;/h2&gt;

&lt;p&gt;Adopting this architecture yields the following specific benefits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Extreme Transmission Efficiency&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Through the flattening design, payload size grows linearly with data volume, unaffected by field name length. In large data query scenarios, bandwidth savings are extremely significant.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Precise Type Restoration&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The Client can accurately restore database NULL states by reading the &lt;code&gt;null_bitmap&lt;/code&gt;, solving the limitations of gRPC default types.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Parsing Performance Improvement&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  For PHP and other languages, processing flat one-dimensional arrays (Indexed Arrays) generally offers better CPU Cache hit rates and lower memory fragmentation compared to processing massive complex nested objects.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  5. Conclusion
&lt;/h2&gt;

&lt;p&gt;This optimization case demonstrates the importance of moderately introducing &lt;strong&gt;low-level system design thinking&lt;/strong&gt; in modern distributed systems. Through collaboration with AI, we moved beyond the framework of simple API design, utilizing &lt;strong&gt;Bitwise Operations&lt;/strong&gt; and &lt;strong&gt;Data Structure Optimization&lt;/strong&gt; to solve inherent limitations of gRPC/Protobuf in database application scenarios at a very low cost.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>php</category>
      <category>grpc</category>
    </item>
    <item>
      <title>[AI 協作筆記] gRPC 傳輸優化：基於 Flattening 與 Bitset 的高效方案</title>
      <dc:creator>Mesak</dc:creator>
      <pubDate>Wed, 17 Dec 2025 03:13:08 +0000</pubDate>
      <link>https://dev.to/mesak/grpc-transmission-optimization-an-efficient-solution-based-on-flattening-and-bitset-n4k</link>
      <guid>https://dev.to/mesak/grpc-transmission-optimization-an-efficient-solution-based-on-flattening-and-bitset-n4k</guid>
      <description>&lt;h1&gt;
  
  
  [AI 協作筆記] gRPC 傳輸優化：基於 Flattening 與 Bitset 的高效方案
&lt;/h1&gt;

&lt;p&gt;本篇文章記錄了在開發高效能資料庫中間件 (&lt;code&gt;hypool&lt;/code&gt;) 時，針對 gRPC 傳輸效率與型別限制所採用的優化方案。該方案由 AI 協助提出，借鑑了資料庫底層儲存原理，解決了傳統 gRPC 在傳輸大量資料庫結果集時面臨的兩個主要挑戰。&lt;/p&gt;




&lt;h2&gt;
  
  
  1. 背景與技術挑戰
&lt;/h2&gt;

&lt;p&gt;在設計資料庫中間件的 API 時，我們通常需要回傳多行的查詢結果。若採用傳統的 gRPC 定義方式，會面臨以下效能與實作上的問題。&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1 Payload 冗餘問題 (Key Repetition)
&lt;/h3&gt;

&lt;p&gt;最直觀的 Protobuf 定義通常是將每一行資料定義為一個 Map 或 Object：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;Row&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;repeated&lt;/span&gt; &lt;span class="n"&gt;Row&lt;/span&gt; &lt;span class="na"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;問題分析&lt;/strong&gt;：&lt;br&gt;
這種結構會導致嚴重的 Payload 膨脹。假設查詢結果有 10,000 筆資料，且包含欄位 &lt;code&gt;customer_id&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;。在傳輸過程中，這些欄位名稱 (Key) 會被重複傳輸 10,000 次，佔用了大量的頻寬資源。&lt;/p&gt;
&lt;h3&gt;
  
  
  1.2 Protobuf 對 NULL 值的限制
&lt;/h3&gt;

&lt;p&gt;Protobuf (proto3) 的設計哲學將純量型別 (Scalar Types) 視為不可為空。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;string&lt;/code&gt; 欄位若為 &lt;code&gt;NULL&lt;/code&gt;，傳輸時會被序列化為空字串 &lt;code&gt;""&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Client 端無法區分這是「空值」還是「原始資料庫中的 NULL」。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;雖然可以使用 &lt;code&gt;google.protobuf.StringValue&lt;/code&gt; 等 Wrapper 類型解決，但這會增加額外的 Message 嵌套層級與處理開銷。&lt;/p&gt;


&lt;h2&gt;
  
  
  2. 解決方案：類資料庫底層架構
&lt;/h2&gt;

&lt;p&gt;針對上述挑戰，AI 建議跳脫傳統的 API 物件思維，轉而參考 &lt;strong&gt;資料庫底層 (Columnar Storage)&lt;/strong&gt; 或 &lt;strong&gt;ODBC/JDBC 驅動&lt;/strong&gt; 的實作方式。&lt;/p&gt;

&lt;p&gt;核心優化策略包含以下兩個部分：&lt;/p&gt;
&lt;h3&gt;
  
  
  A. 陣列扁平化 (Flattening)
&lt;/h3&gt;

&lt;p&gt;主要用於解決 Key-Value 冗餘問題。&lt;/p&gt;

&lt;p&gt;我們不再傳輸「物件陣列 (Array of Objects)」，而是將資料結構&lt;strong&gt;扁平化&lt;/strong&gt;：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Header (Metadata)&lt;/strong&gt;：單獨傳輸一次欄位定義 (&lt;code&gt;columns&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Body (Values)&lt;/strong&gt;：將所有資料值攤平成一個巨大的一維陣列 (&lt;code&gt;values&lt;/code&gt;)。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;這種設計完全移除了每行資料中的 Key 傳輸，顯著降低 Payload 大小。&lt;/p&gt;
&lt;h3&gt;
  
  
  B. Bitset (Bitmap) 機制
&lt;/h3&gt;

&lt;p&gt;主要用於解決 NULL 值標記問題。&lt;/p&gt;

&lt;p&gt;既然 &lt;code&gt;string&lt;/code&gt; 用來傳輸「值」，我們便引入一個額外的二進位欄位 (&lt;code&gt;bytes&lt;/code&gt;) 來專門記錄「狀態」：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;使用 &lt;strong&gt;Bitset&lt;/strong&gt; 來標記每一個值是否為 NULL。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 個 bit 對應 1 個值&lt;/strong&gt;。

&lt;ul&gt;
&lt;li&gt;Bit = &lt;code&gt;1&lt;/code&gt;: 表示該值為 NULL。&lt;/li&gt;
&lt;li&gt;Bit = &lt;code&gt;0&lt;/code&gt;: 表示該值有效。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;空間效率&lt;/strong&gt;：&lt;br&gt;
每 8 個欄位值僅需消耗 1 byte 的額外空間。對於 1,000 行 x 8 欄的資料，僅需約 1 KB 的 Overhead 即可精確記錄所有 NULL 狀態。&lt;/p&gt;


&lt;h2&gt;
  
  
  3. 實作細節 (Implementation Essentials)
&lt;/h2&gt;

&lt;p&gt;本節將展示 Server 端如何「壓縮」與 Client 端如何「還原」的關鍵程式碼。&lt;/p&gt;
&lt;h3&gt;
  
  
  3.1 Protobuf 定義 (&lt;code&gt;proto/query.proto&lt;/code&gt;)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;QueryResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;repeated&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// 扁平化數值&lt;/span&gt;
    &lt;span class="k"&gt;repeated&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt; &lt;span class="na"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 欄位定義&lt;/span&gt;
    &lt;span class="kt"&gt;bytes&lt;/span&gt; &lt;span class="na"&gt;null_bitmap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// NULL 標記位元流&lt;/span&gt;
    &lt;span class="kt"&gt;int32&lt;/span&gt; &lt;span class="na"&gt;row_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  3.2 Server 端：編碼與壓縮 (Encoding)
&lt;/h3&gt;

&lt;p&gt;Server 端的任務是一次性遍歷資料庫結果，同時完成「數值扁平化」與「Bitmap 生成」。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// $result['rows'] 是資料庫回傳的二維陣列&lt;/span&gt;
&lt;span class="nv"&gt;$values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="nv"&gt;$packedBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$currentByte&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$bitIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'rows'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$values&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// 值放空字串佔位&lt;/span&gt;
            &lt;span class="nv"&gt;$currentByte&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$bitIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 標記 NULL (Bit設為1)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$values&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 存入實際值&lt;/span&gt;
            &lt;span class="c1"&gt;// 非 NULL，Bit 保持 0&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// 每滿 8 個 bits 就寫入一個 byte&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nv"&gt;$bitIndex&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$packedBytes&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;chr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$currentByte&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$currentByte&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nv"&gt;$bitIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// 寫入剩餘的 bits&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bitIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$packedBytes&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;chr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$currentByte&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.3 Client 端：解碼與還原 (Decoding)
&lt;/h3&gt;

&lt;p&gt;Client 端收到資料後，需要根據 &lt;code&gt;columns&lt;/code&gt; 數量進切割，並參考 &lt;code&gt;null_bitmap&lt;/code&gt; 將 NULL 還原回來。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$fetchedRows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="nv"&gt;$columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getColumns&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$colCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getValues&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// 取得扁平化陣列&lt;/span&gt;
&lt;span class="nv"&gt;$bitmap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getNullBitmap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 取得 Bitmap string&lt;/span&gt;

&lt;span class="nv"&gt;$rowCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRowCount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$rowCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$r&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$colCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 計算在一維陣列中的絕對索引&lt;/span&gt;
        &lt;span class="nv"&gt;$flatIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$colCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// [精華] Bitset 查表法&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. 找到對應的 Byte 位置&lt;/span&gt;
        &lt;span class="nv"&gt;$bytePos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nv"&gt;$flatIndex&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
        &lt;span class="c1"&gt;// 2. 找到 Byte 中的 Bit 位置&lt;/span&gt;
        &lt;span class="nv"&gt;$bitPos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$flatIndex&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// 3. 檢查該 Bit 是否為 1&lt;/span&gt;
        &lt;span class="nv"&gt;$isNull&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bitmap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$bytePos&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$bitPos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$isNull&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 精確還原 NULL&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$flatIndex&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$fetchedRows&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;透過上述兩段對稱的邏輯，我們即可以極低的運算成本完成資料的壓縮與還原。&lt;/p&gt;




&lt;h2&gt;
  
  
  4. 優化效益分析
&lt;/h2&gt;

&lt;p&gt;採用此架構後，我們獲得了以下具體效益：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;極致的傳輸效率&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  透過扁平化設計，Payload 大小與資料量呈線性單純增長，不受欄位名稱長度影響。在大數據量查詢中，頻寬節省效果極為顯著。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;精確的型別還原&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Client 端可透過讀取 &lt;code&gt;null_bitmap&lt;/code&gt; 精確還原資料庫的 NULL 狀態，解決了 gRPC 預設型別的限制。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;解析效能提升&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  對於 PHP 與其他語言而言，處理扁平的一維陣列 (Indexed Array) 通常比處理大量複雜的巢狀物件 (Nested Objects) 擁有更好的 CPU Cache 命中率與更低的記憶體碎片。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  5. 總結
&lt;/h2&gt;

&lt;p&gt;這個優化案例展示了在現代分散式系統中，適度引入&lt;strong&gt;底層系統設計思維&lt;/strong&gt;的重要性。透過與 AI 的協作，我們跳脫了單純的 API 設計框架，利用 &lt;strong&gt;位元運算&lt;/strong&gt; 與 &lt;strong&gt;資料結構優化&lt;/strong&gt;，以極低的成本解決了 gRPC/Protobuf 在資料庫應用場景下的先天限制。&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>php</category>
      <category>grpc</category>
    </item>
    <item>
      <title>Vue Transitions can't use for nested routes's KeepAlive</title>
      <dc:creator>Mesak</dc:creator>
      <pubDate>Thu, 10 Aug 2023 01:44:33 +0000</pubDate>
      <link>https://dev.to/mesak/vue-transitions-cant-use-for-nested-routess-keepalive-2792</link>
      <guid>https://dev.to/mesak/vue-transitions-cant-use-for-nested-routess-keepalive-2792</guid>
      <description>&lt;p&gt;The following scenario is what I encountered while using the soybean-admin frontend UI framework. &lt;/p&gt;

&lt;p&gt;When using nested routes, the KeepAlive component caches routed elements. However, when reusing them, the transition animation component cannot place the elements. &lt;/p&gt;

&lt;p&gt;This issue arises because the frontend UI framework automatically retains components of the secondary menu, leading to this problem. I am not certain whether the root cause is the KeepAlive or the Transition component, but in any case, excluding components with nested routes from entering the cache can resolve this problem.&lt;/p&gt;

&lt;p&gt;The following is the original code where the problem occurred&lt;br&gt;
&lt;a href="https://github.com/honghuangdc/soybean-admin/blob/v0.10.3/src/layouts/common/global-content/index.vue"&gt;soybean-admin/blob/v0.10.3&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Laravel package api-response</title>
      <dc:creator>Mesak</dc:creator>
      <pubDate>Sat, 31 Dec 2022 10:13:29 +0000</pubDate>
      <link>https://dev.to/mesak/laravel-package-api-response-4e0n</link>
      <guid>https://dev.to/mesak/laravel-package-api-response-4e0n</guid>
      <description>&lt;p&gt;I mentioned a method &lt;a href="https://dev.to/mesak/laravel-controller-use-callaction-to-meke-apicontroller-122l"&gt;before&lt;/a&gt;, which is to use the unified processing of the controller return value to output the API response. For this reason, I wrote my method into a package to make it more convenient to use.&lt;/p&gt;

&lt;p&gt;package in :&lt;br&gt;
&lt;a href="https://github.com/mesak/laravel-api-response"&gt;https://github.com/mesak/laravel-api-response&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;in Laravel 8 or 9&lt;/p&gt;

&lt;p&gt;use command :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;composer require mesak/laravel-api-response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;your can install this package &lt;/p&gt;

&lt;p&gt;then use two step &lt;/p&gt;

&lt;p&gt;change Controller and change Exception\Handler&lt;/p&gt;

&lt;h2&gt;
  
  
  Exception\Handler
&lt;/h2&gt;

&lt;p&gt;change your &lt;code&gt;app/Exceptions/Handler.php&lt;/code&gt; file to extend &lt;code&gt;Mesak\LaravelApiResponse\Exceptions\Handler&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Exceptions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Mesak\LaravelApiResponse\Exceptions\Handler&lt;/span&gt;  &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;ExceptionHandler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Controller
&lt;/h2&gt;

&lt;p&gt;change your api controller, to extend  &lt;code&gt;Mesak\LaravelApiResponse\Http\Controllers\Controller&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Mesak\LaravelApiResponse\Http\Controllers\ApiController&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;BaseController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then enjoy this &lt;/p&gt;

</description>
    </item>
    <item>
      <title>Laravel FormRequest With Opis Validator</title>
      <dc:creator>Mesak</dc:creator>
      <pubDate>Sun, 14 Aug 2022 17:25:16 +0000</pubDate>
      <link>https://dev.to/mesak/laravel-formrequest-with-opis-validator-3poh</link>
      <guid>https://dev.to/mesak/laravel-formrequest-with-opis-validator-3poh</guid>
      <description>&lt;p&gt;I published a package for using &lt;a href="https://github.com/opis/json-schema"&gt;Opis JSON Schema&lt;/a&gt; to FormRequest on laravel&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mesak/laravel-opis-validator"&gt;https://github.com/mesak/laravel-opis-validator&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  installation
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require mesak/laravel-opis-validator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Requests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Requests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Mesak\LaravelOpisValidator\JsonSchemaRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JsonSchema&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JsonSchemaRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$extendValidatorMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'$schema'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"http://json-schema.org/draft-07/schema#"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"type"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"title"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Base Preference"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"description"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Base Preference Setting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"properties"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s2"&gt;"limit"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="s2"&gt;"type"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s2"&gt;"minimum"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s2"&gt;"maximum"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s2"&gt;"title"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s2"&gt;"attrs"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="s2"&gt;"placeholder"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"limit (limit)"&lt;/span&gt;
                    &lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="s2"&gt;"page"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="s2"&gt;"type"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s2"&gt;"title"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Page"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s2"&gt;"attrs"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="s2"&gt;"placeholder"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Page ( Page )"&lt;/span&gt;
                    &lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="s2"&gt;"properties"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="s2"&gt;"limit"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                            &lt;span class="s2"&gt;"type"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"integer"&lt;/span&gt;
                        &lt;span class="p"&gt;]&lt;/span&gt;
                    &lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s2"&gt;"additionalProperties"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"required"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s2"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"page"&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Controller
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Requests\JsonSchema&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;JsonSchemaRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;JsonSchemaRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;dd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
    </item>
    <item>
      <title>Laravel Controller use callAction to meke ApiController</title>
      <dc:creator>Mesak</dc:creator>
      <pubDate>Fri, 29 Jul 2022 14:00:00 +0000</pubDate>
      <link>https://dev.to/mesak/laravel-controller-use-callaction-to-meke-apicontroller-122l</link>
      <guid>https://dev.to/mesak/laravel-controller-use-callaction-to-meke-apicontroller-122l</guid>
      <description>&lt;p&gt;In most projects, the controller will be used to return the response to specify JSON to determine the content.&lt;/p&gt;

&lt;p&gt;I found that in laravel's routing, ControllerDispatcher uses callback function to call the corresponding Method&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Illuminate\Routing\ControllerDispatcher&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Route&lt;/span&gt; &lt;span class="nv"&gt;$route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$parameters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;resolveClassMethodDependencies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$route&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;parametersWithoutNulls&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$method&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'callAction'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$controller&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;callAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$controller&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nb"&gt;array_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see that the Controller will check whether your Controller has a function called &lt;code&gt;callAtion&lt;/code&gt; before executing it. If not, it will return and call the Controller method specified by the route.&lt;/p&gt;

&lt;p&gt;This led me to an idea that when Controller implements API or WEB routing, it must return a response to display content&lt;/p&gt;

&lt;p&gt;Among them, most people will unify json for API return, but the implementation methods are quite diverse.&lt;/p&gt;

&lt;p&gt;I have seen the &lt;a href="https://medium.com/@JoubranJad/laravel-response-macros-for-apis-260198b3114f"&gt;Response::macro&lt;/a&gt; method, or the &lt;a href="https://dev.to/bawa_geek/laravel-api-the-best-way-to-return-the-uniform-response-for-all-type-of-requests-2ao1"&gt;ApiResponseTrait&lt;/a&gt; method, but this method will make me feel difficult to maintain later,&lt;/p&gt;

&lt;p&gt;You will see that your Controller will have a lot of judgments to return 200 or 400 or 200 + content of http status code.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Controller
&lt;/h3&gt;

&lt;p&gt;In my own project, I built an ApiController, let all the content under the API folder in the route inherit ApiController, intercept all results, and convert them into API Response &lt;/p&gt;

&lt;p&gt;&lt;code&gt;ApiController&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;callAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nb"&gt;array_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="nv"&gt;$httpStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$method&lt;/span&gt;  &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'store'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;ResponseSchema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResponseSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$httpStatus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$httpStatus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I created a ResponseSchema Class, specifically defined the JSON Schema format, and then returned it uniformly. The content returned by the Controller will be the result of successful execution. The only small difference is that the newly added method is unified and I call it store, which will put http status code returns 201.&lt;/p&gt;

&lt;p&gt;Since only successful information is returned here, the Controller does not need to write a bunch of try catches, and can focus on returning the necessary information.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error Handler
&lt;/h3&gt;

&lt;p&gt;How to handle error handling? According to &lt;a href="https://laravel.com/docs/8.x/errors#the-exception-handler"&gt;the-exception-handler&lt;/a&gt; mentioned on the official website, there is a special error handling Handler for errors, so only &lt;code&gt;App\Exceptions\Handler&lt;/code&gt; , This program focuses on handling errors. Loading ResponseSchema in the render function and spit out the corresponding method can unify the JSON Response Schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$responseSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResponseSchema&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$responseSchema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$responseSchema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Server Error'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$responseSchema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$statusCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;is&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'api/*'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'oauth/*'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Accept'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$responseSchema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$statusCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At present, this method is my current method of implementing API Response, so that I can focus on Controller writing and achieve the purpose of hierarchical division of labor&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Simple REST API Use Vue Version</title>
      <dc:creator>Mesak</dc:creator>
      <pubDate>Thu, 26 May 2022 16:35:42 +0000</pubDate>
      <link>https://dev.to/mesak/simple-rest-api-use-vue-version-1jo2</link>
      <guid>https://dev.to/mesak/simple-rest-api-use-vue-version-1jo2</guid>
      <description>&lt;p&gt;DEMO: &lt;a href="https://codesandbox.io/s/simple-restapi-vue-gcilc6"&gt;Simple REST API Use Vue 3 Version&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This example is mainly to present, use the functions of ES6 Classes to achieve API encapsulation and access of the same type ...&lt;/p&gt;

&lt;p&gt;this is use Vue Framework, &lt;a href="https://dev.to/mesak/simple-rest-api-448g"&gt;JavaScript Version&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source &amp;amp; Dependencies&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://reqres.in"&gt;REST API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vuejs.org/"&gt;vue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lodash.com"&gt;lodash&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://axios-http.com/docs/intro"&gt;axios&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Simple REST API</title>
      <dc:creator>Mesak</dc:creator>
      <pubDate>Wed, 25 May 2022 17:41:00 +0000</pubDate>
      <link>https://dev.to/mesak/simple-rest-api-448g</link>
      <guid>https://dev.to/mesak/simple-rest-api-448g</guid>
      <description>&lt;h1&gt;
  
  
  DEMO: &lt;a href="https://codesandbox.io/s/simple-restapi-mc4epc"&gt;Simple REST API&lt;/a&gt;
&lt;/h1&gt;

&lt;p&gt;This example is mainly to present, use the functions of ES6 Classes to achieve API encapsulation and access of the same type ...&lt;/p&gt;

&lt;p&gt;Source &amp;amp; Dependencies&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://reqres.in"&gt;REST API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://v5.framework7.io/docs/template7"&gt;template7&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lodash.com"&gt;lodash&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://axios-http.com/docs/intro"&gt;axios&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sweetalert2.github.io/"&gt;sweetalert2&lt;/a&gt; (css include fail :( )&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>傳送 物件/陣列 到 FORMDATA</title>
      <dc:creator>Mesak</dc:creator>
      <pubDate>Sun, 08 May 2022 03:31:48 +0000</pubDate>
      <link>https://dev.to/mesak/chuan-song-wu-jian-zhen-lie-dao-formdata-212k</link>
      <guid>https://dev.to/mesak/chuan-song-wu-jian-zhen-lie-dao-formdata-212k</guid>
      <description>&lt;p&gt;JS 自從有了打包工具之後，很多工具造福不少開發者，以往要送個 AJAX 到後端，大多都是一個頁面處理一個 method&lt;/p&gt;

&lt;p&gt;框架化後，每個程式碼開始有了 物件化、封裝方式來做，現在大多都用 axios 來送 AJAX&lt;/p&gt;

&lt;p&gt;但是遇到一個問題就是送檔案的時候，需要使用到 FormData 的物類別物件。&lt;/p&gt;

&lt;p&gt;但是整個像後端送的程式碼都封裝起來了，要針對一個檔案上傳的方法，做出額外處理，當檔案上傳處理頁面多了之後維護除錯方法就有點麻煩。&lt;/p&gt;

&lt;p&gt;所以比較好的方式還是修改 API 送出前，先把物件轉換好判定是否有 &lt;code&gt;File&lt;/code&gt; 類型，然後轉換成 &lt;code&gt;FormData&lt;/code&gt; 就可以一勞永逸&lt;/p&gt;

&lt;p&gt;問題來了，以往直接用 axios 傳送到後端，一個 object 就可以搞定 value is array 的問題，開發者似乎不用煩惱太多後續處理問題&lt;/p&gt;

&lt;p&gt;頂多就是遇到 &lt;code&gt;GET&lt;/code&gt; 網址參數需要轉換純文字的狀態，但也也只需要一個 qs 套件就可以解決解決&lt;/p&gt;

&lt;p&gt;但如果採用 &lt;code&gt;FormData&lt;/code&gt; 這個類型的時候，整個 object 就必須自己處理，如果遇到傳送一個陣列的時候，就必須額外針對 key = array 的狀態去處理，然後如果 value = array 也是很麻煩的事情。&lt;/p&gt;

&lt;p&gt;舉個例子，往後端送 &lt;code&gt;array1 = [1,2,3]&lt;/code&gt; ，如果是 axios 就可以直接送，如果是送 FormData 就必須處理成&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;array1[]:1;
array1[]:2;
array1[]:3;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;這點可以利用 PostMan 來做測試看看後端可以吃到怎樣結構的陣列&lt;/p&gt;

&lt;p&gt;這邊原本的想法是，把物件的每個 value 歷遍，只要 &lt;code&gt;value instanceof File&lt;/code&gt; 就成立條件，把 key 轉換成陣列&lt;/p&gt;

&lt;p&gt;不過找了很多遞迴解法，都沒有 qs 轉換 陣列來的快速有效，最終還是爬了一下 qs 的實際操作用法，&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;arrayFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;brackets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lodash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uniqueId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__FILE__.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;利用 &lt;a href="https://github.com/ljharb/qs"&gt;qs&lt;/a&gt; 的 filter method 把所有 value 檔案移到暫存變數 files 裡面，利用 lodash 取得目前唯一的數值，將原先的 data (object) 的 File 類型 value，全部轉換成 file 的 uniqueId，最終就可以得到一串處理過的字串，快速的將 物件陣列轉換成字串&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a=__FILE__.1&amp;amp;b[a]=1&amp;amp;b[b]=2 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;只要有了這個純字串的東西，要拆解成陣列 + 物件就方便多拉，拆完之後遇到 &lt;strong&gt;FILE&lt;/strong&gt; 的 value 記得去把 files 裡面的檔案給領回來，這樣塞入 &lt;code&gt;FormData&lt;/code&gt; 就可以解決陣列 key 或是 value = array 的問題了&lt;/p&gt;

&lt;p&gt;完整範例放在 &lt;a href="https://codepen.io/mesak/pen/xxpvLGe"&gt;codepen.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>formdata</category>
      <category>axios</category>
    </item>
  </channel>
</rss>
