[{"data":1,"prerenderedAt":2801},["ShallowReactive",2],{"post-laravel-13-deep-dive":3},{"id":4,"title":5,"author":6,"body":7,"category":2782,"cover":2783,"date":2784,"description":2785,"extension":2786,"featured":127,"meta":2787,"navigation":127,"path":2788,"published":127,"readingTime":2789,"seo":2790,"sitemap":2791,"stem":2792,"tags":2793,"updated":2784,"__hash__":2800},"posts/posts/laravel-13-deep-dive.md","Laravel 13: A Deep Technical Dive Into What Actually Changed Under the Hood","Manash Sonowal",{"type":8,"value":9,"toc":2757},"minimark",[10,14,17,22,38,41,45,53,60,65,76,286,289,435,438,447,458,499,502,506,590,594,609,847,857,870,941,945,952,1144,1157,1161,1298,1305,1309,1458,1464,1468,1755,1768,1772,1779,1848,1855,1859,1891,1894,1898,1999,2003,2014,2116,2126,2130,2151,2155,2158,2273,2276,2290,2294,2305,2308,2361,2364,2368,2375,2395,2601,2604,2608,2611,2617,2627,2631,2634,2650,2657,2693,2696,2700,2703,2730,2740,2743,2747,2750,2753],[11,12,13],"p",{},"Laravel 13 dropped on March 17, 2026 with a promise that sounds almost too good: zero breaking changes. And for the most part, Taylor Otwell delivered. But \"zero breaking changes\" doesn't mean \"nothing happened.\" Quite the opposite — this release reshapes how you structure models, configure jobs, query vectors, and build AI features, all without forcing a single line of migration code.",[11,15,16],{},"Let's skip the marketing bullet points and dig into what actually matters.",[18,19,21],"h2",{"id":20},"php-83-is-now-the-floor","PHP 8.3 Is Now the Floor",[11,23,24,25,29,30,33,34,37],{},"Laravel 13 drops PHP 8.2 support. This isn't just a version bump — it unlocks ",[26,27,28],"code",{},"json_validate()",", typed class constants, ",[26,31,32],{},"#[\\Override]",", and the ",[26,35,36],{},"Randomizer"," additions that the framework now leans on internally. If you're still on 8.2, this is your forcing function.",[11,39,40],{},"Support window: bug fixes through Q3 2027, security patches through Q1 2028.",[18,42,44],{"id":43},"php-attributes-36-new-attributes-one-architectural-shift","PHP Attributes: 36 New Attributes, One Architectural Shift",[11,46,47,48,52],{},"This is the headline feature, and it deserves more than a passing mention. Laravel 13 introduces ",[49,50,51],"strong",{},"36 new attribute classes"," across models, jobs, commands, form requests, resources, factories, and tests. Combined with the 17+ that already existed since Laravel 11-12, you now have 50+ attributes available.",[11,54,55,56,59],{},"But here's what most articles miss: ",[49,57,58],{},"this isn't syntactic sugar."," It's a shift toward declarative configuration that the framework can introspect at boot time, opening the door for better static analysis, IDE support, and tooling.",[61,62,64],"h3",{"id":63},"eloquent-model-attributes","Eloquent Model Attributes",[11,66,67,68,71,72,75],{},"The ",[26,69,70],{},"#[Table]"," attribute is the most powerful — it replaces ",[49,73,74],{},"six"," separate properties in a single declaration:",[77,78,83],"pre",{"className":79,"code":80,"language":81,"meta":82,"style":82},"language-php shiki shiki-themes github-light","use Illuminate\\Database\\Eloquent\\Attributes\\Table;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Fillable;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Hidden;\n\n#[Table(\n    name: 'external_orders',\n    key: 'uuid',\n    keyType: 'string',\n    incrementing: false,\n    timestamps: false,\n)]\n#[Fillable(['customer_id', 'total', 'currency', 'status'])]\n#[Hidden(['internal_notes', 'margin'])]\nclass ExternalOrder extends Model {}\n","php","",[26,84,85,102,112,122,129,141,158,171,184,197,209,215,248,268],{"__ignoreMap":82},[86,87,90,94,98],"span",{"class":88,"line":89},"line",1,[86,91,93],{"class":92},"sD7c4","use",[86,95,97],{"class":96},"sYu0t"," Illuminate\\Database\\Eloquent\\Attributes\\Table",[86,99,101],{"class":100},"sgsFI",";\n",[86,103,105,107,110],{"class":88,"line":104},2,[86,106,93],{"class":92},[86,108,109],{"class":96}," Illuminate\\Database\\Eloquent\\Attributes\\Fillable",[86,111,101],{"class":100},[86,113,115,117,120],{"class":88,"line":114},3,[86,116,93],{"class":92},[86,118,119],{"class":96}," Illuminate\\Database\\Eloquent\\Attributes\\Hidden",[86,121,101],{"class":100},[86,123,125],{"class":88,"line":124},4,[86,126,128],{"emptyLinePlaceholder":127},true,"\n",[86,130,132,135,138],{"class":88,"line":131},5,[86,133,134],{"class":100},"#[",[86,136,137],{"class":96},"Table",[86,139,140],{"class":100},"(\n",[86,142,144,148,151,155],{"class":88,"line":143},6,[86,145,147],{"class":146},"s7eDp","    name",[86,149,150],{"class":100},": ",[86,152,154],{"class":153},"sYBdl","'external_orders'",[86,156,157],{"class":100},",\n",[86,159,161,164,166,169],{"class":88,"line":160},7,[86,162,163],{"class":146},"    key",[86,165,150],{"class":100},[86,167,168],{"class":153},"'uuid'",[86,170,157],{"class":100},[86,172,174,177,179,182],{"class":88,"line":173},8,[86,175,176],{"class":146},"    keyType",[86,178,150],{"class":100},[86,180,181],{"class":153},"'string'",[86,183,157],{"class":100},[86,185,187,190,192,195],{"class":88,"line":186},9,[86,188,189],{"class":146},"    incrementing",[86,191,150],{"class":100},[86,193,194],{"class":96},"false",[86,196,157],{"class":100},[86,198,200,203,205,207],{"class":88,"line":199},10,[86,201,202],{"class":146},"    timestamps",[86,204,150],{"class":100},[86,206,194],{"class":96},[86,208,157],{"class":100},[86,210,212],{"class":88,"line":211},11,[86,213,214],{"class":100},")]\n",[86,216,218,220,223,226,229,232,235,237,240,242,245],{"class":88,"line":217},12,[86,219,134],{"class":100},[86,221,222],{"class":96},"Fillable",[86,224,225],{"class":100},"([",[86,227,228],{"class":153},"'customer_id'",[86,230,231],{"class":100},", ",[86,233,234],{"class":153},"'total'",[86,236,231],{"class":100},[86,238,239],{"class":153},"'currency'",[86,241,231],{"class":100},[86,243,244],{"class":153},"'status'",[86,246,247],{"class":100},"])]\n",[86,249,251,253,256,258,261,263,266],{"class":88,"line":250},13,[86,252,134],{"class":100},[86,254,255],{"class":96},"Hidden",[86,257,225],{"class":100},[86,259,260],{"class":153},"'internal_notes'",[86,262,231],{"class":100},[86,264,265],{"class":153},"'margin'",[86,267,247],{"class":100},[86,269,271,274,277,280,283],{"class":88,"line":270},14,[86,272,273],{"class":92},"class",[86,275,276],{"class":146}," ExternalOrder",[86,278,279],{"class":92}," extends",[86,281,282],{"class":146}," Model",[86,284,285],{"class":100}," {}\n",[11,287,288],{},"Compare that to the old way:",[77,290,292],{"className":79,"code":291,"language":81,"meta":82,"style":82},"class ExternalOrder extends Model\n{\n    protected $table = 'external_orders';\n    protected $primaryKey = 'uuid';\n    protected $keyType = 'string';\n    public $incrementing = false;\n    public $timestamps = false;\n    protected $fillable = ['customer_id', 'total', 'currency', 'status'];\n    protected $hidden = ['internal_notes', 'margin'];\n}\n",[26,293,294,305,310,326,340,354,369,382,411,430],{"__ignoreMap":82},[86,295,296,298,300,302],{"class":88,"line":89},[86,297,273],{"class":92},[86,299,276],{"class":146},[86,301,279],{"class":92},[86,303,304],{"class":146}," Model\n",[86,306,307],{"class":88,"line":104},[86,308,309],{"class":100},"{\n",[86,311,312,315,318,321,324],{"class":88,"line":114},[86,313,314],{"class":92},"    protected",[86,316,317],{"class":100}," $table ",[86,319,320],{"class":92},"=",[86,322,323],{"class":153}," 'external_orders'",[86,325,101],{"class":100},[86,327,328,330,333,335,338],{"class":88,"line":124},[86,329,314],{"class":92},[86,331,332],{"class":100}," $primaryKey ",[86,334,320],{"class":92},[86,336,337],{"class":153}," 'uuid'",[86,339,101],{"class":100},[86,341,342,344,347,349,352],{"class":88,"line":131},[86,343,314],{"class":92},[86,345,346],{"class":100}," $keyType ",[86,348,320],{"class":92},[86,350,351],{"class":153}," 'string'",[86,353,101],{"class":100},[86,355,356,359,362,364,367],{"class":88,"line":143},[86,357,358],{"class":92},"    public",[86,360,361],{"class":100}," $incrementing ",[86,363,320],{"class":92},[86,365,366],{"class":96}," false",[86,368,101],{"class":100},[86,370,371,373,376,378,380],{"class":88,"line":160},[86,372,358],{"class":92},[86,374,375],{"class":100}," $timestamps ",[86,377,320],{"class":92},[86,379,366],{"class":96},[86,381,101],{"class":100},[86,383,384,386,389,391,394,396,398,400,402,404,406,408],{"class":88,"line":173},[86,385,314],{"class":92},[86,387,388],{"class":100}," $fillable ",[86,390,320],{"class":92},[86,392,393],{"class":100}," [",[86,395,228],{"class":153},[86,397,231],{"class":100},[86,399,234],{"class":153},[86,401,231],{"class":100},[86,403,239],{"class":153},[86,405,231],{"class":100},[86,407,244],{"class":153},[86,409,410],{"class":100},"];\n",[86,412,413,415,418,420,422,424,426,428],{"class":88,"line":186},[86,414,314],{"class":92},[86,416,417],{"class":100}," $hidden ",[86,419,320],{"class":92},[86,421,393],{"class":100},[86,423,260],{"class":153},[86,425,231],{"class":100},[86,427,265],{"class":153},[86,429,410],{"class":100},[86,431,432],{"class":88,"line":199},[86,433,434],{"class":100},"}\n",[11,436,437],{},"That's 7 lines of scattered property declarations collapsed into 3 focused attributes at the top of the class. The model body is now free for relationships, scopes, and business logic.",[439,440,442,443,446],"h4",{"id":441},"the-new-unguarded-attribute","The New ",[26,444,445],{},"#[Unguarded]"," Attribute",[11,448,449,450,453,454,457],{},"This one has no property equivalent. Previously, to make a model unguarded, you had to call ",[26,451,452],{},"Model::unguard()"," globally in a service provider, which affected ",[49,455,456],{},"all"," models. Now:",[77,459,461],{"className":79,"code":460,"language":81,"meta":82,"style":82},"use Illuminate\\Database\\Eloquent\\Attributes\\Unguarded;\n\n#[Unguarded]\nclass AppSetting extends Model {}\n",[26,462,463,472,476,486],{"__ignoreMap":82},[86,464,465,467,470],{"class":88,"line":89},[86,466,93],{"class":92},[86,468,469],{"class":96}," Illuminate\\Database\\Eloquent\\Attributes\\Unguarded",[86,471,101],{"class":100},[86,473,474],{"class":88,"line":104},[86,475,128],{"emptyLinePlaceholder":127},[86,477,478,480,483],{"class":88,"line":114},[86,479,134],{"class":100},[86,481,482],{"class":96},"Unguarded",[86,484,485],{"class":100},"]\n",[86,487,488,490,493,495,497],{"class":88,"line":124},[86,489,273],{"class":92},[86,491,492],{"class":146}," AppSetting",[86,494,279],{"class":92},[86,496,282],{"class":146},[86,498,285],{"class":100},[11,500,501],{},"Scoped unguarding per model. This is useful for internal config tables or seed-only models where mass assignment protection is meaningless overhead.",[439,503,505],{"id":504},"other-model-attributes","Other Model Attributes",[77,507,509],{"className":79,"code":508,"language":81,"meta":82,"style":82},"#[Connection('analytics')]   // Dedicated DB connection\n#[Touches(['post'])]         // Auto-touch parent timestamps\n#[Visible(['id', 'name'])]   // Whitelist serialization (inverse of Hidden)\n#[Appends(['full_name'])]    // Append accessors to serialization\n",[26,510,511,531,549,572],{"__ignoreMap":82},[86,512,513,515,518,521,524,527],{"class":88,"line":89},[86,514,134],{"class":100},[86,516,517],{"class":96},"Connection",[86,519,520],{"class":100},"(",[86,522,523],{"class":153},"'analytics'",[86,525,526],{"class":100},")]   ",[86,528,530],{"class":529},"sAwPA","// Dedicated DB connection\n",[86,532,533,535,538,540,543,546],{"class":88,"line":104},[86,534,134],{"class":100},[86,536,537],{"class":96},"Touches",[86,539,225],{"class":100},[86,541,542],{"class":153},"'post'",[86,544,545],{"class":100},"])]         ",[86,547,548],{"class":529},"// Auto-touch parent timestamps\n",[86,550,551,553,556,558,561,563,566,569],{"class":88,"line":114},[86,552,134],{"class":100},[86,554,555],{"class":96},"Visible",[86,557,225],{"class":100},[86,559,560],{"class":153},"'id'",[86,562,231],{"class":100},[86,564,565],{"class":153},"'name'",[86,567,568],{"class":100},"])]   ",[86,570,571],{"class":529},"// Whitelist serialization (inverse of Hidden)\n",[86,573,574,576,579,581,584,587],{"class":88,"line":124},[86,575,134],{"class":100},[86,577,578],{"class":96},"Appends",[86,580,225],{"class":100},[86,582,583],{"class":153},"'full_name'",[86,585,586],{"class":100},"])]    ",[86,588,589],{"class":529},"// Append accessors to serialization\n",[61,591,593],{"id":592},"queue-job-attributes","Queue Job Attributes",[11,595,596,597,600,601,604,605,608],{},"This is where attributes shine the most. Job configuration was always awkward — you'd define ",[26,598,599],{},"$tries"," as a property, ",[26,602,603],{},"$backoff"," as a method, and ",[26,606,607],{},"$timeout"," somewhere in between. Now everything sits at the class level:",[77,610,612],{"className":79,"code":611,"language":81,"meta":82,"style":82},"use Illuminate\\Queue\\Attributes\\{Connection, Queue, Tries, Timeout, Backoff, MaxExceptions, FailOnTimeout};\n\n#[Connection('redis')]\n#[Queue('payments')]\n#[Tries(3)]\n#[Timeout(60)]\n#[Backoff([5, 15, 30])]\n#[MaxExceptions(2)]\n#[FailOnTimeout]\nclass ProcessPayment implements ShouldQueue\n{\n    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;\n\n    public function handle(): void\n    {\n        // Your job logic — no configuration noise here\n    }\n}\n",[26,613,614,659,663,676,689,702,715,738,751,759,772,776,801,805,824,830,836,842],{"__ignoreMap":82},[86,615,616,618,621,624,626,628,631,633,636,638,641,643,646,648,651,653,656],{"class":88,"line":89},[86,617,93],{"class":92},[86,619,620],{"class":96}," Illuminate\\Queue\\Attributes\\",[86,622,623],{"class":100},"{",[86,625,517],{"class":96},[86,627,231],{"class":100},[86,629,630],{"class":96},"Queue",[86,632,231],{"class":100},[86,634,635],{"class":96},"Tries",[86,637,231],{"class":100},[86,639,640],{"class":96},"Timeout",[86,642,231],{"class":100},[86,644,645],{"class":96},"Backoff",[86,647,231],{"class":100},[86,649,650],{"class":96},"MaxExceptions",[86,652,231],{"class":100},[86,654,655],{"class":96},"FailOnTimeout",[86,657,658],{"class":100},"};\n",[86,660,661],{"class":88,"line":104},[86,662,128],{"emptyLinePlaceholder":127},[86,664,665,667,669,671,674],{"class":88,"line":114},[86,666,134],{"class":100},[86,668,517],{"class":96},[86,670,520],{"class":100},[86,672,673],{"class":153},"'redis'",[86,675,214],{"class":100},[86,677,678,680,682,684,687],{"class":88,"line":124},[86,679,134],{"class":100},[86,681,630],{"class":96},[86,683,520],{"class":100},[86,685,686],{"class":153},"'payments'",[86,688,214],{"class":100},[86,690,691,693,695,697,700],{"class":88,"line":131},[86,692,134],{"class":100},[86,694,635],{"class":96},[86,696,520],{"class":100},[86,698,699],{"class":96},"3",[86,701,214],{"class":100},[86,703,704,706,708,710,713],{"class":88,"line":143},[86,705,134],{"class":100},[86,707,640],{"class":96},[86,709,520],{"class":100},[86,711,712],{"class":96},"60",[86,714,214],{"class":100},[86,716,717,719,721,723,726,728,731,733,736],{"class":88,"line":160},[86,718,134],{"class":100},[86,720,645],{"class":96},[86,722,225],{"class":100},[86,724,725],{"class":96},"5",[86,727,231],{"class":100},[86,729,730],{"class":96},"15",[86,732,231],{"class":100},[86,734,735],{"class":96},"30",[86,737,247],{"class":100},[86,739,740,742,744,746,749],{"class":88,"line":173},[86,741,134],{"class":100},[86,743,650],{"class":96},[86,745,520],{"class":100},[86,747,748],{"class":96},"2",[86,750,214],{"class":100},[86,752,753,755,757],{"class":88,"line":186},[86,754,134],{"class":100},[86,756,655],{"class":96},[86,758,485],{"class":100},[86,760,761,763,766,769],{"class":88,"line":199},[86,762,273],{"class":92},[86,764,765],{"class":146}," ProcessPayment",[86,767,768],{"class":92}," implements",[86,770,771],{"class":146}," ShouldQueue\n",[86,773,774],{"class":88,"line":211},[86,775,309],{"class":100},[86,777,778,781,784,786,789,791,794,796,799],{"class":88,"line":217},[86,779,780],{"class":92},"    use",[86,782,783],{"class":96}," Dispatchable",[86,785,231],{"class":100},[86,787,788],{"class":96},"InteractsWithQueue",[86,790,231],{"class":100},[86,792,793],{"class":96},"Queueable",[86,795,231],{"class":100},[86,797,798],{"class":96},"SerializesModels",[86,800,101],{"class":100},[86,802,803],{"class":88,"line":250},[86,804,128],{"emptyLinePlaceholder":127},[86,806,807,809,812,815,818,821],{"class":88,"line":270},[86,808,358],{"class":92},[86,810,811],{"class":92}," function",[86,813,814],{"class":146}," handle",[86,816,817],{"class":100},"()",[86,819,820],{"class":92},":",[86,822,823],{"class":92}," void\n",[86,825,827],{"class":88,"line":826},15,[86,828,829],{"class":100},"    {\n",[86,831,833],{"class":88,"line":832},16,[86,834,835],{"class":529},"        // Your job logic — no configuration noise here\n",[86,837,839],{"class":88,"line":838},17,[86,840,841],{"class":100},"    }\n",[86,843,845],{"class":88,"line":844},18,[86,846,434],{"class":100},[11,848,67,849,852,853,856],{},[26,850,851],{},"#[Backoff([5, 15, 30])]"," syntax is new — pass an array for exponential backoff intervals. Previously this required a ",[26,854,855],{},"backoff()"," method returning an array.",[11,858,67,859,862,863,866,867,820],{},[26,860,861],{},"#[UniqueFor]"," attribute replaces implementing ",[26,864,865],{},"ShouldBeUnique"," plus defining ",[26,868,869],{},"$uniqueFor",[77,871,873],{"className":79,"code":872,"language":81,"meta":82,"style":82},"use Illuminate\\Queue\\Attributes\\UniqueFor;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\n\n#[UniqueFor(3600)]\nclass RebuildSearchIndex implements ShouldQueue, ShouldBeUnique\n{\n    // Unique lock for 1 hour — no property/method needed\n}\n",[26,874,875,884,893,897,911,928,932,937],{"__ignoreMap":82},[86,876,877,879,882],{"class":88,"line":89},[86,878,93],{"class":92},[86,880,881],{"class":96}," Illuminate\\Queue\\Attributes\\UniqueFor",[86,883,101],{"class":100},[86,885,886,888,891],{"class":88,"line":104},[86,887,93],{"class":92},[86,889,890],{"class":96}," Illuminate\\Contracts\\Queue\\ShouldBeUnique",[86,892,101],{"class":100},[86,894,895],{"class":88,"line":114},[86,896,128],{"emptyLinePlaceholder":127},[86,898,899,901,904,906,909],{"class":88,"line":124},[86,900,134],{"class":100},[86,902,903],{"class":96},"UniqueFor",[86,905,520],{"class":100},[86,907,908],{"class":96},"3600",[86,910,214],{"class":100},[86,912,913,915,918,920,923,925],{"class":88,"line":131},[86,914,273],{"class":92},[86,916,917],{"class":146}," RebuildSearchIndex",[86,919,768],{"class":92},[86,921,922],{"class":146}," ShouldQueue",[86,924,231],{"class":100},[86,926,927],{"class":146},"ShouldBeUnique\n",[86,929,930],{"class":88,"line":143},[86,931,309],{"class":100},[86,933,934],{"class":88,"line":160},[86,935,936],{"class":529},"    // Unique lock for 1 hour — no property/method needed\n",[86,938,939],{"class":88,"line":173},[86,940,434],{"class":100},[61,942,944],{"id":943},"controller-attributes-middleware-and-authorization","Controller Attributes: Middleware and Authorization",[11,946,947,948,951],{},"This removes the need for ",[26,949,950],{},"__construct()"," middleware registration entirely:",[77,953,955],{"className":79,"code":954,"language":81,"meta":82,"style":82},"use Illuminate\\Routing\\Attributes\\Controllers\\{Middleware, Authorize};\n\n#[Middleware('auth')]\n#[Authorize('manage-users', only: ['edit', 'update', 'destroy'])]\nclass UserController extends Controller\n{\n    #[Middleware('throttle:10,1')]\n    public function store(Request $request)\n    {\n        // ...\n    }\n\n    #[Authorize('delete', User::class)]\n    public function destroy(User $user)\n    {\n        // ...\n    }\n}\n",[26,956,957,976,980,993,1027,1039,1043,1057,1074,1078,1083,1087,1091,1112,1128,1132,1136,1140],{"__ignoreMap":82},[86,958,959,961,964,966,969,971,974],{"class":88,"line":89},[86,960,93],{"class":92},[86,962,963],{"class":96}," Illuminate\\Routing\\Attributes\\Controllers\\",[86,965,623],{"class":100},[86,967,968],{"class":96},"Middleware",[86,970,231],{"class":100},[86,972,973],{"class":96},"Authorize",[86,975,658],{"class":100},[86,977,978],{"class":88,"line":104},[86,979,128],{"emptyLinePlaceholder":127},[86,981,982,984,986,988,991],{"class":88,"line":114},[86,983,134],{"class":100},[86,985,968],{"class":96},[86,987,520],{"class":100},[86,989,990],{"class":153},"'auth'",[86,992,214],{"class":100},[86,994,995,997,999,1001,1004,1006,1009,1012,1015,1017,1020,1022,1025],{"class":88,"line":124},[86,996,134],{"class":100},[86,998,973],{"class":96},[86,1000,520],{"class":100},[86,1002,1003],{"class":153},"'manage-users'",[86,1005,231],{"class":100},[86,1007,1008],{"class":146},"only",[86,1010,1011],{"class":100},": [",[86,1013,1014],{"class":153},"'edit'",[86,1016,231],{"class":100},[86,1018,1019],{"class":153},"'update'",[86,1021,231],{"class":100},[86,1023,1024],{"class":153},"'destroy'",[86,1026,247],{"class":100},[86,1028,1029,1031,1034,1036],{"class":88,"line":131},[86,1030,273],{"class":92},[86,1032,1033],{"class":146}," UserController",[86,1035,279],{"class":92},[86,1037,1038],{"class":146}," Controller\n",[86,1040,1041],{"class":88,"line":143},[86,1042,309],{"class":100},[86,1044,1045,1048,1050,1052,1055],{"class":88,"line":160},[86,1046,1047],{"class":100},"    #[",[86,1049,968],{"class":96},[86,1051,520],{"class":100},[86,1053,1054],{"class":153},"'throttle:10,1'",[86,1056,214],{"class":100},[86,1058,1059,1061,1063,1066,1068,1071],{"class":88,"line":173},[86,1060,358],{"class":92},[86,1062,811],{"class":92},[86,1064,1065],{"class":146}," store",[86,1067,520],{"class":100},[86,1069,1070],{"class":96},"Request",[86,1072,1073],{"class":100}," $request)\n",[86,1075,1076],{"class":88,"line":186},[86,1077,829],{"class":100},[86,1079,1080],{"class":88,"line":199},[86,1081,1082],{"class":529},"        // ...\n",[86,1084,1085],{"class":88,"line":211},[86,1086,841],{"class":100},[86,1088,1089],{"class":88,"line":217},[86,1090,128],{"emptyLinePlaceholder":127},[86,1092,1093,1095,1097,1099,1102,1104,1107,1110],{"class":88,"line":250},[86,1094,1047],{"class":100},[86,1096,973],{"class":96},[86,1098,520],{"class":100},[86,1100,1101],{"class":153},"'delete'",[86,1103,231],{"class":100},[86,1105,1106],{"class":96},"User",[86,1108,1109],{"class":92},"::class",[86,1111,214],{"class":100},[86,1113,1114,1116,1118,1121,1123,1125],{"class":88,"line":270},[86,1115,358],{"class":92},[86,1117,811],{"class":92},[86,1119,1120],{"class":146}," destroy",[86,1122,520],{"class":100},[86,1124,1106],{"class":96},[86,1126,1127],{"class":100}," $user)\n",[86,1129,1130],{"class":88,"line":826},[86,1131,829],{"class":100},[86,1133,1134],{"class":88,"line":832},[86,1135,1082],{"class":529},[86,1137,1138],{"class":88,"line":838},[86,1139,841],{"class":100},[86,1141,1142],{"class":88,"line":844},[86,1143,434],{"class":100},[11,1145,67,1146,1148,1149,1152,1153,1156],{},[26,1147,1008],{}," and ",[26,1150,1151],{},"except"," parameters work just like they did with ",[26,1154,1155],{},"$this->middleware()",", but are now statically analyzable.",[61,1158,1160],{"id":1159},"console-command-attributes","Console Command Attributes",[77,1162,1164],{"className":79,"code":1163,"language":81,"meta":82,"style":82},"use Illuminate\\Console\\Attributes\\{Signature, Description, Usage, Hidden};\n\n#[Signature('orders:reconcile {--since= : Start date} {--dry-run}')]\n#[Description('Reconcile order totals against payment gateway')]\n#[Usage('orders:reconcile --since=2026-01-01')]\n#[Usage('orders:reconcile --since=2026-01-01 --dry-run')]\nclass ReconcileOrders extends Command\n{\n    public function handle(): int\n    {\n        // No $signature or $description properties needed\n    }\n}\n",[26,1165,1166,1194,1198,1211,1224,1237,1250,1262,1266,1281,1285,1290,1294],{"__ignoreMap":82},[86,1167,1168,1170,1173,1175,1178,1180,1183,1185,1188,1190,1192],{"class":88,"line":89},[86,1169,93],{"class":92},[86,1171,1172],{"class":96}," Illuminate\\Console\\Attributes\\",[86,1174,623],{"class":100},[86,1176,1177],{"class":96},"Signature",[86,1179,231],{"class":100},[86,1181,1182],{"class":96},"Description",[86,1184,231],{"class":100},[86,1186,1187],{"class":96},"Usage",[86,1189,231],{"class":100},[86,1191,255],{"class":96},[86,1193,658],{"class":100},[86,1195,1196],{"class":88,"line":104},[86,1197,128],{"emptyLinePlaceholder":127},[86,1199,1200,1202,1204,1206,1209],{"class":88,"line":114},[86,1201,134],{"class":100},[86,1203,1177],{"class":96},[86,1205,520],{"class":100},[86,1207,1208],{"class":153},"'orders:reconcile {--since= : Start date} {--dry-run}'",[86,1210,214],{"class":100},[86,1212,1213,1215,1217,1219,1222],{"class":88,"line":124},[86,1214,134],{"class":100},[86,1216,1182],{"class":96},[86,1218,520],{"class":100},[86,1220,1221],{"class":153},"'Reconcile order totals against payment gateway'",[86,1223,214],{"class":100},[86,1225,1226,1228,1230,1232,1235],{"class":88,"line":131},[86,1227,134],{"class":100},[86,1229,1187],{"class":96},[86,1231,520],{"class":100},[86,1233,1234],{"class":153},"'orders:reconcile --since=2026-01-01'",[86,1236,214],{"class":100},[86,1238,1239,1241,1243,1245,1248],{"class":88,"line":143},[86,1240,134],{"class":100},[86,1242,1187],{"class":96},[86,1244,520],{"class":100},[86,1246,1247],{"class":153},"'orders:reconcile --since=2026-01-01 --dry-run'",[86,1249,214],{"class":100},[86,1251,1252,1254,1257,1259],{"class":88,"line":160},[86,1253,273],{"class":92},[86,1255,1256],{"class":146}," ReconcileOrders",[86,1258,279],{"class":92},[86,1260,1261],{"class":146}," Command\n",[86,1263,1264],{"class":88,"line":173},[86,1265,309],{"class":100},[86,1267,1268,1270,1272,1274,1276,1278],{"class":88,"line":186},[86,1269,358],{"class":92},[86,1271,811],{"class":92},[86,1273,814],{"class":146},[86,1275,817],{"class":100},[86,1277,820],{"class":92},[86,1279,1280],{"class":92}," int\n",[86,1282,1283],{"class":88,"line":199},[86,1284,829],{"class":100},[86,1286,1287],{"class":88,"line":211},[86,1288,1289],{"class":529},"        // No $signature or $description properties needed\n",[86,1291,1292],{"class":88,"line":217},[86,1293,841],{"class":100},[86,1295,1296],{"class":88,"line":250},[86,1297,434],{"class":100},[11,1299,1300,1301,1304],{},"Multiple ",[26,1302,1303],{},"#[Usage]"," attributes stack — a small but thoughtful touch for help screen documentation.",[61,1306,1308],{"id":1307},"form-request-attributes","Form Request Attributes",[77,1310,1312],{"className":79,"code":1311,"language":81,"meta":82,"style":82},"use Illuminate\\Foundation\\Http\\Attributes\\{ErrorBag, RedirectToRoute, StopOnFirstFailure};\n\n#[ErrorBag('createPost')]\n#[RedirectToRoute('posts.create')]\n#[StopOnFirstFailure]\nclass StorePostRequest extends FormRequest\n{\n    public function rules(): array\n    {\n        return [\n            'title' => 'required|max:255',\n            'body' => 'required',\n        ];\n    }\n}\n",[26,1313,1314,1338,1342,1355,1368,1376,1388,1392,1408,1412,1420,1433,1445,1450,1454],{"__ignoreMap":82},[86,1315,1316,1318,1321,1323,1326,1328,1331,1333,1336],{"class":88,"line":89},[86,1317,93],{"class":92},[86,1319,1320],{"class":96}," Illuminate\\Foundation\\Http\\Attributes\\",[86,1322,623],{"class":100},[86,1324,1325],{"class":96},"ErrorBag",[86,1327,231],{"class":100},[86,1329,1330],{"class":96},"RedirectToRoute",[86,1332,231],{"class":100},[86,1334,1335],{"class":96},"StopOnFirstFailure",[86,1337,658],{"class":100},[86,1339,1340],{"class":88,"line":104},[86,1341,128],{"emptyLinePlaceholder":127},[86,1343,1344,1346,1348,1350,1353],{"class":88,"line":114},[86,1345,134],{"class":100},[86,1347,1325],{"class":96},[86,1349,520],{"class":100},[86,1351,1352],{"class":153},"'createPost'",[86,1354,214],{"class":100},[86,1356,1357,1359,1361,1363,1366],{"class":88,"line":124},[86,1358,134],{"class":100},[86,1360,1330],{"class":96},[86,1362,520],{"class":100},[86,1364,1365],{"class":153},"'posts.create'",[86,1367,214],{"class":100},[86,1369,1370,1372,1374],{"class":88,"line":131},[86,1371,134],{"class":100},[86,1373,1335],{"class":96},[86,1375,485],{"class":100},[86,1377,1378,1380,1383,1385],{"class":88,"line":143},[86,1379,273],{"class":92},[86,1381,1382],{"class":146}," StorePostRequest",[86,1384,279],{"class":92},[86,1386,1387],{"class":146}," FormRequest\n",[86,1389,1390],{"class":88,"line":160},[86,1391,309],{"class":100},[86,1393,1394,1396,1398,1401,1403,1405],{"class":88,"line":173},[86,1395,358],{"class":92},[86,1397,811],{"class":92},[86,1399,1400],{"class":146}," rules",[86,1402,817],{"class":100},[86,1404,820],{"class":92},[86,1406,1407],{"class":92}," array\n",[86,1409,1410],{"class":88,"line":186},[86,1411,829],{"class":100},[86,1413,1414,1417],{"class":88,"line":199},[86,1415,1416],{"class":92},"        return",[86,1418,1419],{"class":100}," [\n",[86,1421,1422,1425,1428,1431],{"class":88,"line":211},[86,1423,1424],{"class":153},"            'title'",[86,1426,1427],{"class":92}," =>",[86,1429,1430],{"class":153}," 'required|max:255'",[86,1432,157],{"class":100},[86,1434,1435,1438,1440,1443],{"class":88,"line":217},[86,1436,1437],{"class":153},"            'body'",[86,1439,1427],{"class":92},[86,1441,1442],{"class":153}," 'required'",[86,1444,157],{"class":100},[86,1446,1447],{"class":88,"line":250},[86,1448,1449],{"class":100},"        ];\n",[86,1451,1452],{"class":88,"line":270},[86,1453,841],{"class":100},[86,1455,1456],{"class":88,"line":826},[86,1457,434],{"class":100},[11,1459,1460,1463],{},[26,1461,1462],{},"#[StopOnFirstFailure]"," is particularly useful for wizard-style forms where validating everything at once is wasted effort.",[61,1465,1467],{"id":1466},"testing-attributes","Testing Attributes",[77,1469,1471],{"className":79,"code":1470,"language":81,"meta":82,"style":82},"use Illuminate\\Foundation\\Testing\\Attributes\\{Seed, SetUp, TearDown};\n\n#[Seed(RoleAndPermissionSeeder::class)]\nclass AdminAccessTest extends TestCase\n{\n    private User $admin;\n\n    #[SetUp]\n    public function createAdmin(): void\n    {\n        $this->admin = User::factory()->admin()->create();\n    }\n\n    #[TearDown]\n    public function clearPermissionCache(): void\n    {\n        app()['permission.registrar']->forgetCachedPermissions();\n    }\n\n    public function test_admin_can_access_dashboard(): void\n    {\n        $this->actingAs($this->admin)\n            ->get('/admin')\n            ->assertOk();\n    }\n}\n",[26,1472,1473,1497,1501,1516,1528,1532,1543,1547,1555,1570,1574,1612,1616,1620,1628,1643,1647,1668,1672,1677,1693,1698,1718,1735,1745,1750],{"__ignoreMap":82},[86,1474,1475,1477,1480,1482,1485,1487,1490,1492,1495],{"class":88,"line":89},[86,1476,93],{"class":92},[86,1478,1479],{"class":96}," Illuminate\\Foundation\\Testing\\Attributes\\",[86,1481,623],{"class":100},[86,1483,1484],{"class":96},"Seed",[86,1486,231],{"class":100},[86,1488,1489],{"class":96},"SetUp",[86,1491,231],{"class":100},[86,1493,1494],{"class":96},"TearDown",[86,1496,658],{"class":100},[86,1498,1499],{"class":88,"line":104},[86,1500,128],{"emptyLinePlaceholder":127},[86,1502,1503,1505,1507,1509,1512,1514],{"class":88,"line":114},[86,1504,134],{"class":100},[86,1506,1484],{"class":96},[86,1508,520],{"class":100},[86,1510,1511],{"class":96},"RoleAndPermissionSeeder",[86,1513,1109],{"class":92},[86,1515,214],{"class":100},[86,1517,1518,1520,1523,1525],{"class":88,"line":124},[86,1519,273],{"class":92},[86,1521,1522],{"class":146}," AdminAccessTest",[86,1524,279],{"class":92},[86,1526,1527],{"class":146}," TestCase\n",[86,1529,1530],{"class":88,"line":131},[86,1531,309],{"class":100},[86,1533,1534,1537,1540],{"class":88,"line":143},[86,1535,1536],{"class":92},"    private",[86,1538,1539],{"class":96}," User",[86,1541,1542],{"class":100}," $admin;\n",[86,1544,1545],{"class":88,"line":160},[86,1546,128],{"emptyLinePlaceholder":127},[86,1548,1549,1551,1553],{"class":88,"line":173},[86,1550,1047],{"class":100},[86,1552,1489],{"class":96},[86,1554,485],{"class":100},[86,1556,1557,1559,1561,1564,1566,1568],{"class":88,"line":186},[86,1558,358],{"class":92},[86,1560,811],{"class":92},[86,1562,1563],{"class":146}," createAdmin",[86,1565,817],{"class":100},[86,1567,820],{"class":92},[86,1569,823],{"class":92},[86,1571,1572],{"class":88,"line":199},[86,1573,829],{"class":100},[86,1575,1576,1579,1582,1585,1587,1589,1592,1595,1597,1599,1602,1604,1606,1609],{"class":88,"line":211},[86,1577,1578],{"class":96},"        $this",[86,1580,1581],{"class":92},"->",[86,1583,1584],{"class":100},"admin ",[86,1586,320],{"class":92},[86,1588,1539],{"class":96},[86,1590,1591],{"class":92},"::",[86,1593,1594],{"class":146},"factory",[86,1596,817],{"class":100},[86,1598,1581],{"class":92},[86,1600,1601],{"class":146},"admin",[86,1603,817],{"class":100},[86,1605,1581],{"class":92},[86,1607,1608],{"class":146},"create",[86,1610,1611],{"class":100},"();\n",[86,1613,1614],{"class":88,"line":217},[86,1615,841],{"class":100},[86,1617,1618],{"class":88,"line":250},[86,1619,128],{"emptyLinePlaceholder":127},[86,1621,1622,1624,1626],{"class":88,"line":270},[86,1623,1047],{"class":100},[86,1625,1494],{"class":96},[86,1627,485],{"class":100},[86,1629,1630,1632,1634,1637,1639,1641],{"class":88,"line":826},[86,1631,358],{"class":92},[86,1633,811],{"class":92},[86,1635,1636],{"class":146}," clearPermissionCache",[86,1638,817],{"class":100},[86,1640,820],{"class":92},[86,1642,823],{"class":92},[86,1644,1645],{"class":88,"line":832},[86,1646,829],{"class":100},[86,1648,1649,1652,1655,1658,1661,1663,1666],{"class":88,"line":838},[86,1650,1651],{"class":146},"        app",[86,1653,1654],{"class":100},"()[",[86,1656,1657],{"class":153},"'permission.registrar'",[86,1659,1660],{"class":100},"]",[86,1662,1581],{"class":92},[86,1664,1665],{"class":146},"forgetCachedPermissions",[86,1667,1611],{"class":100},[86,1669,1670],{"class":88,"line":844},[86,1671,841],{"class":100},[86,1673,1675],{"class":88,"line":1674},19,[86,1676,128],{"emptyLinePlaceholder":127},[86,1678,1680,1682,1684,1687,1689,1691],{"class":88,"line":1679},20,[86,1681,358],{"class":92},[86,1683,811],{"class":92},[86,1685,1686],{"class":146}," test_admin_can_access_dashboard",[86,1688,817],{"class":100},[86,1690,820],{"class":92},[86,1692,823],{"class":92},[86,1694,1696],{"class":88,"line":1695},21,[86,1697,829],{"class":100},[86,1699,1701,1703,1705,1708,1710,1713,1715],{"class":88,"line":1700},22,[86,1702,1578],{"class":96},[86,1704,1581],{"class":92},[86,1706,1707],{"class":146},"actingAs",[86,1709,520],{"class":100},[86,1711,1712],{"class":96},"$this",[86,1714,1581],{"class":92},[86,1716,1717],{"class":100},"admin)\n",[86,1719,1721,1724,1727,1729,1732],{"class":88,"line":1720},23,[86,1722,1723],{"class":92},"            ->",[86,1725,1726],{"class":146},"get",[86,1728,520],{"class":100},[86,1730,1731],{"class":153},"'/admin'",[86,1733,1734],{"class":100},")\n",[86,1736,1738,1740,1743],{"class":88,"line":1737},24,[86,1739,1723],{"class":92},[86,1741,1742],{"class":146},"assertOk",[86,1744,1611],{"class":100},[86,1746,1748],{"class":88,"line":1747},25,[86,1749,841],{"class":100},[86,1751,1753],{"class":88,"line":1752},26,[86,1754,434],{"class":100},[11,1756,1757,1148,1760,1763,1764,1767],{},[26,1758,1759],{},"#[SetUp]",[26,1761,1762],{},"#[TearDown]"," let you name your setup methods descriptively instead of overriding ",[26,1765,1766],{},"setUp()"," with a growing block of unrelated initialization.",[18,1769,1771],{"id":1770},"native-vector-search-with-pgvector","Native Vector Search with pgvector",[11,1773,1774,1775,1778],{},"This is the feature that flew under the radar. Laravel 13 adds ",[26,1776,1777],{},"whereVectorSimilarTo()"," to the query builder for native semantic search using PostgreSQL's pgvector extension.",[77,1780,1782],{"className":79,"code":1781,"language":81,"meta":82,"style":82},"$results = DB::table('documents')\n    ->whereVectorSimilarTo('embedding', 'Best wineries in Napa Valley')\n    ->limit(10)\n    ->get();\n",[26,1783,1784,1806,1826,1840],{"__ignoreMap":82},[86,1785,1786,1789,1791,1794,1796,1799,1801,1804],{"class":88,"line":89},[86,1787,1788],{"class":100},"$results ",[86,1790,320],{"class":92},[86,1792,1793],{"class":96}," DB",[86,1795,1591],{"class":92},[86,1797,1798],{"class":146},"table",[86,1800,520],{"class":100},[86,1802,1803],{"class":153},"'documents'",[86,1805,1734],{"class":100},[86,1807,1808,1811,1814,1816,1819,1821,1824],{"class":88,"line":104},[86,1809,1810],{"class":92},"    ->",[86,1812,1813],{"class":146},"whereVectorSimilarTo",[86,1815,520],{"class":100},[86,1817,1818],{"class":153},"'embedding'",[86,1820,231],{"class":100},[86,1822,1823],{"class":153},"'Best wineries in Napa Valley'",[86,1825,1734],{"class":100},[86,1827,1828,1830,1833,1835,1838],{"class":88,"line":114},[86,1829,1810],{"class":92},[86,1831,1832],{"class":146},"limit",[86,1834,520],{"class":100},[86,1836,1837],{"class":96},"10",[86,1839,1734],{"class":100},[86,1841,1842,1844,1846],{"class":88,"line":124},[86,1843,1810],{"class":92},[86,1845,1726],{"class":146},[86,1847,1611],{"class":100},[11,1849,1850,1851,1854],{},"What's happening here: the string is converted to an embedding vector via the configured AI provider (through the AI SDK), then compared against stored vectors using ",[49,1852,1853],{},"cosine similarity",". Results are automatically ordered by relevance.",[61,1856,1858],{"id":1857},"how-it-works-internally","How It Works Internally",[1860,1861,1862,1866,1877,1884],"ol",{},[1863,1864,1865],"li",{},"The query string is sent to your configured embedding model (OpenAI, Anthropic, etc.) via the AI SDK",[1863,1867,1868,1869,1872,1873,1876],{},"The returned vector is compared against the ",[26,1870,1871],{},"embedding"," column using pgvector's ",[26,1874,1875],{},"\u003C=>"," cosine distance operator",[1863,1878,1879,1880,1883],{},"Results below the ",[26,1881,1882],{},"minSimilarity"," threshold are filtered out",[1863,1885,1886,1887,1890],{},"An ",[49,1888,1889],{},"HNSW (Hierarchical Navigable Small World) index"," is automatically created on vector columns, giving you approximate nearest neighbor search that scales to millions of rows",[11,1892,1893],{},"This eliminates the need for separate vector databases like Pinecone or Weaviate for most use cases. Your embeddings live alongside your relational data in PostgreSQL.",[61,1895,1897],{"id":1896},"migration-example","Migration Example",[77,1899,1901],{"className":79,"code":1900,"language":81,"meta":82,"style":82},"Schema::create('documents', function (Blueprint $table) {\n    $table->id();\n    $table->text('content');\n    $table->vector('embedding', 1536); // 1536 dimensions for OpenAI ada-002\n    $table->timestamps();\n});\n",[26,1902,1903,1930,1942,1959,1983,1994],{"__ignoreMap":82},[86,1904,1905,1908,1910,1912,1914,1916,1918,1921,1924,1927],{"class":88,"line":89},[86,1906,1907],{"class":96},"Schema",[86,1909,1591],{"class":92},[86,1911,1608],{"class":146},[86,1913,520],{"class":100},[86,1915,1803],{"class":153},[86,1917,231],{"class":100},[86,1919,1920],{"class":92},"function",[86,1922,1923],{"class":100}," (",[86,1925,1926],{"class":96},"Blueprint",[86,1928,1929],{"class":100}," $table) {\n",[86,1931,1932,1935,1937,1940],{"class":88,"line":104},[86,1933,1934],{"class":100},"    $table",[86,1936,1581],{"class":92},[86,1938,1939],{"class":146},"id",[86,1941,1611],{"class":100},[86,1943,1944,1946,1948,1951,1953,1956],{"class":88,"line":114},[86,1945,1934],{"class":100},[86,1947,1581],{"class":92},[86,1949,1950],{"class":146},"text",[86,1952,520],{"class":100},[86,1954,1955],{"class":153},"'content'",[86,1957,1958],{"class":100},");\n",[86,1960,1961,1963,1965,1968,1970,1972,1974,1977,1980],{"class":88,"line":124},[86,1962,1934],{"class":100},[86,1964,1581],{"class":92},[86,1966,1967],{"class":146},"vector",[86,1969,520],{"class":100},[86,1971,1818],{"class":153},[86,1973,231],{"class":100},[86,1975,1976],{"class":96},"1536",[86,1978,1979],{"class":100},"); ",[86,1981,1982],{"class":529},"// 1536 dimensions for OpenAI ada-002\n",[86,1984,1985,1987,1989,1992],{"class":88,"line":131},[86,1986,1934],{"class":100},[86,1988,1581],{"class":92},[86,1990,1991],{"class":146},"timestamps",[86,1993,1611],{"class":100},[86,1995,1996],{"class":88,"line":143},[86,1997,1998],{"class":100},"});\n",[18,2000,2002],{"id":2001},"cachetouch-finally","Cache::touch() — Finally",[11,2004,2005,2006,2009,2010,2013],{},"A deceptively simple addition that solves a real problem. ",[26,2007,2008],{},"Cache::touch()"," extends a cached item's TTL ",[49,2011,2012],{},"without reading or re-storing the value",".",[77,2015,2017],{"className":79,"code":2016,"language":81,"meta":82,"style":82},"// Extend session TTL on activity — no deserialization overhead\nCache::touch('user_session:123', 3600);\n\n// Use Carbon for readability\nCache::touch('analytics_data', now()->addHours(6));\n\n// Remove TTL entirely (make permanent)\nCache::touch('report_cache', null);\n",[26,2018,2019,2024,2045,2049,2054,2087,2091,2096],{"__ignoreMap":82},[86,2020,2021],{"class":88,"line":89},[86,2022,2023],{"class":529},"// Extend session TTL on activity — no deserialization overhead\n",[86,2025,2026,2029,2031,2034,2036,2039,2041,2043],{"class":88,"line":104},[86,2027,2028],{"class":96},"Cache",[86,2030,1591],{"class":92},[86,2032,2033],{"class":146},"touch",[86,2035,520],{"class":100},[86,2037,2038],{"class":153},"'user_session:123'",[86,2040,231],{"class":100},[86,2042,908],{"class":96},[86,2044,1958],{"class":100},[86,2046,2047],{"class":88,"line":114},[86,2048,128],{"emptyLinePlaceholder":127},[86,2050,2051],{"class":88,"line":124},[86,2052,2053],{"class":529},"// Use Carbon for readability\n",[86,2055,2056,2058,2060,2062,2064,2067,2069,2072,2074,2076,2079,2081,2084],{"class":88,"line":131},[86,2057,2028],{"class":96},[86,2059,1591],{"class":92},[86,2061,2033],{"class":146},[86,2063,520],{"class":100},[86,2065,2066],{"class":153},"'analytics_data'",[86,2068,231],{"class":100},[86,2070,2071],{"class":146},"now",[86,2073,817],{"class":100},[86,2075,1581],{"class":92},[86,2077,2078],{"class":146},"addHours",[86,2080,520],{"class":100},[86,2082,2083],{"class":96},"6",[86,2085,2086],{"class":100},"));\n",[86,2088,2089],{"class":88,"line":143},[86,2090,128],{"emptyLinePlaceholder":127},[86,2092,2093],{"class":88,"line":160},[86,2094,2095],{"class":529},"// Remove TTL entirely (make permanent)\n",[86,2097,2098,2100,2102,2104,2106,2109,2111,2114],{"class":88,"line":173},[86,2099,2028],{"class":96},[86,2101,1591],{"class":92},[86,2103,2033],{"class":146},[86,2105,520],{"class":100},[86,2107,2108],{"class":153},"'report_cache'",[86,2110,231],{"class":100},[86,2112,2113],{"class":96},"null",[86,2115,1958],{"class":100},[11,2117,2118,2119,2122,2123,2125],{},"Returns ",[26,2120,2121],{},"true"," on success, ",[26,2124,194],{}," if the key doesn't exist.",[61,2127,2129],{"id":2128},"why-this-matters","Why This Matters",[11,2131,2132,2133,2136,2137,2140,2141,2143,2144,2147,2148,2150],{},"Previously, extending a TTL required ",[26,2134,2135],{},"Cache::get()"," + ",[26,2138,2139],{},"Cache::put()",", which meant deserializing and re-serializing the value. For large cached objects (compiled views, serialized collections, aggregated reports), this was wasteful. ",[26,2142,2008],{}," is a single atomic operation on every driver — Redis ",[26,2145,2146],{},"EXPIRE",", Memcached ",[26,2149,2033],{},", and equivalent operations on Database, DynamoDB, File, and APC.",[18,2152,2154],{"id":2153},"queueroute-centralized-job-routing","Queue::route() — Centralized Job Routing",[11,2156,2157],{},"Instead of scattering queue/connection config across dozens of job classes:",[77,2159,2161],{"className":79,"code":2160,"language":81,"meta":82,"style":82},"// In a service provider\nQueue::route(ProcessPayment::class, connection: 'redis', queue: 'payments');\nQueue::route(SendNewsletter::class, connection: 'sqs', queue: 'emails');\nQueue::route(GenerateReport::class, connection: 'redis', queue: 'reports');\n",[26,2162,2163,2168,2204,2239],{"__ignoreMap":82},[86,2164,2165],{"class":88,"line":89},[86,2166,2167],{"class":529},"// In a service provider\n",[86,2169,2170,2172,2174,2177,2179,2182,2184,2186,2189,2191,2193,2195,2198,2200,2202],{"class":88,"line":104},[86,2171,630],{"class":96},[86,2173,1591],{"class":92},[86,2175,2176],{"class":146},"route",[86,2178,520],{"class":100},[86,2180,2181],{"class":96},"ProcessPayment",[86,2183,1109],{"class":92},[86,2185,231],{"class":100},[86,2187,2188],{"class":146},"connection",[86,2190,150],{"class":100},[86,2192,673],{"class":153},[86,2194,231],{"class":100},[86,2196,2197],{"class":146},"queue",[86,2199,150],{"class":100},[86,2201,686],{"class":153},[86,2203,1958],{"class":100},[86,2205,2206,2208,2210,2212,2214,2217,2219,2221,2223,2225,2228,2230,2232,2234,2237],{"class":88,"line":114},[86,2207,630],{"class":96},[86,2209,1591],{"class":92},[86,2211,2176],{"class":146},[86,2213,520],{"class":100},[86,2215,2216],{"class":96},"SendNewsletter",[86,2218,1109],{"class":92},[86,2220,231],{"class":100},[86,2222,2188],{"class":146},[86,2224,150],{"class":100},[86,2226,2227],{"class":153},"'sqs'",[86,2229,231],{"class":100},[86,2231,2197],{"class":146},[86,2233,150],{"class":100},[86,2235,2236],{"class":153},"'emails'",[86,2238,1958],{"class":100},[86,2240,2241,2243,2245,2247,2249,2252,2254,2256,2258,2260,2262,2264,2266,2268,2271],{"class":88,"line":124},[86,2242,630],{"class":96},[86,2244,1591],{"class":92},[86,2246,2176],{"class":146},[86,2248,520],{"class":100},[86,2250,2251],{"class":96},"GenerateReport",[86,2253,1109],{"class":92},[86,2255,231],{"class":100},[86,2257,2188],{"class":146},[86,2259,150],{"class":100},[86,2261,673],{"class":153},[86,2263,231],{"class":100},[86,2265,2197],{"class":146},[86,2267,150],{"class":100},[86,2269,2270],{"class":153},"'reports'",[86,2272,1958],{"class":100},[11,2274,2275],{},"This gives you a single place to see and manage your entire job topology. It also means you can change routing without modifying job classes — useful when migrating queue backends or rebalancing workers.",[11,2277,2278,2279,231,2282,2285,2286,2289],{},"Job-level attributes (",[26,2280,2281],{},"#[Connection]",[26,2283,2284],{},"#[Queue]",") take precedence over ",[26,2287,2288],{},"Queue::route()",", so you can use routing as a default with per-job overrides.",[18,2291,2293],{"id":2292},"jsonapi-resources","JSON:API Resources",[11,2295,2296,2297,2300,2301,2304],{},"Laravel 13 ships first-party JSON:API specification support. If you've used ",[26,2298,2299],{},"spatie/laravel-json-api"," or ",[26,2302,2303],{},"laravel-json-api/laravel",", you know the pain of wiring up compliant responses. Now it's built in.",[11,2306,2307],{},"The new resource classes handle:",[2309,2310,2311,2329,2338,2347,2353],"ul",{},[1863,2312,2313,2316,2317,231,2320,231,2322,231,2325,2328],{},[49,2314,2315],{},"Resource object serialization"," — ",[26,2318,2319],{},"type",[26,2321,1939],{},[26,2323,2324],{},"attributes",[26,2326,2327],{},"relationships"," structure",[1863,2330,2331,2316,2334,2337],{},[49,2332,2333],{},"Relationship inclusion",[26,2335,2336],{},"?include=author,comments"," query parameter",[1863,2339,2340,2316,2343,2346],{},[49,2341,2342],{},"Sparse fieldsets",[26,2344,2345],{},"?fields[posts]=title,body"," to reduce payload",[1863,2348,2349,2352],{},[49,2350,2351],{},"Links"," — self, related, and pagination links",[1863,2354,2355,2316,2358],{},[49,2356,2357],{},"Compliant headers",[26,2359,2360],{},"Content-Type: application/vnd.api+json",[11,2362,2363],{},"This is significant for teams building APIs consumed by Ember.js, JSON:API-compliant mobile clients, or any frontend that benefits from a standardized response format.",[18,2365,2367],{"id":2366},"laravel-ai-sdk-production-stable","Laravel AI SDK — Production Stable",[11,2369,2370,2371,2374],{},"The AI SDK that was beta in Laravel 12 is now stable. It provides a ",[49,2372,2373],{},"provider-agnostic interface"," for:",[2309,2376,2377,2380,2383,2386,2389,2392],{},[1863,2378,2379],{},"Text generation",[1863,2381,2382],{},"Tool-calling agents",[1863,2384,2385],{},"Image creation",[1863,2387,2388],{},"Audio synthesis",[1863,2390,2391],{},"Embedding generation",[1863,2393,2394],{},"Vector store integrations",[77,2396,2398],{"className":79,"code":2397,"language":81,"meta":82,"style":82},"use App\\Agents\\SalesCoach;\n\n// Streaming response\nRoute::get('/coach', function () {\n    return (new SalesCoach)->stream('Analyze this sales transcript...');\n});\n\n// Queued agent with promise-style callbacks\nRoute::post('/coach', function (Request $request) {\n    return (new SalesCoach)\n        ->queue($request->input('transcript'))\n        ->then(function (AgentResponse $response) {\n            // Process result\n        })\n        ->catch(function (Throwable $e) {\n            // Handle error\n        });\n});\n",[26,2399,2400,2409,2413,2418,2439,2467,2471,2475,2480,2504,2516,2539,2558,2563,2568,2587,2592,2597],{"__ignoreMap":82},[86,2401,2402,2404,2407],{"class":88,"line":89},[86,2403,93],{"class":92},[86,2405,2406],{"class":96}," App\\Agents\\SalesCoach",[86,2408,101],{"class":100},[86,2410,2411],{"class":88,"line":104},[86,2412,128],{"emptyLinePlaceholder":127},[86,2414,2415],{"class":88,"line":114},[86,2416,2417],{"class":529},"// Streaming response\n",[86,2419,2420,2423,2425,2427,2429,2432,2434,2436],{"class":88,"line":124},[86,2421,2422],{"class":96},"Route",[86,2424,1591],{"class":92},[86,2426,1726],{"class":146},[86,2428,520],{"class":100},[86,2430,2431],{"class":153},"'/coach'",[86,2433,231],{"class":100},[86,2435,1920],{"class":92},[86,2437,2438],{"class":100}," () {\n",[86,2440,2441,2444,2446,2449,2452,2455,2457,2460,2462,2465],{"class":88,"line":131},[86,2442,2443],{"class":92},"    return",[86,2445,1923],{"class":100},[86,2447,2448],{"class":92},"new",[86,2450,2451],{"class":96}," SalesCoach",[86,2453,2454],{"class":100},")",[86,2456,1581],{"class":92},[86,2458,2459],{"class":146},"stream",[86,2461,520],{"class":100},[86,2463,2464],{"class":153},"'Analyze this sales transcript...'",[86,2466,1958],{"class":100},[86,2468,2469],{"class":88,"line":143},[86,2470,1998],{"class":100},[86,2472,2473],{"class":88,"line":160},[86,2474,128],{"emptyLinePlaceholder":127},[86,2476,2477],{"class":88,"line":173},[86,2478,2479],{"class":529},"// Queued agent with promise-style callbacks\n",[86,2481,2482,2484,2486,2489,2491,2493,2495,2497,2499,2501],{"class":88,"line":186},[86,2483,2422],{"class":96},[86,2485,1591],{"class":92},[86,2487,2488],{"class":146},"post",[86,2490,520],{"class":100},[86,2492,2431],{"class":153},[86,2494,231],{"class":100},[86,2496,1920],{"class":92},[86,2498,1923],{"class":100},[86,2500,1070],{"class":96},[86,2502,2503],{"class":100}," $request) {\n",[86,2505,2506,2508,2510,2512,2514],{"class":88,"line":199},[86,2507,2443],{"class":92},[86,2509,1923],{"class":100},[86,2511,2448],{"class":92},[86,2513,2451],{"class":96},[86,2515,1734],{"class":100},[86,2517,2518,2521,2523,2526,2528,2531,2533,2536],{"class":88,"line":211},[86,2519,2520],{"class":92},"        ->",[86,2522,2197],{"class":146},[86,2524,2525],{"class":100},"($request",[86,2527,1581],{"class":92},[86,2529,2530],{"class":146},"input",[86,2532,520],{"class":100},[86,2534,2535],{"class":153},"'transcript'",[86,2537,2538],{"class":100},"))\n",[86,2540,2541,2543,2546,2548,2550,2552,2555],{"class":88,"line":217},[86,2542,2520],{"class":92},[86,2544,2545],{"class":146},"then",[86,2547,520],{"class":100},[86,2549,1920],{"class":92},[86,2551,1923],{"class":100},[86,2553,2554],{"class":96},"AgentResponse",[86,2556,2557],{"class":100}," $response) {\n",[86,2559,2560],{"class":88,"line":250},[86,2561,2562],{"class":529},"            // Process result\n",[86,2564,2565],{"class":88,"line":270},[86,2566,2567],{"class":100},"        })\n",[86,2569,2570,2572,2575,2577,2579,2581,2584],{"class":88,"line":826},[86,2571,2520],{"class":92},[86,2573,2574],{"class":146},"catch",[86,2576,520],{"class":100},[86,2578,1920],{"class":92},[86,2580,1923],{"class":100},[86,2582,2583],{"class":96},"Throwable",[86,2585,2586],{"class":100}," $e) {\n",[86,2588,2589],{"class":88,"line":832},[86,2590,2591],{"class":529},"            // Handle error\n",[86,2593,2594],{"class":88,"line":838},[86,2595,2596],{"class":100},"        });\n",[86,2598,2599],{"class":88,"line":844},[86,2600,1998],{"class":100},[11,2602,2603],{},"The SDK handles retry logic, error normalization, and queue integration behind the scenes. Swap providers by changing config, not code.",[18,2605,2607],{"id":2606},"multi-tenancy-teams-in-starter-kits","Multi-Tenancy Teams in Starter Kits",[11,2609,2610],{},"Laravel 13 brings team-based multi-tenancy back to official starter kits — but with a critical architectural change from the old Jetstream implementation.",[11,2612,2613,2616],{},[49,2614,2615],{},"The old way (Jetstream):"," Teams were session-based. The \"current team\" was stored in the session, which meant you couldn't operate in two team contexts simultaneously — opening a second tab shared the same session state.",[11,2618,2619,2622,2623,2626],{},[49,2620,2621],{},"The new way (Laravel 13):"," Teams are ",[49,2624,2625],{},"URL-routed",". The team context is part of the URL path, so different tabs can operate in different team contexts. This is how multi-tenancy should work in a stateless HTTP world.",[18,2628,2630],{"id":2629},"security-cache-serialization-hardening","Security: Cache Serialization Hardening",[11,2632,2633],{},"A subtle but important change. The default cache config now includes:",[77,2635,2637],{"className":79,"code":2636,"language":81,"meta":82,"style":82},"'serializable_classes' => false,\n",[26,2638,2639],{"__ignoreMap":82},[86,2640,2641,2644,2646,2648],{"class":88,"line":89},[86,2642,2643],{"class":153},"'serializable_classes'",[86,2645,1427],{"class":92},[86,2647,366],{"class":96},[86,2649,157],{"class":100},[11,2651,2652,2653,2656],{},"This prevents PHP object deserialization from cache stores entirely by default. If your ",[26,2654,2655],{},"APP_KEY"," is leaked, an attacker can no longer exploit deserialization gadget chains through cached values. If you intentionally cache PHP objects, you'll need to explicitly whitelist those classes:",[77,2658,2660],{"className":79,"code":2659,"language":81,"meta":82,"style":82},"'serializable_classes' => [\n    App\\DataTransferObjects\\ReportData::class,\n    App\\ValueObjects\\Money::class,\n],\n",[26,2661,2662,2670,2679,2688],{"__ignoreMap":82},[86,2663,2664,2666,2668],{"class":88,"line":89},[86,2665,2643],{"class":153},[86,2667,1427],{"class":92},[86,2669,1419],{"class":100},[86,2671,2672,2675,2677],{"class":88,"line":104},[86,2673,2674],{"class":96},"    App\\DataTransferObjects\\ReportData",[86,2676,1109],{"class":92},[86,2678,157],{"class":100},[86,2680,2681,2684,2686],{"class":88,"line":114},[86,2682,2683],{"class":96},"    App\\ValueObjects\\Money",[86,2685,1109],{"class":92},[86,2687,157],{"class":100},[86,2689,2690],{"class":88,"line":124},[86,2691,2692],{"class":100},"],\n",[11,2694,2695],{},"This is defense-in-depth done right — secure by default, opt-in for specific use cases.",[18,2697,2699],{"id":2698},"upgrading-from-laravel-12","Upgrading from Laravel 12",[11,2701,2702],{},"The upgrade is genuinely painless for most apps:",[1860,2704,2705,2714,2717,2724],{},[1863,2706,2707,2708,150,2711],{},"Update ",[26,2709,2710],{},"composer.json",[26,2712,2713],{},"\"laravel/framework\": \"^13.0\"",[1863,2715,2716],{},"Ensure PHP >= 8.3",[1863,2718,2719,2720,2723],{},"Review the ",[26,2721,2722],{},"serializable_classes"," cache config if you cache PHP objects",[1863,2725,2726,2727],{},"Run ",[26,2728,2729],{},"composer update",[11,2731,2732,2735,2736,2739],{},[49,2733,2734],{},"Laravel Boost"," (the first-party MCP server) offers guided upgrades via ",[26,2737,2738],{},"/upgrade-laravel-v13"," in Claude Code, Cursor, or VS Code if you want automated migration assistance.",[11,2741,2742],{},"All attribute features are opt-in. Your existing property-based configuration continues to work unchanged. You can adopt attributes incrementally — even mixing properties and attributes in the same class.",[18,2744,2746],{"id":2745},"final-thoughts","Final Thoughts",[11,2748,2749],{},"Laravel 13 is a \"quality of life\" release that manages to be architecturally significant without being disruptive. The attributes system, vector search, and AI SDK aren't just conveniences — they signal where the framework is headed: declarative configuration, AI-native tooling, and fewer external dependencies for common patterns.",[11,2751,2752],{},"The zero-breaking-changes promise is real. Upgrade, then adopt new features at your own pace. That's how framework evolution should work.",[2754,2755,2756],"style",{},"html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}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 pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":82,"searchDepth":104,"depth":104,"links":2758},[2759,2760,2768,2772,2775,2776,2777,2778,2779,2780,2781],{"id":20,"depth":104,"text":21},{"id":43,"depth":104,"text":44,"children":2761},[2762,2763,2764,2765,2766,2767],{"id":63,"depth":114,"text":64},{"id":592,"depth":114,"text":593},{"id":943,"depth":114,"text":944},{"id":1159,"depth":114,"text":1160},{"id":1307,"depth":114,"text":1308},{"id":1466,"depth":114,"text":1467},{"id":1770,"depth":104,"text":1771,"children":2769},[2770,2771],{"id":1857,"depth":114,"text":1858},{"id":1896,"depth":114,"text":1897},{"id":2001,"depth":104,"text":2002,"children":2773},[2774],{"id":2128,"depth":114,"text":2129},{"id":2153,"depth":104,"text":2154},{"id":2292,"depth":104,"text":2293},{"id":2366,"depth":104,"text":2367},{"id":2606,"depth":104,"text":2607},{"id":2629,"depth":104,"text":2630},{"id":2698,"depth":104,"text":2699},{"id":2745,"depth":104,"text":2746},"Backend Development","/blog-covers/laravel-13-deep-dive.png","2026-03-27","A senior developer's breakdown of Laravel 13 — PHP attributes across 36+ locations, native vector search with pgvector, Cache::touch(), Queue::route(), JSON:API resources, and the AI SDK. Real code, real architecture decisions.","md",{},"/posts/laravel-13-deep-dive","20 min read",{"title":5,"description":2785},{"loc":2788,"lastmod":2784},"posts/laravel-13-deep-dive",[2794,81,2795,2796,2797,2798,2799],"laravel","laravel-13","backend","eloquent","ai","vector-search","xTIJddn2d209ktlH2VGH2rYaTJQ8JR2e1gSjK6os0p0",1777897734833]