[{"data":1,"prerenderedAt":1348},["ShallowReactive",2],{"blog-list":3},[4,277,603,969],{"id":5,"title":6,"body":7,"date":264,"description":28,"excerpt":265,"extension":266,"meta":267,"navigation":145,"path":268,"seo":269,"stem":270,"tags":271,"__hash__":276},"blog\u002Fblog\u002Flaravel-query-optimization.md","N+1 Queries in Laravel: Detection and Elimination at Scale",{"type":8,"value":9,"toc":256},"minimark",[10,15,19,22,58,61,65,68,123,127,157,164,184,188,207,210,214,225,245,249,252],[11,12,14],"h2",{"id":13},"the-problem-no-one-talks-about-until-its-too-late","The Problem No One Talks About Until It's Too Late",[16,17,18],"p",{},"Your local environment runs fine. Tests pass. Then you hit 10,000 users and your database server catches fire. N+1 queries are often the culprit.",[16,20,21],{},"Consider a basic relationship:",[23,24,29],"pre",{"className":25,"code":26,"language":27,"meta":28,"style":28},"language-php shiki shiki-themes github-light github-dark","$posts = Post::all();\nforeach ($posts as $post) {\n    echo $post->author->name; \u002F\u002F SELECT * FROM users WHERE id = ?\n}\n","php","",[30,31,32,40,46,52],"code",{"__ignoreMap":28},[33,34,37],"span",{"class":35,"line":36},"line",1,[33,38,39],{},"$posts = Post::all();\n",[33,41,43],{"class":35,"line":42},2,[33,44,45],{},"foreach ($posts as $post) {\n",[33,47,49],{"class":35,"line":48},3,[33,50,51],{},"    echo $post->author->name; \u002F\u002F SELECT * FROM users WHERE id = ?\n",[33,53,55],{"class":35,"line":54},4,[33,56,57],{},"}\n",[16,59,60],{},"For 100 posts, this executes 101 queries. For 10,000 posts: 10,001 queries.",[11,62,64],{"id":63},"detection-with-laravel-telescope","Detection with Laravel Telescope",[16,66,67],{},"Enable query watching in Telescope and filter by request path. Any endpoint showing a query count that scales linearly with record count is a candidate for investigation.",[23,69,71],{"className":25,"code":70,"language":27,"meta":28,"style":28},"\u002F\u002F In a service provider (dev only)\nDB::listen(function ($query) {\n    if ($query->time > 100) { \u002F\u002F milliseconds\n        Log::warning('Slow query', [\n            'sql' => $query->sql,\n            'time' => $query->time,\n        ]);\n    }\n});\n",[30,72,73,78,83,88,93,99,105,111,117],{"__ignoreMap":28},[33,74,75],{"class":35,"line":36},[33,76,77],{},"\u002F\u002F In a service provider (dev only)\n",[33,79,80],{"class":35,"line":42},[33,81,82],{},"DB::listen(function ($query) {\n",[33,84,85],{"class":35,"line":48},[33,86,87],{},"    if ($query->time > 100) { \u002F\u002F milliseconds\n",[33,89,90],{"class":35,"line":54},[33,91,92],{},"        Log::warning('Slow query', [\n",[33,94,96],{"class":35,"line":95},5,[33,97,98],{},"            'sql' => $query->sql,\n",[33,100,102],{"class":35,"line":101},6,[33,103,104],{},"            'time' => $query->time,\n",[33,106,108],{"class":35,"line":107},7,[33,109,110],{},"        ]);\n",[33,112,114],{"class":35,"line":113},8,[33,115,116],{},"    }\n",[33,118,120],{"class":35,"line":119},9,[33,121,122],{},"});\n",[11,124,126],{"id":125},"the-fix-eager-loading","The Fix: Eager Loading",[23,128,130],{"className":25,"code":129,"language":27,"meta":28,"style":28},"\u002F\u002F Bad — triggers a query per iteration\n$posts = Post::all();\n\n\u002F\u002F Good — single JOIN resolves the relationship\n$posts = Post::with(['author', 'tags', 'comments.author'])->get();\n",[30,131,132,137,141,147,152],{"__ignoreMap":28},[33,133,134],{"class":35,"line":36},[33,135,136],{},"\u002F\u002F Bad — triggers a query per iteration\n",[33,138,139],{"class":35,"line":42},[33,140,39],{},[33,142,143],{"class":35,"line":48},[33,144,146],{"emptyLinePlaceholder":145},true,"\n",[33,148,149],{"class":35,"line":54},[33,150,151],{},"\u002F\u002F Good — single JOIN resolves the relationship\n",[33,153,154],{"class":35,"line":95},[33,155,156],{},"$posts = Post::with(['author', 'tags', 'comments.author'])->get();\n",[16,158,159,160,163],{},"For conditional relationships, use ",[30,161,162],{},"when",":",[23,165,167],{"className":25,"code":166,"language":27,"meta":28,"style":28},"$posts = Post::with(['author'])\n    ->when($request->include_comments, fn($q) => $q->with('comments'))\n    ->paginate(20);\n",[30,168,169,174,179],{"__ignoreMap":28},[33,170,171],{"class":35,"line":36},[33,172,173],{},"$posts = Post::with(['author'])\n",[33,175,176],{"class":35,"line":42},[33,177,178],{},"    ->when($request->include_comments, fn($q) => $q->with('comments'))\n",[33,180,181],{"class":35,"line":48},[33,182,183],{},"    ->paginate(20);\n",[11,185,187],{"id":186},"select-only-what-you-need","Select Only What You Need",[23,189,191],{"className":25,"code":190,"language":27,"meta":28,"style":28},"Post::with(['author:id,name,avatar'])\n    ->select(['id', 'title', 'user_id', 'created_at'])\n    ->paginate(20);\n",[30,192,193,198,203],{"__ignoreMap":28},[33,194,195],{"class":35,"line":36},[33,196,197],{},"Post::with(['author:id,name,avatar'])\n",[33,199,200],{"class":35,"line":42},[33,201,202],{},"    ->select(['id', 'title', 'user_id', 'created_at'])\n",[33,204,205],{"class":35,"line":48},[33,206,183],{},[16,208,209],{},"Selecting specific columns dramatically reduces memory usage and network transfer between your app and database. At scale, this matters more than eager loading alone.",[11,211,213],{"id":212},"lazy-eager-loading-for-conditional-data","Lazy Eager Loading for Conditional Data",[16,215,216,217,220,221,224],{},"Sometimes you need to load relationships after the initial query. Use ",[30,218,219],{},"loadMissing"," instead of ",[30,222,223],{},"load"," to avoid redundant queries when the relationship may already be loaded:",[23,226,228],{"className":25,"code":227,"language":27,"meta":28,"style":28},"$posts->loadMissing(['comments' => function ($query) {\n    $query->where('approved', true)->with('author:id,name');\n}]);\n",[30,229,230,235,240],{"__ignoreMap":28},[33,231,232],{"class":35,"line":36},[33,233,234],{},"$posts->loadMissing(['comments' => function ($query) {\n",[33,236,237],{"class":35,"line":42},[33,238,239],{},"    $query->where('approved', true)->with('author:id,name');\n",[33,241,242],{"class":35,"line":48},[33,243,244],{},"}]);\n",[11,246,248],{"id":247},"monitoring-in-production","Monitoring in Production",[16,250,251],{},"The Debugbar package adds a query count badge in dev. For production, track query counts per request in your APM (Datadog, New Relic) and alert when any endpoint exceeds a threshold. Catching a 50-query endpoint before it reaches 50,000 users is always cheaper than the firefight after.",[253,254,255],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":28,"searchDepth":42,"depth":42,"links":257},[258,259,260,261,262,263],{"id":13,"depth":42,"text":14},{"id":63,"depth":42,"text":64},{"id":125,"depth":42,"text":126},{"id":186,"depth":42,"text":187},{"id":212,"depth":42,"text":213},{"id":247,"depth":42,"text":248},"2024-11-15",null,"md",{},"\u002Fblog\u002Flaravel-query-optimization",{"title":6,"description":28},"blog\u002Flaravel-query-optimization",[272,273,274,275],"Laravel","PHP","MySQL","Performance","WI39NZWbsoMpKtYn4pAdM5wJO-5GYRFcv_LSmJ1uTu4",{"id":278,"title":279,"body":280,"date":594,"description":28,"excerpt":265,"extension":266,"meta":595,"navigation":145,"path":596,"seo":597,"stem":598,"tags":599,"__hash__":602},"blog\u002Fblog\u002Fredis-caching-patterns.md","Redis Caching Patterns for Laravel APIs: Beyond Cache::remember()",{"type":8,"value":281,"toc":585},[282,290,295,299,302,336,339,343,346,430,434,437,486,489,493,504,533,536,540,543,550,574,580,583],[11,283,285,286,289],{"id":284},"why-cacheremember-isnt-enough","Why ",[30,287,288],{},"Cache::remember()"," Isn't Enough",[16,291,292,294],{},[30,293,288],{}," is great for simple cases. Production cache architecture requires thinking about key namespacing, TTL strategy, stampede protection, and invalidation. Getting any of these wrong under load can cause more damage than having no cache at all.",[11,296,298],{"id":297},"key-namespacing-strategy","Key Namespacing Strategy",[16,300,301],{},"Flat cache keys are a ticking time bomb. They collide across deployments, tenants, and feature versions.",[23,303,305],{"className":25,"code":304,"language":27,"meta":28,"style":28},"\u002F\u002F Bad: flat, unversioned, will collide\nCache::remember('users', 3600, fn() => User::all());\n\n\u002F\u002F Good: versioned, tenant-scoped, descriptive\n$key = \"v2:tenant:{$tenantId}:users:active:page:{$page}\";\nCache::remember($key, 3600, fn() => ...);\n",[30,306,307,312,317,321,326,331],{"__ignoreMap":28},[33,308,309],{"class":35,"line":36},[33,310,311],{},"\u002F\u002F Bad: flat, unversioned, will collide\n",[33,313,314],{"class":35,"line":42},[33,315,316],{},"Cache::remember('users', 3600, fn() => User::all());\n",[33,318,319],{"class":35,"line":48},[33,320,146],{"emptyLinePlaceholder":145},[33,322,323],{"class":35,"line":54},[33,324,325],{},"\u002F\u002F Good: versioned, tenant-scoped, descriptive\n",[33,327,328],{"class":35,"line":95},[33,329,330],{},"$key = \"v2:tenant:{$tenantId}:users:active:page:{$page}\";\n",[33,332,333],{"class":35,"line":101},[33,334,335],{},"Cache::remember($key, 3600, fn() => ...);\n",[16,337,338],{},"Add a version prefix. When your data model changes, bump the version — old keys expire naturally without a manual flush.",[11,340,342],{"id":341},"cache-stampede-protection","Cache Stampede Protection",[16,344,345],{},"When a cached item expires and 1,000 concurrent requests try to regenerate it simultaneously, your database dies. Laravel's atomic locks solve this cleanly:",[23,347,349],{"className":25,"code":348,"language":27,"meta":28,"style":28},"$data = Cache::get($key);\nif (!$data) {\n    $lock = Cache::lock(\"lock:{$key}\", 10);\n    try {\n        if ($lock->get()) {\n            $data = expensiveQuery();\n            Cache::put($key, $data, 3600);\n        } else {\n            $lock->block(5); \u002F\u002F wait up to 5s for the other process\n            $data = Cache::get($key);\n        }\n    } finally {\n        optional($lock)->release();\n    }\n}\n",[30,350,351,356,361,366,371,376,381,386,391,396,402,408,414,420,425],{"__ignoreMap":28},[33,352,353],{"class":35,"line":36},[33,354,355],{},"$data = Cache::get($key);\n",[33,357,358],{"class":35,"line":42},[33,359,360],{},"if (!$data) {\n",[33,362,363],{"class":35,"line":48},[33,364,365],{},"    $lock = Cache::lock(\"lock:{$key}\", 10);\n",[33,367,368],{"class":35,"line":54},[33,369,370],{},"    try {\n",[33,372,373],{"class":35,"line":95},[33,374,375],{},"        if ($lock->get()) {\n",[33,377,378],{"class":35,"line":101},[33,379,380],{},"            $data = expensiveQuery();\n",[33,382,383],{"class":35,"line":107},[33,384,385],{},"            Cache::put($key, $data, 3600);\n",[33,387,388],{"class":35,"line":113},[33,389,390],{},"        } else {\n",[33,392,393],{"class":35,"line":119},[33,394,395],{},"            $lock->block(5); \u002F\u002F wait up to 5s for the other process\n",[33,397,399],{"class":35,"line":398},10,[33,400,401],{},"            $data = Cache::get($key);\n",[33,403,405],{"class":35,"line":404},11,[33,406,407],{},"        }\n",[33,409,411],{"class":35,"line":410},12,[33,412,413],{},"    } finally {\n",[33,415,417],{"class":35,"line":416},13,[33,418,419],{},"        optional($lock)->release();\n",[33,421,423],{"class":35,"line":422},14,[33,424,116],{},[33,426,428],{"class":35,"line":427},15,[33,429,57],{},[11,431,433],{"id":432},"redis-hash-for-partial-updates","Redis Hash for Partial Updates",[16,435,436],{},"Instead of caching a full user object as a serialized blob (requiring a full rewrite on any change), use Redis hashes:",[23,438,440],{"className":25,"code":439,"language":27,"meta":28,"style":28},"Redis::hmset(\"user:{$id}\", [\n    'name'  => $user->name,\n    'email' => $user->email,\n    'plan'  => $user->plan,\n]);\nRedis::expire(\"user:{$id}\", 3600);\n\n\u002F\u002F Update just the plan without touching the rest of the cached data\nRedis::hset(\"user:{$id}\", 'plan', 'enterprise');\n",[30,441,442,447,452,457,462,467,472,476,481],{"__ignoreMap":28},[33,443,444],{"class":35,"line":36},[33,445,446],{},"Redis::hmset(\"user:{$id}\", [\n",[33,448,449],{"class":35,"line":42},[33,450,451],{},"    'name'  => $user->name,\n",[33,453,454],{"class":35,"line":48},[33,455,456],{},"    'email' => $user->email,\n",[33,458,459],{"class":35,"line":54},[33,460,461],{},"    'plan'  => $user->plan,\n",[33,463,464],{"class":35,"line":95},[33,465,466],{},"]);\n",[33,468,469],{"class":35,"line":101},[33,470,471],{},"Redis::expire(\"user:{$id}\", 3600);\n",[33,473,474],{"class":35,"line":107},[33,475,146],{"emptyLinePlaceholder":145},[33,477,478],{"class":35,"line":113},[33,479,480],{},"\u002F\u002F Update just the plan without touching the rest of the cached data\n",[33,482,483],{"class":35,"line":119},[33,484,485],{},"Redis::hset(\"user:{$id}\", 'plan', 'enterprise');\n",[16,487,488],{},"This pattern eliminates the cache invalidation problem for objects with multiple independent fields.",[11,490,492],{"id":491},"sorted-sets-for-leaderboards-and-feeds","Sorted Sets for Leaderboards and Feeds",[16,494,495,496,499,500,503],{},"Redis sorted sets (",[30,497,498],{},"ZADD",", ",[30,501,502],{},"ZRANGE",") are ideal for ranked data:",[23,505,507],{"className":25,"code":506,"language":27,"meta":28,"style":28},"\u002F\u002F Increment a user's score\nRedis::zadd('leaderboard:monthly', ['increment' => true], 10, \"user:{$userId}\");\n\n\u002F\u002F Fetch top 10\n$top10 = Redis::zrevrange('leaderboard:monthly', 0, 9, 'WITHSCORES');\n",[30,508,509,514,519,523,528],{"__ignoreMap":28},[33,510,511],{"class":35,"line":36},[33,512,513],{},"\u002F\u002F Increment a user's score\n",[33,515,516],{"class":35,"line":42},[33,517,518],{},"Redis::zadd('leaderboard:monthly', ['increment' => true], 10, \"user:{$userId}\");\n",[33,520,521],{"class":35,"line":48},[33,522,146],{"emptyLinePlaceholder":145},[33,524,525],{"class":35,"line":54},[33,526,527],{},"\u002F\u002F Fetch top 10\n",[33,529,530],{"class":35,"line":95},[33,531,532],{},"$top10 = Redis::zrevrange('leaderboard:monthly', 0, 9, 'WITHSCORES');\n",[16,534,535],{},"No SQL aggregate query. No locks. O(log N) writes, O(log N + M) reads.",[11,537,539],{"id":538},"cache-invalidation-strategies","Cache Invalidation Strategies",[16,541,542],{},"The two practical approaches:",[16,544,545,549],{},[546,547,548],"strong",{},"Tag-based invalidation"," (Laravel's built-in support):",[23,551,553],{"className":25,"code":552,"language":27,"meta":28,"style":28},"Cache::tags(['posts', \"user:{$userId}\"])->remember($key, 3600, fn() => ...);\n\n\u002F\u002F Later, flush everything tagged 'posts'\nCache::tags(['posts'])->flush();\n",[30,554,555,560,564,569],{"__ignoreMap":28},[33,556,557],{"class":35,"line":36},[33,558,559],{},"Cache::tags(['posts', \"user:{$userId}\"])->remember($key, 3600, fn() => ...);\n",[33,561,562],{"class":35,"line":42},[33,563,146],{"emptyLinePlaceholder":145},[33,565,566],{"class":35,"line":48},[33,567,568],{},"\u002F\u002F Later, flush everything tagged 'posts'\n",[33,570,571],{"class":35,"line":54},[33,572,573],{},"Cache::tags(['posts'])->flush();\n",[16,575,576,579],{},[546,577,578],{},"Event-driven invalidation",": Listen for model events and explicitly delete affected keys. More granular, more code, but no risk of tag-based flush wiping unrelated data.",[16,581,582],{},"Choose tags for simplicity, events for precision.",[253,584,255],{},{"title":28,"searchDepth":42,"depth":42,"links":586},[587,589,590,591,592,593],{"id":284,"depth":42,"text":588},"Why Cache::remember() Isn't Enough",{"id":297,"depth":42,"text":298},{"id":341,"depth":42,"text":342},{"id":432,"depth":42,"text":433},{"id":491,"depth":42,"text":492},{"id":538,"depth":42,"text":539},"2024-10-02",{},"\u002Fblog\u002Fredis-caching-patterns",{"title":279,"description":28},"blog\u002Fredis-caching-patterns",[600,272,273,601],"Redis","Caching","zu8lce5xoIIz1S5zueMC38N-5RRikwPb5z2Q8Vomz9Y",{"id":604,"title":605,"body":606,"date":959,"description":28,"excerpt":265,"extension":266,"meta":960,"navigation":145,"path":961,"seo":962,"stem":963,"tags":964,"__hash__":968},"blog\u002Fblog\u002Fphp8-fibers-async.md","PHP 8.1 Fibers: Writing Cooperative Concurrency Without a Framework",{"type":8,"value":607,"toc":952},[608,612,615,663,683,687,690,810,813,817,906,910,917,920,924,947,950],[11,609,611],{"id":610},"what-fibers-actually-are","What Fibers Actually Are",[16,613,614],{},"A Fiber is a stackful coroutine — a function that can suspend its own execution and yield control back to the caller, then be resumed from exactly where it stopped. No OS threads. No shared memory. Full control of the scheduler.",[23,616,618],{"className":25,"code":617,"language":27,"meta":28,"style":28},"$fiber = new Fiber(function(): string {\n    $value = Fiber::suspend('first suspension');\n    echo \"Resumed with: {$value}\\n\";\n    return 'fiber complete';\n});\n\n$firstValue  = $fiber->start();          \u002F\u002F returns 'first suspension'\n$returnValue = $fiber->resume('hello');  \u002F\u002F prints \"Resumed with: hello\"\n                                         \u002F\u002F $returnValue === 'fiber complete'\n",[30,619,620,625,630,635,640,644,648,653,658],{"__ignoreMap":28},[33,621,622],{"class":35,"line":36},[33,623,624],{},"$fiber = new Fiber(function(): string {\n",[33,626,627],{"class":35,"line":42},[33,628,629],{},"    $value = Fiber::suspend('first suspension');\n",[33,631,632],{"class":35,"line":48},[33,633,634],{},"    echo \"Resumed with: {$value}\\n\";\n",[33,636,637],{"class":35,"line":54},[33,638,639],{},"    return 'fiber complete';\n",[33,641,642],{"class":35,"line":95},[33,643,122],{},[33,645,646],{"class":35,"line":101},[33,647,146],{"emptyLinePlaceholder":145},[33,649,650],{"class":35,"line":107},[33,651,652],{},"$firstValue  = $fiber->start();          \u002F\u002F returns 'first suspension'\n",[33,654,655],{"class":35,"line":113},[33,656,657],{},"$returnValue = $fiber->resume('hello');  \u002F\u002F prints \"Resumed with: hello\"\n",[33,659,660],{"class":35,"line":119},[33,661,662],{},"                                         \u002F\u002F $returnValue === 'fiber complete'\n",[16,664,665,666,669,670,674,675,678,679,682],{},"The key insight: ",[30,667,668],{},"Fiber::suspend()"," is called from ",[671,672,673],"em",{},"inside"," the fiber, not outside it. The caller gets back whatever was passed to ",[30,676,677],{},"suspend()"," and can send data back via ",[30,680,681],{},"resume()",".",[11,684,686],{"id":685},"building-a-simple-cooperative-scheduler","Building a Simple Cooperative Scheduler",[16,688,689],{},"The real power emerges when you run multiple fibers through a scheduler loop:",[23,691,693],{"className":25,"code":692,"language":27,"meta":28,"style":28},"class Scheduler\n{\n    private array $fibers = [];\n\n    public function add(Fiber $fiber): void\n    {\n        $this->fibers[] = $fiber;\n    }\n\n    public function run(): void\n    {\n        while ($this->fibers) {\n            foreach ($this->fibers as $key => $fiber) {\n                if (!$fiber->isStarted())       $fiber->start();\n                elseif ($fiber->isSuspended())  $fiber->resume();\n\n                if ($fiber->isTerminated()) {\n                    unset($this->fibers[$key]);\n                }\n            }\n        }\n    }\n}\n",[30,694,695,700,705,710,714,719,724,729,733,737,742,746,751,756,761,766,771,777,783,789,795,800,805],{"__ignoreMap":28},[33,696,697],{"class":35,"line":36},[33,698,699],{},"class Scheduler\n",[33,701,702],{"class":35,"line":42},[33,703,704],{},"{\n",[33,706,707],{"class":35,"line":48},[33,708,709],{},"    private array $fibers = [];\n",[33,711,712],{"class":35,"line":54},[33,713,146],{"emptyLinePlaceholder":145},[33,715,716],{"class":35,"line":95},[33,717,718],{},"    public function add(Fiber $fiber): void\n",[33,720,721],{"class":35,"line":101},[33,722,723],{},"    {\n",[33,725,726],{"class":35,"line":107},[33,727,728],{},"        $this->fibers[] = $fiber;\n",[33,730,731],{"class":35,"line":113},[33,732,116],{},[33,734,735],{"class":35,"line":119},[33,736,146],{"emptyLinePlaceholder":145},[33,738,739],{"class":35,"line":398},[33,740,741],{},"    public function run(): void\n",[33,743,744],{"class":35,"line":404},[33,745,723],{},[33,747,748],{"class":35,"line":410},[33,749,750],{},"        while ($this->fibers) {\n",[33,752,753],{"class":35,"line":416},[33,754,755],{},"            foreach ($this->fibers as $key => $fiber) {\n",[33,757,758],{"class":35,"line":422},[33,759,760],{},"                if (!$fiber->isStarted())       $fiber->start();\n",[33,762,763],{"class":35,"line":427},[33,764,765],{},"                elseif ($fiber->isSuspended())  $fiber->resume();\n",[33,767,769],{"class":35,"line":768},16,[33,770,146],{"emptyLinePlaceholder":145},[33,772,774],{"class":35,"line":773},17,[33,775,776],{},"                if ($fiber->isTerminated()) {\n",[33,778,780],{"class":35,"line":779},18,[33,781,782],{},"                    unset($this->fibers[$key]);\n",[33,784,786],{"class":35,"line":785},19,[33,787,788],{},"                }\n",[33,790,792],{"class":35,"line":791},20,[33,793,794],{},"            }\n",[33,796,798],{"class":35,"line":797},21,[33,799,407],{},[33,801,803],{"class":35,"line":802},22,[33,804,116],{},[33,806,808],{"class":35,"line":807},23,[33,809,57],{},[16,811,812],{},"Each iteration of the outer loop ticks all registered fibers once. When a fiber suspends, the scheduler moves on to the next one. This is cooperative multitasking — no OS involvement, fully deterministic.",[11,814,816],{"id":815},"practical-use-case-non-blocking-io-simulation","Practical Use Case: Non-Blocking I\u002FO Simulation",[23,818,820],{"className":25,"code":819,"language":27,"meta":28,"style":28},"function fetchUrl(string $url): Generator\n{\n    \u002F\u002F In a real event loop, this would be non-blocking socket I\u002FO.\n    \u002F\u002F Here we simulate it with a suspend point.\n    Fiber::suspend('waiting');\n    return file_get_contents($url);\n}\n\n$scheduler = new Scheduler();\n\nforeach ($urls as $url) {\n    $scheduler->add(new Fiber(function() use ($url) {\n        $result = yield from fetchUrl($url);\n        echo \"Got \" . strlen($result) . \" bytes from {$url}\\n\";\n    }));\n}\n\n$scheduler->run();\n",[30,821,822,827,831,836,841,846,851,855,859,864,868,873,878,883,888,893,897,901],{"__ignoreMap":28},[33,823,824],{"class":35,"line":36},[33,825,826],{},"function fetchUrl(string $url): Generator\n",[33,828,829],{"class":35,"line":42},[33,830,704],{},[33,832,833],{"class":35,"line":48},[33,834,835],{},"    \u002F\u002F In a real event loop, this would be non-blocking socket I\u002FO.\n",[33,837,838],{"class":35,"line":54},[33,839,840],{},"    \u002F\u002F Here we simulate it with a suspend point.\n",[33,842,843],{"class":35,"line":95},[33,844,845],{},"    Fiber::suspend('waiting');\n",[33,847,848],{"class":35,"line":101},[33,849,850],{},"    return file_get_contents($url);\n",[33,852,853],{"class":35,"line":107},[33,854,57],{},[33,856,857],{"class":35,"line":113},[33,858,146],{"emptyLinePlaceholder":145},[33,860,861],{"class":35,"line":119},[33,862,863],{},"$scheduler = new Scheduler();\n",[33,865,866],{"class":35,"line":398},[33,867,146],{"emptyLinePlaceholder":145},[33,869,870],{"class":35,"line":404},[33,871,872],{},"foreach ($urls as $url) {\n",[33,874,875],{"class":35,"line":410},[33,876,877],{},"    $scheduler->add(new Fiber(function() use ($url) {\n",[33,879,880],{"class":35,"line":416},[33,881,882],{},"        $result = yield from fetchUrl($url);\n",[33,884,885],{"class":35,"line":422},[33,886,887],{},"        echo \"Got \" . strlen($result) . \" bytes from {$url}\\n\";\n",[33,889,890],{"class":35,"line":427},[33,891,892],{},"    }));\n",[33,894,895],{"class":35,"line":768},[33,896,57],{},[33,898,899],{"class":35,"line":773},[33,900,146],{"emptyLinePlaceholder":145},[33,902,903],{"class":35,"line":779},[33,904,905],{},"$scheduler->run();\n",[11,907,909],{"id":908},"the-relationship-to-reactphp-and-amphp","The Relationship to ReactPHP and AMPHP",[16,911,912,913,916],{},"Fibers are the foundation that modern PHP async frameworks build on. Before PHP 8.1, ReactPHP used generators and a complex callback chain. Now both ReactPHP and AMPHP use Fibers as their suspension primitive, giving you async I\u002FO with code that ",[671,914,915],{},"looks"," sequential.",[16,918,919],{},"Understanding Fibers at this level makes frameworks like AMPHP far less magical — you can debug their scheduler, reason about execution order, and write extensions without cargo-culting patterns.",[11,921,923],{"id":922},"what-fibers-are-not","What Fibers Are Not",[925,926,927,935,941],"ul",{},[928,929,930,931,934],"li",{},"They are ",[546,932,933],{},"not threads"," — no parallelism, no shared-memory races",[928,936,930,937,940],{},[546,938,939],{},"not true async"," — they need an event loop to be useful for I\u002FO",[928,942,930,943,946],{},[546,944,945],{},"not a replacement for queues"," — CPU-bound work still blocks the process",[16,948,949],{},"Use Fibers for I\u002FO multiplexing and cooperative scheduling. Use queues (Laravel Horizon, Beanstalkd) for CPU-bound background work.",[253,951,255],{},{"title":28,"searchDepth":42,"depth":42,"links":953},[954,955,956,957,958],{"id":610,"depth":42,"text":611},{"id":685,"depth":42,"text":686},{"id":815,"depth":42,"text":816},{"id":908,"depth":42,"text":909},{"id":922,"depth":42,"text":923},"2024-08-20",{},"\u002Fblog\u002Fphp8-fibers-async",{"title":605,"description":28},"blog\u002Fphp8-fibers-async",[273,965,966,967],"PHP 8.1","Fibers","Concurrency","IHFdqB-3rWfDTaO2x_NrOL-JGFZ-yvzQBgpYdk5KzUI",{"id":970,"title":971,"body":972,"date":1339,"description":28,"excerpt":265,"extension":266,"meta":1340,"navigation":145,"path":1341,"seo":1342,"stem":1343,"tags":1344,"__hash__":1347},"blog\u002Fblog\u002Fpostgresql-jsonb-laravel.md","Using PostgreSQL JSONB Columns in Laravel Without Losing Your Mind",{"type":8,"value":973,"toc":1330},[974,978,981,1001,1004,1008,1056,1063,1067,1074,1131,1135,1142,1202,1206,1221,1238,1241,1250,1253,1262,1266,1277,1306,1317,1321,1328],[11,975,977],{"id":976},"when-jsonb-makes-sense","When JSONB Makes Sense",[16,979,980],{},"JSONB is not a replacement for relational design. It shines when:",[925,982,983,989,995],{},[928,984,985,988],{},[546,986,987],{},"Schema varies per record"," — product attributes, event metadata, feature flags",[928,990,991,994],{},[546,992,993],{},"Third-party data"," — you're storing API responses you don't fully control",[928,996,997,1000],{},[546,998,999],{},"Rapid iteration"," — the schema needs to evolve before normalization is justified",[16,1002,1003],{},"The anti-pattern is using JSONB to avoid designing a proper schema. If you know every field, use columns.",[11,1005,1007],{"id":1006},"migration-and-index-setup","Migration and Index Setup",[23,1009,1011],{"className":25,"code":1010,"language":27,"meta":28,"style":28},"Schema::create('products', function (Blueprint $table) {\n    $table->id();\n    $table->string('name');\n    $table->jsonb('attributes')->default('{}');\n    $table->timestamps();\n});\n\n\u002F\u002F GIN index enables containment and key-existence queries\nDB::statement('CREATE INDEX idx_products_attributes ON products USING GIN (attributes)');\n",[30,1012,1013,1018,1023,1028,1033,1038,1042,1046,1051],{"__ignoreMap":28},[33,1014,1015],{"class":35,"line":36},[33,1016,1017],{},"Schema::create('products', function (Blueprint $table) {\n",[33,1019,1020],{"class":35,"line":42},[33,1021,1022],{},"    $table->id();\n",[33,1024,1025],{"class":35,"line":48},[33,1026,1027],{},"    $table->string('name');\n",[33,1029,1030],{"class":35,"line":54},[33,1031,1032],{},"    $table->jsonb('attributes')->default('{}');\n",[33,1034,1035],{"class":35,"line":95},[33,1036,1037],{},"    $table->timestamps();\n",[33,1039,1040],{"class":35,"line":101},[33,1041,122],{},[33,1043,1044],{"class":35,"line":107},[33,1045,146],{"emptyLinePlaceholder":145},[33,1047,1048],{"class":35,"line":113},[33,1049,1050],{},"\u002F\u002F GIN index enables containment and key-existence queries\n",[33,1052,1053],{"class":35,"line":119},[33,1054,1055],{},"DB::statement('CREATE INDEX idx_products_attributes ON products USING GIN (attributes)');\n",[16,1057,1058,1059,1062],{},"The GIN index is critical. Without it, every ",[30,1060,1061],{},"WHERE attributes @> '{\"color\":\"red\"}'"," query is a full table scan.",[11,1064,1066],{"id":1065},"querying-with-eloquent","Querying with Eloquent",[16,1068,1069,1070,1073],{},"Laravel exposes JSONB querying through ",[30,1071,1072],{},"whereJsonContains"," and raw expressions:",[23,1075,1077],{"className":25,"code":1076,"language":27,"meta":28,"style":28},"\u002F\u002F Containment: find products where attributes contains {color: \"red\"}\nProduct::whereJsonContains('attributes->color', 'red')->get();\n\n\u002F\u002F Numeric comparison via cast\nProduct::whereRaw(\"(attributes->>'weight')::numeric > ?\", [10.0])->get();\n\n\u002F\u002F Key existence\nProduct::whereRaw(\"attributes ? 'warranty'\")->get();\n\n\u002F\u002F Nested path\nProduct::whereJsonContains('attributes->dimensions->unit', 'cm')->get();\n",[30,1078,1079,1084,1089,1093,1098,1103,1107,1112,1117,1121,1126],{"__ignoreMap":28},[33,1080,1081],{"class":35,"line":36},[33,1082,1083],{},"\u002F\u002F Containment: find products where attributes contains {color: \"red\"}\n",[33,1085,1086],{"class":35,"line":42},[33,1087,1088],{},"Product::whereJsonContains('attributes->color', 'red')->get();\n",[33,1090,1091],{"class":35,"line":48},[33,1092,146],{"emptyLinePlaceholder":145},[33,1094,1095],{"class":35,"line":54},[33,1096,1097],{},"\u002F\u002F Numeric comparison via cast\n",[33,1099,1100],{"class":35,"line":95},[33,1101,1102],{},"Product::whereRaw(\"(attributes->>'weight')::numeric > ?\", [10.0])->get();\n",[33,1104,1105],{"class":35,"line":101},[33,1106,146],{"emptyLinePlaceholder":145},[33,1108,1109],{"class":35,"line":107},[33,1110,1111],{},"\u002F\u002F Key existence\n",[33,1113,1114],{"class":35,"line":113},[33,1115,1116],{},"Product::whereRaw(\"attributes ? 'warranty'\")->get();\n",[33,1118,1119],{"class":35,"line":119},[33,1120,146],{"emptyLinePlaceholder":145},[33,1122,1123],{"class":35,"line":398},[33,1124,1125],{},"\u002F\u002F Nested path\n",[33,1127,1128],{"class":35,"line":404},[33,1129,1130],{},"Product::whereJsonContains('attributes->dimensions->unit', 'cm')->get();\n",[11,1132,1134],{"id":1133},"eloquent-casting-for-type-safety","Eloquent Casting for Type Safety",[16,1136,1137,1138,1141],{},"Without a cast, ",[30,1139,1140],{},"attributes"," comes back as a JSON string. Add the cast:",[23,1143,1145],{"className":25,"code":1144,"language":27,"meta":28,"style":28},"class Product extends Model\n{\n    protected $casts = [\n        'attributes' => 'array', \u002F\u002F or AsCollection::class for a fluent interface\n    ];\n}\n\n\u002F\u002F Now you can read\u002Fwrite like a PHP array\n$product = Product::find(1);\n$product->attributes['color'] = 'blue';\n$product->save(); \u002F\u002F Eloquent serializes it back to JSON automatically\n",[30,1146,1147,1152,1156,1161,1169,1174,1178,1182,1187,1192,1197],{"__ignoreMap":28},[33,1148,1149],{"class":35,"line":36},[33,1150,1151],{},"class Product extends Model\n",[33,1153,1154],{"class":35,"line":42},[33,1155,704],{},[33,1157,1158],{"class":35,"line":48},[33,1159,1160],{},"    protected $casts = [\n",[33,1162,1163,1166],{"class":35,"line":54},[33,1164,1165],{},"        'attributes' => 'array',",[33,1167,1168],{}," \u002F\u002F or AsCollection::class for a fluent interface\n",[33,1170,1171],{"class":35,"line":95},[33,1172,1173],{},"    ];\n",[33,1175,1176],{"class":35,"line":101},[33,1177,57],{},[33,1179,1180],{"class":35,"line":107},[33,1181,146],{"emptyLinePlaceholder":145},[33,1183,1184],{"class":35,"line":113},[33,1185,1186],{},"\u002F\u002F Now you can read\u002Fwrite like a PHP array\n",[33,1188,1189],{"class":35,"line":119},[33,1190,1191],{},"$product = Product::find(1);\n",[33,1193,1194],{"class":35,"line":398},[33,1195,1196],{},"$product->attributes['color'] = 'blue';\n",[33,1198,1199],{"class":35,"line":404},[33,1200,1201],{},"$product->save(); \u002F\u002F Eloquent serializes it back to JSON automatically\n",[11,1203,1205],{"id":1204},"the-gotcha-when-the-gin-index-doesnt-help","The Gotcha: When the GIN Index Doesn't Help",[16,1207,1208,1209,1212,1213,1216,1217,1220],{},"The GIN index accelerates ",[30,1210,1211],{},"@>"," (containment) and ",[30,1214,1215],{},"?"," (key exists). It does ",[546,1218,1219],{},"not"," help extraction with comparison:",[23,1222,1226],{"className":1223,"code":1224,"language":1225,"meta":28,"style":28},"language-sql shiki shiki-themes github-light github-dark","-- NOT covered by GIN — full table scan\nWHERE attributes->>'weight' > '10'\n","sql",[30,1227,1228,1233],{"__ignoreMap":28},[33,1229,1230],{"class":35,"line":36},[33,1231,1232],{},"-- NOT covered by GIN — full table scan\n",[33,1234,1235],{"class":35,"line":42},[33,1236,1237],{},"WHERE attributes->>'weight' > '10'\n",[16,1239,1240],{},"For those queries, add a functional B-tree index:",[23,1242,1244],{"className":1223,"code":1243,"language":1225,"meta":28,"style":28},"CREATE INDEX idx_products_weight ON products ((attributes->>'weight'));\n",[30,1245,1246],{"__ignoreMap":28},[33,1247,1248],{"class":35,"line":36},[33,1249,1243],{},[16,1251,1252],{},"And cast properly in the query:",[23,1254,1256],{"className":1223,"code":1255,"language":1225,"meta":28,"style":28},"CREATE INDEX idx_products_weight ON products (((attributes->>'weight')::numeric));\n",[30,1257,1258],{"__ignoreMap":28},[33,1259,1260],{"class":35,"line":36},[33,1261,1255],{},[11,1263,1265],{"id":1264},"updating-nested-fields","Updating Nested Fields",[16,1267,1268,1269,1272,1273,1276],{},"The ",[30,1270,1271],{},"-"," and ",[30,1274,1275],{},"||"," operators let you update JSONB without fetching the whole record:",[23,1278,1280],{"className":1223,"code":1279,"language":1225,"meta":28,"style":28},"-- Remove a key\nUPDATE products SET attributes = attributes - 'legacy_field';\n\n-- Merge\u002Fupdate a nested path (requires PostgreSQL 14+)\nUPDATE products SET attributes = jsonb_set(attributes, '{dimensions,unit}', '\"mm\"');\n",[30,1281,1282,1287,1292,1296,1301],{"__ignoreMap":28},[33,1283,1284],{"class":35,"line":36},[33,1285,1286],{},"-- Remove a key\n",[33,1288,1289],{"class":35,"line":42},[33,1290,1291],{},"UPDATE products SET attributes = attributes - 'legacy_field';\n",[33,1293,1294],{"class":35,"line":48},[33,1295,146],{"emptyLinePlaceholder":145},[33,1297,1298],{"class":35,"line":54},[33,1299,1300],{},"-- Merge\u002Fupdate a nested path (requires PostgreSQL 14+)\n",[33,1302,1303],{"class":35,"line":95},[33,1304,1305],{},"UPDATE products SET attributes = jsonb_set(attributes, '{dimensions,unit}', '\"mm\"');\n",[16,1307,1308,1309,1312,1313,1316],{},"In Laravel, wrap these in ",[30,1310,1311],{},"DB::statement()"," or a raw update for bulk operations — Eloquent's ",[30,1314,1315],{},"save()"," always replaces the entire column.",[11,1318,1320],{"id":1319},"when-to-stop-using-jsonb","When to Stop Using JSONB",[16,1322,1323,1324,1327],{},"Once you find yourself writing ",[30,1325,1326],{},"jsonb_path_query"," expressions in production queries, it's time to normalize. The flexibility tax compounds — indexes multiply, query complexity grows, and future schema migrations become painful. JSONB buys you iteration speed early on; relational design pays dividends at scale.",[253,1329,255],{},{"title":28,"searchDepth":42,"depth":42,"links":1331},[1332,1333,1334,1335,1336,1337,1338],{"id":976,"depth":42,"text":977},{"id":1006,"depth":42,"text":1007},{"id":1065,"depth":42,"text":1066},{"id":1133,"depth":42,"text":1134},{"id":1204,"depth":42,"text":1205},{"id":1264,"depth":42,"text":1265},{"id":1319,"depth":42,"text":1320},"2024-06-10",{},"\u002Fblog\u002Fpostgresql-jsonb-laravel",{"title":971,"description":28},"blog\u002Fpostgresql-jsonb-laravel",[1345,272,273,1346],"PostgreSQL","Database","mpi6vEOBrgjrhzAt80bdmJHN79AcFSkJa05qc48msTU",1779430667058]