[{"data":1,"prerenderedAt":1639},["ShallowReactive",2],{"\u002Farticles\u002Fembrace-the-framework-supplier-imports-with-collections-and-pipelines":3,"article-series:embrace-the-framework":1585},{"id":4,"title":5,"body":6,"description":1567,"draft":1568,"extension":1569,"meta":1570,"navigation":69,"path":1571,"published":1572,"seo":1573,"seoDescription":1574,"series":1575,"seriesOrder":1576,"stem":1577,"tags":1578,"updated":1583,"__hash__":1584},"articles\u002Farticles\u002Fembrace-the-framework-supplier-imports-with-collections-and-pipelines.md","Embrace the Framework: Combine Collections and Pipelines for Reliable Supplier Imports",{"type":7,"value":8,"toc":1559},"minimark",[9,13,16,19,29,34,37,673,687,700,704,707,1036,1061,1064,1194,1197,1200,1204,1207,1377,1380,1383,1429,1432,1436,1439,1529,1532,1536,1539,1542,1545,1549,1552,1555],[10,11,12],"p",{},"Supplier inventory imports are a good example of a task that needs more than one Laravel tool.",[10,14,15],{},"A supplier sends a CSV or API payload containing product codes, stock levels, prices, and availability dates. We need to validate the input, match it to our products, apply the changes, and tell the operations team what happened. Some records may be invalid, some product codes may no longer exist, and some stock changes may be unusual enough to need attention.",[10,17,18],{},"A Pipeline and a Collection each have a useful role here.",[20,21,22,26],"ul",{},[23,24,25],"li",{},"The Pipeline moves one import through a series of processing stages.",[23,27,28],{},"Collections help us group the results, identify exceptions, and produce a useful reconciliation report.",[30,31,33],"h2",{"id":32},"the-import-context","The import context",[10,35,36],{},"As with order processing, a context object is a convenient way to move the import and its results through the Pipeline.",[38,39,44],"pre",{"className":40,"code":41,"language":42,"meta":43,"style":43},"language-php shiki shiki-themes one-light one-dark-pro","\u003C?php\n\nnamespace App\\SupplierImport;\n\nuse App\\Models\\Product;\nuse Illuminate\\Support\\Collection;\n\nfinal readonly class InventoryChange\n{\n    public function __construct(\n        public Product $product,\n        public int $previousStockLevel,\n        public int $newStockLevel,\n    ) {\n    }\n\n    public function isUnusual(): bool\n    {\n        $difference = abs($this->newStockLevel - $this->previousStockLevel);\n\n        return $this->previousStockLevel === 0\n            ? $this->newStockLevel > 0\n            : $difference > ($this->previousStockLevel * 0.5);\n    }\n}\n\nclass SupplierImportContext\n{\n    public function __construct(\n        public Collection $rows,\n        public Collection $validRows,\n        public Collection $inventoryChanges,\n        public Collection $issues,\n    ) {\n    }\n\n    public function rejectRow(array $row, string $type, string $message): void\n    {\n        $this->issues->push([\n            'row_number' => $row['row_number'],\n            'type' => $type,\n            'message' => $message,\n        ]);\n\n        $this->validRows = $this->validRows\n            ->reject(fn (array $candidate) =>\n                $candidate['row_number'] === $row['row_number']\n            )\n            ->values();\n    }\n}\n","php","",[45,46,47,64,71,86,91,105,118,123,138,144,160,176,190,202,208,214,219,236,242,281,286,305,322,350,355,361,366,375,380,391,404,416,428,440,445,450,455,495,500,519,541,554,567,573,578,597,621,646,652,663,668],"code",{"__ignoreMap":43},[48,49,52,56,60],"span",{"class":50,"line":51},"line",1,[48,53,55],{"class":54},"sknuh","\u003C",[48,57,59],{"class":58},"sblXP","?",[48,61,63],{"class":62},"sxymB","php\n",[48,65,67],{"class":50,"line":66},2,[48,68,70],{"emptyLinePlaceholder":69},true,"\n",[48,72,74,78,82],{"class":50,"line":73},3,[48,75,77],{"class":76},"sLKXg","namespace",[48,79,81],{"class":80},"sC09Y"," App\\SupplierImport",[48,83,85],{"class":84},"s5ixo",";\n",[48,87,89],{"class":50,"line":88},4,[48,90,70],{"emptyLinePlaceholder":69},[48,92,94,97,100,103],{"class":50,"line":93},5,[48,95,96],{"class":76},"use",[48,98,99],{"class":84}," App\\Models\\",[48,101,102],{"class":80},"Product",[48,104,85],{"class":84},[48,106,108,110,113,116],{"class":50,"line":107},6,[48,109,96],{"class":76},[48,111,112],{"class":84}," Illuminate\\Support\\",[48,114,115],{"class":80},"Collection",[48,117,85],{"class":84},[48,119,121],{"class":50,"line":120},7,[48,122,70],{"emptyLinePlaceholder":69},[48,124,126,129,132,135],{"class":50,"line":125},8,[48,127,128],{"class":76},"final",[48,130,131],{"class":76}," readonly",[48,133,134],{"class":76}," class",[48,136,137],{"class":80}," InventoryChange\n",[48,139,141],{"class":50,"line":140},9,[48,142,143],{"class":84},"{\n",[48,145,147,150,153,157],{"class":50,"line":146},10,[48,148,149],{"class":76},"    public",[48,151,152],{"class":76}," function",[48,154,156],{"class":155},"s_Sar"," __construct",[48,158,159],{"class":84},"(\n",[48,161,163,166,169,173],{"class":50,"line":162},11,[48,164,165],{"class":76},"        public",[48,167,168],{"class":80}," Product",[48,170,172],{"class":171},"sJa8x"," $product",[48,174,175],{"class":84},",\n",[48,177,179,181,185,188],{"class":50,"line":178},12,[48,180,165],{"class":76},[48,182,184],{"class":183},"sip94"," int",[48,186,187],{"class":171}," $previousStockLevel",[48,189,175],{"class":84},[48,191,193,195,197,200],{"class":50,"line":192},13,[48,194,165],{"class":76},[48,196,184],{"class":183},[48,198,199],{"class":171}," $newStockLevel",[48,201,175],{"class":84},[48,203,205],{"class":50,"line":204},14,[48,206,207],{"class":84},"    ) {\n",[48,209,211],{"class":50,"line":210},15,[48,212,213],{"class":84},"    }\n",[48,215,217],{"class":50,"line":216},16,[48,218,70],{"emptyLinePlaceholder":69},[48,220,222,224,226,230,233],{"class":50,"line":221},17,[48,223,149],{"class":76},[48,225,152],{"class":76},[48,227,229],{"class":228},"sAdtL"," isUnusual",[48,231,232],{"class":84},"(): ",[48,234,235],{"class":183},"bool\n",[48,237,239],{"class":50,"line":238},18,[48,240,241],{"class":84},"    {\n",[48,243,245,248,251,254,257,261,264,267,270,273,275,278],{"class":50,"line":244},19,[48,246,247],{"class":171},"        $difference",[48,249,250],{"class":54}," =",[48,252,253],{"class":155}," abs",[48,255,256],{"class":84},"(",[48,258,260],{"class":259},"s2QsP","$this",[48,262,263],{"class":84},"->",[48,265,266],{"class":171},"newStockLevel",[48,268,269],{"class":54}," -",[48,271,272],{"class":259}," $this",[48,274,263],{"class":84},[48,276,277],{"class":171},"previousStockLevel",[48,279,280],{"class":84},");\n",[48,282,284],{"class":50,"line":283},20,[48,285,70],{"emptyLinePlaceholder":69},[48,287,289,292,294,296,298,301],{"class":50,"line":288},21,[48,290,291],{"class":76},"        return",[48,293,272],{"class":259},[48,295,263],{"class":84},[48,297,277],{"class":171},[48,299,300],{"class":54}," ===",[48,302,304],{"class":303},"sAGMh"," 0\n",[48,306,308,311,313,315,317,320],{"class":50,"line":307},22,[48,309,310],{"class":58},"            ?",[48,312,272],{"class":259},[48,314,263],{"class":84},[48,316,266],{"class":171},[48,318,319],{"class":54}," >",[48,321,304],{"class":303},[48,323,325,328,331,333,336,338,340,342,345,348],{"class":50,"line":324},23,[48,326,327],{"class":58},"            :",[48,329,330],{"class":171}," $difference",[48,332,319],{"class":54},[48,334,335],{"class":84}," (",[48,337,260],{"class":259},[48,339,263],{"class":84},[48,341,277],{"class":171},[48,343,344],{"class":54}," *",[48,346,347],{"class":303}," 0.5",[48,349,280],{"class":84},[48,351,353],{"class":50,"line":352},24,[48,354,213],{"class":84},[48,356,358],{"class":50,"line":357},25,[48,359,360],{"class":84},"}\n",[48,362,364],{"class":50,"line":363},26,[48,365,70],{"emptyLinePlaceholder":69},[48,367,369,372],{"class":50,"line":368},27,[48,370,371],{"class":76},"class",[48,373,374],{"class":80}," SupplierImportContext\n",[48,376,378],{"class":50,"line":377},28,[48,379,143],{"class":84},[48,381,383,385,387,389],{"class":50,"line":382},29,[48,384,149],{"class":76},[48,386,152],{"class":76},[48,388,156],{"class":155},[48,390,159],{"class":84},[48,392,394,396,399,402],{"class":50,"line":393},30,[48,395,165],{"class":76},[48,397,398],{"class":80}," Collection",[48,400,401],{"class":171}," $rows",[48,403,175],{"class":84},[48,405,407,409,411,414],{"class":50,"line":406},31,[48,408,165],{"class":76},[48,410,398],{"class":80},[48,412,413],{"class":171}," $validRows",[48,415,175],{"class":84},[48,417,419,421,423,426],{"class":50,"line":418},32,[48,420,165],{"class":76},[48,422,398],{"class":80},[48,424,425],{"class":171}," $inventoryChanges",[48,427,175],{"class":84},[48,429,431,433,435,438],{"class":50,"line":430},33,[48,432,165],{"class":76},[48,434,398],{"class":80},[48,436,437],{"class":171}," $issues",[48,439,175],{"class":84},[48,441,443],{"class":50,"line":442},34,[48,444,207],{"class":84},[48,446,448],{"class":50,"line":447},35,[48,449,213],{"class":84},[48,451,453],{"class":50,"line":452},36,[48,454,70],{"emptyLinePlaceholder":69},[48,456,458,460,462,465,467,470,473,476,479,482,484,486,489,492],{"class":50,"line":457},37,[48,459,149],{"class":76},[48,461,152],{"class":76},[48,463,464],{"class":228}," rejectRow",[48,466,256],{"class":84},[48,468,469],{"class":183},"array",[48,471,472],{"class":171}," $row",[48,474,475],{"class":84},", ",[48,477,478],{"class":183},"string",[48,480,481],{"class":171}," $type",[48,483,475],{"class":84},[48,485,478],{"class":183},[48,487,488],{"class":171}," $message",[48,490,491],{"class":84},"): ",[48,493,494],{"class":183},"void\n",[48,496,498],{"class":50,"line":497},38,[48,499,241],{"class":84},[48,501,503,506,508,511,513,516],{"class":50,"line":502},39,[48,504,505],{"class":259},"        $this",[48,507,263],{"class":84},[48,509,510],{"class":171},"issues",[48,512,263],{"class":84},[48,514,515],{"class":228},"push",[48,517,518],{"class":84},"([\n",[48,520,522,526,529,532,535,538],{"class":50,"line":521},40,[48,523,525],{"class":524},"sDhpE","            'row_number'",[48,527,528],{"class":84}," => ",[48,530,531],{"class":171},"$row",[48,533,534],{"class":84},"[",[48,536,537],{"class":524},"'row_number'",[48,539,540],{"class":84},"],\n",[48,542,544,547,549,552],{"class":50,"line":543},41,[48,545,546],{"class":524},"            'type'",[48,548,528],{"class":84},[48,550,551],{"class":171},"$type",[48,553,175],{"class":84},[48,555,557,560,562,565],{"class":50,"line":556},42,[48,558,559],{"class":524},"            'message'",[48,561,528],{"class":84},[48,563,564],{"class":171},"$message",[48,566,175],{"class":84},[48,568,570],{"class":50,"line":569},43,[48,571,572],{"class":84},"        ]);\n",[48,574,576],{"class":50,"line":575},44,[48,577,70],{"emptyLinePlaceholder":69},[48,579,581,583,585,588,590,592,594],{"class":50,"line":580},45,[48,582,505],{"class":259},[48,584,263],{"class":84},[48,586,587],{"class":171},"validRows",[48,589,250],{"class":54},[48,591,272],{"class":259},[48,593,263],{"class":84},[48,595,596],{"class":171},"validRows\n",[48,598,600,603,606,608,611,613,615,618],{"class":50,"line":599},46,[48,601,602],{"class":84},"            ->",[48,604,605],{"class":228},"reject",[48,607,256],{"class":84},[48,609,610],{"class":76},"fn",[48,612,335],{"class":84},[48,614,469],{"class":183},[48,616,617],{"class":171}," $candidate",[48,619,620],{"class":84},") =>\n",[48,622,624,627,629,631,634,637,639,641,643],{"class":50,"line":623},47,[48,625,626],{"class":171},"                $candidate",[48,628,534],{"class":84},[48,630,537],{"class":524},[48,632,633],{"class":84},"] ",[48,635,636],{"class":54},"===",[48,638,472],{"class":171},[48,640,534],{"class":84},[48,642,537],{"class":524},[48,644,645],{"class":84},"]\n",[48,647,649],{"class":50,"line":648},48,[48,650,651],{"class":84},"            )\n",[48,653,655,657,660],{"class":50,"line":654},49,[48,656,602],{"class":84},[48,658,659],{"class":228},"values",[48,661,662],{"class":84},"();\n",[48,664,666],{"class":50,"line":665},50,[48,667,213],{"class":84},[48,669,671],{"class":50,"line":670},51,[48,672,360],{"class":84},[10,674,675,676,679,680,682,683,686],{},"The context makes both the input and output of the import explicit. ",[45,677,678],{},"rows"," preserves the complete numbered input for reporting, while ",[45,681,587],{}," is the shrinking set that may reach the database update. Validation and matching pipes call ",[45,684,685],{},"rejectRow()"," when a record cannot continue, which both records a consistently shaped issue and removes that row from later processing.",[10,688,689,692,693,695,696,699],{},[45,690,691],{},"InventoryChange"," is a snapshot of one successful update. Keeping the previous and new quantities in a result object avoids attaching undocumented temporary properties to an Eloquent ",[45,694,102],{},". Its ",[45,697,698],{},"isUnusual()"," method also defines what happens when the previous quantity was zero.",[30,701,703],{"id":702},"the-processing-stages","The processing stages",[10,705,706],{},"The import service can describe the workflow in one place:",[38,708,710],{"className":40,"code":709,"language":42,"meta":43,"style":43},"use Illuminate\\Support\\Facades\\DB;\n\npublic function handle(Collection $rows): SupplierImportContext\n{\n    $numberedRows = $rows\n        ->values()\n        ->map(fn (array $row, int $index) => [\n            ...$row,\n            'row_number' => $index + 1,\n        ]);\n\n    $context = new SupplierImportContext(\n        rows: $numberedRows,\n        validRows: $numberedRows,\n        inventoryChanges: collect(),\n        issues: collect(),\n    );\n\n    return DB::transaction(fn () =>\n        app(Pipeline::class)\n            ->send($context)\n            ->through([\n                NormaliseSupplierRows::class,\n                ValidateRequiredFields::class,\n                MatchProductsBySupplierCode::class,\n                ApplyInventoryChanges::class,\n                FlagStockAnomalies::class,\n            ])\n            ->thenReturn()\n    );\n}\n",[45,711,712,724,728,749,753,763,773,801,810,827,831,835,850,860,869,880,889,894,898,919,936,950,959,970,981,992,1003,1014,1019,1028,1032],{"__ignoreMap":43},[48,713,714,716,719,722],{"class":50,"line":51},[48,715,96],{"class":76},[48,717,718],{"class":84}," Illuminate\\Support\\Facades\\",[48,720,721],{"class":80},"DB",[48,723,85],{"class":84},[48,725,726],{"class":50,"line":66},[48,727,70],{"emptyLinePlaceholder":69},[48,729,730,733,735,738,740,742,744,746],{"class":50,"line":73},[48,731,732],{"class":76},"public",[48,734,152],{"class":76},[48,736,737],{"class":228}," handle",[48,739,256],{"class":84},[48,741,115],{"class":80},[48,743,401],{"class":171},[48,745,491],{"class":84},[48,747,748],{"class":80},"SupplierImportContext\n",[48,750,751],{"class":50,"line":88},[48,752,143],{"class":84},[48,754,755,758,760],{"class":50,"line":93},[48,756,757],{"class":171},"    $numberedRows",[48,759,250],{"class":54},[48,761,762],{"class":171}," $rows\n",[48,764,765,768,770],{"class":50,"line":107},[48,766,767],{"class":84},"        ->",[48,769,659],{"class":228},[48,771,772],{"class":84},"()\n",[48,774,775,777,780,782,784,786,788,790,792,795,798],{"class":50,"line":120},[48,776,767],{"class":84},[48,778,779],{"class":228},"map",[48,781,256],{"class":84},[48,783,610],{"class":76},[48,785,335],{"class":84},[48,787,469],{"class":183},[48,789,472],{"class":171},[48,791,475],{"class":84},[48,793,794],{"class":183},"int",[48,796,797],{"class":171}," $index",[48,799,800],{"class":84},") => [\n",[48,802,803,806,808],{"class":50,"line":125},[48,804,805],{"class":84},"            ...",[48,807,531],{"class":171},[48,809,175],{"class":84},[48,811,812,814,816,819,822,825],{"class":50,"line":140},[48,813,525],{"class":524},[48,815,528],{"class":84},[48,817,818],{"class":171},"$index",[48,820,821],{"class":54}," +",[48,823,824],{"class":303}," 1",[48,826,175],{"class":84},[48,828,829],{"class":50,"line":146},[48,830,572],{"class":84},[48,832,833],{"class":50,"line":162},[48,834,70],{"emptyLinePlaceholder":69},[48,836,837,840,842,845,848],{"class":50,"line":178},[48,838,839],{"class":171},"    $context",[48,841,250],{"class":54},[48,843,844],{"class":76}," new",[48,846,847],{"class":80}," SupplierImportContext",[48,849,159],{"class":84},[48,851,852,855,858],{"class":50,"line":192},[48,853,854],{"class":84},"        rows:",[48,856,857],{"class":171}," $numberedRows",[48,859,175],{"class":84},[48,861,862,865,867],{"class":50,"line":204},[48,863,864],{"class":84},"        validRows:",[48,866,857],{"class":171},[48,868,175],{"class":84},[48,870,871,874,877],{"class":50,"line":210},[48,872,873],{"class":84},"        inventoryChanges:",[48,875,876],{"class":228}," collect",[48,878,879],{"class":84},"(),\n",[48,881,882,885,887],{"class":50,"line":216},[48,883,884],{"class":84},"        issues:",[48,886,876],{"class":228},[48,888,879],{"class":84},[48,890,891],{"class":50,"line":221},[48,892,893],{"class":84},"    );\n",[48,895,896],{"class":50,"line":238},[48,897,70],{"emptyLinePlaceholder":69},[48,899,900,903,906,909,912,914,916],{"class":50,"line":244},[48,901,902],{"class":76},"    return",[48,904,905],{"class":80}," DB",[48,907,908],{"class":84},"::",[48,910,911],{"class":228},"transaction",[48,913,256],{"class":84},[48,915,610],{"class":76},[48,917,918],{"class":84}," () =>\n",[48,920,921,924,926,929,931,933],{"class":50,"line":283},[48,922,923],{"class":228},"        app",[48,925,256],{"class":84},[48,927,928],{"class":80},"Pipeline",[48,930,908],{"class":84},[48,932,371],{"class":76},[48,934,935],{"class":84},")\n",[48,937,938,940,943,945,948],{"class":50,"line":288},[48,939,602],{"class":84},[48,941,942],{"class":228},"send",[48,944,256],{"class":84},[48,946,947],{"class":171},"$context",[48,949,935],{"class":84},[48,951,952,954,957],{"class":50,"line":307},[48,953,602],{"class":84},[48,955,956],{"class":228},"through",[48,958,518],{"class":84},[48,960,961,964,966,968],{"class":50,"line":324},[48,962,963],{"class":80},"                NormaliseSupplierRows",[48,965,908],{"class":84},[48,967,371],{"class":76},[48,969,175],{"class":84},[48,971,972,975,977,979],{"class":50,"line":352},[48,973,974],{"class":80},"                ValidateRequiredFields",[48,976,908],{"class":84},[48,978,371],{"class":76},[48,980,175],{"class":84},[48,982,983,986,988,990],{"class":50,"line":357},[48,984,985],{"class":80},"                MatchProductsBySupplierCode",[48,987,908],{"class":84},[48,989,371],{"class":76},[48,991,175],{"class":84},[48,993,994,997,999,1001],{"class":50,"line":363},[48,995,996],{"class":80},"                ApplyInventoryChanges",[48,998,908],{"class":84},[48,1000,371],{"class":76},[48,1002,175],{"class":84},[48,1004,1005,1008,1010,1012],{"class":50,"line":368},[48,1006,1007],{"class":80},"                FlagStockAnomalies",[48,1009,908],{"class":84},[48,1011,371],{"class":76},[48,1013,175],{"class":84},[48,1015,1016],{"class":50,"line":377},[48,1017,1018],{"class":84},"            ])\n",[48,1020,1021,1023,1026],{"class":50,"line":382},[48,1022,602],{"class":84},[48,1024,1025],{"class":228},"thenReturn",[48,1027,772],{"class":84},[48,1029,1030],{"class":50,"line":393},[48,1031,893],{"class":84},[48,1033,1034],{"class":50,"line":406},[48,1035,360],{"class":84},[10,1037,1038,1039,1042,1043,1046,1047,1050,1051,1053,1054,1057,1058,1060],{},"Each pipe has one responsibility. ",[45,1040,1041],{},"NormaliseSupplierRows"," standardises column names and trims values in both row Collections. ",[45,1044,1045],{},"ValidateRequiredFields"," and ",[45,1048,1049],{},"MatchProductsBySupplierCode"," call ",[45,1052,685],{}," for records that cannot continue. ",[45,1055,1056],{},"ApplyInventoryChanges"," therefore iterates only over ",[45,1059,587],{},".",[10,1062,1063],{},"For each successful update, that pipe captures the state before changing the product:",[38,1065,1067],{"className":40,"code":1066,"language":42,"meta":43,"style":43},"$previousStockLevel = $product->stock_level;\n$newStockLevel = (int) $row['stock_level'];\n\n$product->update(['stock_level' => $newStockLevel]);\n\n$context->inventoryChanges->push(new InventoryChange(\n    product: $product,\n    previousStockLevel: $previousStockLevel,\n    newStockLevel: $newStockLevel,\n));\n",[45,1068,1069,1085,1109,1113,1135,1139,1162,1171,1180,1189],{"__ignoreMap":43},[48,1070,1071,1074,1076,1078,1080,1083],{"class":50,"line":51},[48,1072,1073],{"class":171},"$previousStockLevel",[48,1075,250],{"class":54},[48,1077,172],{"class":171},[48,1079,263],{"class":84},[48,1081,1082],{"class":171},"stock_level",[48,1084,85],{"class":84},[48,1086,1087,1090,1092,1094,1096,1099,1101,1103,1106],{"class":50,"line":66},[48,1088,1089],{"class":171},"$newStockLevel",[48,1091,250],{"class":54},[48,1093,335],{"class":84},[48,1095,794],{"class":76},[48,1097,1098],{"class":84},") ",[48,1100,531],{"class":171},[48,1102,534],{"class":84},[48,1104,1105],{"class":524},"'stock_level'",[48,1107,1108],{"class":84},"];\n",[48,1110,1111],{"class":50,"line":73},[48,1112,70],{"emptyLinePlaceholder":69},[48,1114,1115,1118,1120,1123,1126,1128,1130,1132],{"class":50,"line":88},[48,1116,1117],{"class":171},"$product",[48,1119,263],{"class":84},[48,1121,1122],{"class":228},"update",[48,1124,1125],{"class":84},"([",[48,1127,1105],{"class":524},[48,1129,528],{"class":84},[48,1131,1089],{"class":171},[48,1133,1134],{"class":84},"]);\n",[48,1136,1137],{"class":50,"line":93},[48,1138,70],{"emptyLinePlaceholder":69},[48,1140,1141,1143,1145,1148,1150,1152,1154,1157,1160],{"class":50,"line":107},[48,1142,947],{"class":171},[48,1144,263],{"class":84},[48,1146,1147],{"class":171},"inventoryChanges",[48,1149,263],{"class":84},[48,1151,515],{"class":228},[48,1153,256],{"class":84},[48,1155,1156],{"class":76},"new",[48,1158,1159],{"class":80}," InventoryChange",[48,1161,159],{"class":84},[48,1163,1164,1167,1169],{"class":50,"line":120},[48,1165,1166],{"class":84},"    product:",[48,1168,172],{"class":171},[48,1170,175],{"class":84},[48,1172,1173,1176,1178],{"class":50,"line":125},[48,1174,1175],{"class":84},"    previousStockLevel:",[48,1177,187],{"class":171},[48,1179,175],{"class":84},[48,1181,1182,1185,1187],{"class":50,"line":140},[48,1183,1184],{"class":84},"    newStockLevel:",[48,1186,199],{"class":171},[48,1188,175],{"class":84},[48,1190,1191],{"class":50,"line":146},[48,1192,1193],{"class":84},"));\n",[10,1195,1196],{},"The service wraps the complete Pipeline in one transaction. Expected bad rows are reported and skipped, while an unexpected exception rolls back every inventory update from this import. If an application deliberately wants each valid row to commit independently, that is a different partial-success policy and should be expressed with a per-row transaction instead.",[10,1198,1199],{},"The Pipeline is concerned with processing. It is not where we decide how to present the outcome to a person.",[30,1201,1203],{"id":1202},"use-collections-to-understand-the-result","Use Collections to understand the result",[10,1205,1206],{},"After the Pipeline has finished, the context contains the information needed for a reconciliation report.",[38,1208,1210],{"className":40,"code":1209,"language":42,"meta":43,"style":43},"$context = $importSupplierInventory->handle($rows);\n\n$issuesByType = $context->issues\n    ->groupBy('type')\n    ->map(fn (Collection $issues) => $issues->count());\n\n$updatedByWarehouse = $context->inventoryChanges\n    ->groupBy(fn (InventoryChange $change) => $change->product->warehouse_id)\n    ->map(fn (Collection $changes) => $changes->count());\n",[45,1211,1212,1233,1237,1252,1267,1297,1301,1315,1349],{"__ignoreMap":43},[48,1213,1214,1216,1218,1221,1223,1226,1228,1231],{"class":50,"line":51},[48,1215,947],{"class":171},[48,1217,250],{"class":54},[48,1219,1220],{"class":171}," $importSupplierInventory",[48,1222,263],{"class":84},[48,1224,1225],{"class":228},"handle",[48,1227,256],{"class":84},[48,1229,1230],{"class":171},"$rows",[48,1232,280],{"class":84},[48,1234,1235],{"class":50,"line":66},[48,1236,70],{"emptyLinePlaceholder":69},[48,1238,1239,1242,1244,1247,1249],{"class":50,"line":73},[48,1240,1241],{"class":171},"$issuesByType",[48,1243,250],{"class":54},[48,1245,1246],{"class":171}," $context",[48,1248,263],{"class":84},[48,1250,1251],{"class":171},"issues\n",[48,1253,1254,1257,1260,1262,1265],{"class":50,"line":88},[48,1255,1256],{"class":84},"    ->",[48,1258,1259],{"class":228},"groupBy",[48,1261,256],{"class":84},[48,1263,1264],{"class":524},"'type'",[48,1266,935],{"class":84},[48,1268,1269,1271,1273,1275,1277,1279,1281,1283,1286,1289,1291,1294],{"class":50,"line":93},[48,1270,1256],{"class":84},[48,1272,779],{"class":228},[48,1274,256],{"class":84},[48,1276,610],{"class":76},[48,1278,335],{"class":84},[48,1280,115],{"class":80},[48,1282,437],{"class":171},[48,1284,1285],{"class":84},") => ",[48,1287,1288],{"class":171},"$issues",[48,1290,263],{"class":84},[48,1292,1293],{"class":228},"count",[48,1295,1296],{"class":84},"());\n",[48,1298,1299],{"class":50,"line":107},[48,1300,70],{"emptyLinePlaceholder":69},[48,1302,1303,1306,1308,1310,1312],{"class":50,"line":120},[48,1304,1305],{"class":171},"$updatedByWarehouse",[48,1307,250],{"class":54},[48,1309,1246],{"class":171},[48,1311,263],{"class":84},[48,1313,1314],{"class":171},"inventoryChanges\n",[48,1316,1317,1319,1321,1323,1325,1327,1329,1332,1334,1337,1339,1342,1344,1347],{"class":50,"line":125},[48,1318,1256],{"class":84},[48,1320,1259],{"class":228},[48,1322,256],{"class":84},[48,1324,610],{"class":76},[48,1326,335],{"class":84},[48,1328,691],{"class":80},[48,1330,1331],{"class":171}," $change",[48,1333,1285],{"class":84},[48,1335,1336],{"class":171},"$change",[48,1338,263],{"class":84},[48,1340,1341],{"class":171},"product",[48,1343,263],{"class":84},[48,1345,1346],{"class":171},"warehouse_id",[48,1348,935],{"class":84},[48,1350,1351,1353,1355,1357,1359,1361,1363,1366,1368,1371,1373,1375],{"class":50,"line":140},[48,1352,1256],{"class":84},[48,1354,779],{"class":228},[48,1356,256],{"class":84},[48,1358,610],{"class":76},[48,1360,335],{"class":84},[48,1362,115],{"class":80},[48,1364,1365],{"class":171}," $changes",[48,1367,1285],{"class":84},[48,1369,1370],{"class":171},"$changes",[48,1372,263],{"class":84},[48,1374,1293],{"class":228},[48,1376,1296],{"class":84},[10,1378,1379],{},"This is the point where Collections shine. We already have the import result in memory, and we want to ask useful questions about it.",[10,1381,1382],{},"We can also identify unusually large stock changes. Perhaps any change of more than 50 percent should be reviewed before the supplier feed is trusted fully:",[38,1384,1386],{"className":40,"code":1385,"language":42,"meta":43,"style":43},"$unusualChanges = $context->inventoryChanges\n    ->filter(fn (InventoryChange $change) => $change->isUnusual());\n",[45,1387,1388,1401],{"__ignoreMap":43},[48,1389,1390,1393,1395,1397,1399],{"class":50,"line":51},[48,1391,1392],{"class":171},"$unusualChanges",[48,1394,250],{"class":54},[48,1396,1246],{"class":171},[48,1398,263],{"class":84},[48,1400,1314],{"class":171},[48,1402,1403,1405,1408,1410,1412,1414,1416,1418,1420,1422,1424,1427],{"class":50,"line":66},[48,1404,1256],{"class":84},[48,1406,1407],{"class":228},"filter",[48,1409,256],{"class":84},[48,1411,610],{"class":76},[48,1413,335],{"class":84},[48,1415,691],{"class":80},[48,1417,1331],{"class":171},[48,1419,1285],{"class":84},[48,1421,1336],{"class":171},[48,1423,263],{"class":84},[48,1425,1426],{"class":228},"isUnusual",[48,1428,1296],{"class":84},[10,1430,1431],{},"The result is not a vague boolean hidden inside the import process. It is a named Collection that can be shown in an admin screen, included in a Slack notification, or queued for review.",[30,1433,1435],{"id":1434},"produce-an-honest-report","Produce an honest report",[10,1437,1438],{},"The final report can describe both successful work and the parts that need attention:",[38,1440,1442],{"className":40,"code":1441,"language":42,"meta":43,"style":43},"$report = [\n    'rows_received' => $context->rows->count(),\n    'products_updated' => $context->inventoryChanges->count(),\n    'issues_by_type' => $issuesByType,\n    'updates_by_warehouse' => $updatedByWarehouse,\n    'unusual_stock_changes' => $unusualChanges,\n];\n",[45,1443,1444,1454,1473,1492,1503,1514,1525],{"__ignoreMap":43},[48,1445,1446,1449,1451],{"class":50,"line":51},[48,1447,1448],{"class":171},"$report",[48,1450,250],{"class":54},[48,1452,1453],{"class":84}," [\n",[48,1455,1456,1459,1461,1463,1465,1467,1469,1471],{"class":50,"line":66},[48,1457,1458],{"class":524},"    'rows_received'",[48,1460,528],{"class":84},[48,1462,947],{"class":171},[48,1464,263],{"class":84},[48,1466,678],{"class":171},[48,1468,263],{"class":84},[48,1470,1293],{"class":228},[48,1472,879],{"class":84},[48,1474,1475,1478,1480,1482,1484,1486,1488,1490],{"class":50,"line":73},[48,1476,1477],{"class":524},"    'products_updated'",[48,1479,528],{"class":84},[48,1481,947],{"class":171},[48,1483,263],{"class":84},[48,1485,1147],{"class":171},[48,1487,263],{"class":84},[48,1489,1293],{"class":228},[48,1491,879],{"class":84},[48,1493,1494,1497,1499,1501],{"class":50,"line":88},[48,1495,1496],{"class":524},"    'issues_by_type'",[48,1498,528],{"class":84},[48,1500,1241],{"class":171},[48,1502,175],{"class":84},[48,1504,1505,1508,1510,1512],{"class":50,"line":93},[48,1506,1507],{"class":524},"    'updates_by_warehouse'",[48,1509,528],{"class":84},[48,1511,1305],{"class":171},[48,1513,175],{"class":84},[48,1515,1516,1519,1521,1523],{"class":50,"line":107},[48,1517,1518],{"class":524},"    'unusual_stock_changes'",[48,1520,528],{"class":84},[48,1522,1392],{"class":171},[48,1524,175],{"class":84},[48,1526,1527],{"class":50,"line":120},[48,1528,1108],{"class":84},[10,1530,1531],{},"This is a much more useful outcome than simply returning \"import completed\". Operations can see how much changed, whether the feed has data-quality issues, and what needs a person to look at next.",[30,1533,1535],{"id":1534},"why-not-put-it-all-in-one-class","Why not put it all in one class?",[10,1537,1538],{},"A single import service with a few loops can work. It may validate rows, update products, store errors, and return a report. The issue is not that one class is always wrong; the issue is that it tends to grow in several directions at once.",[10,1540,1541],{},"Pipelines keep the processing stages separate and testable. Collections keep the result analysis expressive. The service coordinating the import stays small enough to understand.",[10,1543,1544],{},"Each piece uses Laravel for the job it is good at, without inventing a large internal framework around a routine business process.",[30,1546,1548],{"id":1547},"the-point-of-the-series","The point of the series",[10,1550,1551],{},"Collections, custom Collections, and Pipelines are all small tools. Their value becomes clear when an application has real rules, real data, and a team that needs to keep changing both safely.",[10,1553,1554],{},"Embracing the framework is not about avoiding custom code. It is about using Laravel's conventions where they help, then adding application-specific language where it matters. That gives us software that is easier to explain, easier to extend, and much more comfortable to hand to the next developer.",[1556,1557,1558],"style",{},"html pre.shiki code .sknuh, html code.shiki .sknuh{--shiki-default:#383A42;--shiki-dark:#56B6C2}html pre.shiki code .sblXP, html code.shiki .sblXP{--shiki-default:#383A42;--shiki-dark:#C678DD}html pre.shiki code .sxymB, html code.shiki .sxymB{--shiki-default:#986801;--shiki-dark:#ABB2BF}html pre.shiki code .sLKXg, html code.shiki .sLKXg{--shiki-default:#A626A4;--shiki-dark:#C678DD}html pre.shiki code .sC09Y, html code.shiki .sC09Y{--shiki-default:#C18401;--shiki-dark:#E5C07B}html pre.shiki code .s5ixo, html code.shiki .s5ixo{--shiki-default:#383A42;--shiki-dark:#ABB2BF}html pre.shiki code .s_Sar, html code.shiki .s_Sar{--shiki-default:#0184BC;--shiki-dark:#56B6C2}html pre.shiki code .sJa8x, html code.shiki .sJa8x{--shiki-default:#E45649;--shiki-dark:#E06C75}html pre.shiki code .sip94, html code.shiki .sip94{--shiki-default:#A626A4;--shiki-dark:#E5C07B}html pre.shiki code .sAdtL, html code.shiki .sAdtL{--shiki-default:#4078F2;--shiki-dark:#61AFEF}html pre.shiki code .s2QsP, html code.shiki .s2QsP{--shiki-default:#E45649;--shiki-dark:#E5C07B}html pre.shiki code .sAGMh, html code.shiki .sAGMh{--shiki-default:#986801;--shiki-dark:#D19A66}html pre.shiki code .sDhpE, html code.shiki .sDhpE{--shiki-default:#50A14F;--shiki-dark:#98C379}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":43,"searchDepth":66,"depth":66,"links":1560},[1561,1562,1563,1564,1565,1566],{"id":32,"depth":66,"text":33},{"id":702,"depth":66,"text":703},{"id":1202,"depth":66,"text":1203},{"id":1434,"depth":66,"text":1435},{"id":1534,"depth":66,"text":1535},{"id":1547,"depth":66,"text":1548},"A practical supplier inventory import that uses Pipelines for processing and Collections for reconciliation, reporting, and action.",false,"md",{},"\u002Farticles\u002Fembrace-the-framework-supplier-imports-with-collections-and-pipelines","2026-08-08",{"title":5,"description":1567},"Learn how Laravel Collections and Pipelines work together to validate supplier inventory imports and produce useful reconciliation reports.","embrace-the-framework","8","articles\u002Fembrace-the-framework-supplier-imports-with-collections-and-pipelines",[1579,42,1580,1581,1582],"laravel","collections","pipelines","imports","2026-08-05","t7FU1uuyHoL_Kn7MiSeX5tjixM3AWg2xc-gdxNe7xQI",[1586,1591,1596,1601,1606,1611,1616,1617,1622,1627,1631,1635],{"path":1587,"title":1588,"published":1589,"series":1575,"seriesOrder":1590},"\u002Farticles\u002Fembrace-the-framework-custom-collections-for-subscriptions","Embrace the Framework: Put Subscription Business Rules in a Custom Collection","2026-07-28","3",{"path":1592,"title":1593,"published":1594,"series":1575,"seriesOrder":1595},"\u002Farticles\u002Fembrace-the-framework-customer-eligibility-with-custom-collections","Embrace the Framework: Make Eligibility Rules Read Like Business Policy","2026-08-04","6",{"path":1597,"title":1598,"published":1599,"series":1575,"seriesOrder":1600},"\u002Farticles\u002Fembrace-the-framework-operational-reports-with-collections","Embrace the Framework: Build Clear Operational Reports With Collections","2026-08-02","5",{"path":1602,"title":1603,"published":1604,"series":1575,"seriesOrder":1605},"\u002Farticles\u002Fembrace-the-framework-order-fulfilment-with-collections","Embrace the Framework: Make Order Fulfilment Clearer With Collections","2026-07-25","2",{"path":1607,"title":1608,"published":1609,"series":1575,"seriesOrder":1610},"\u002Farticles\u002Fembrace-the-framework-order-processing-pipeline-that-can-grow","Embrace the Framework: Build an Order Processing Pipeline That Can Grow","2026-08-06","7",{"path":1612,"title":1613,"published":1614,"series":1575,"seriesOrder":1615},"\u002Farticles\u002Fembrace-the-framework-order-processing-with-pipelines","Embrace the Framework: Break Order Processing Into Steps With Pipelines","2026-07-31","4",{"path":1571,"title":5,"published":1572,"series":1575,"seriesOrder":1576},{"path":1618,"title":1619,"published":1620,"series":1575,"seriesOrder":1621},"\u002Farticles\u002Fembrace-the-framework-why-working-with-laravel-beats-working-around-it","Embrace the Framework: Why Working With Laravel Beats Working Around It","2026-07-23","1",{"path":1623,"title":1624,"published":1625,"series":1626,"seriesOrder":1626},"\u002Farticles\u002Fsimple-search-helper","A Simple Laravel Search Helper","2023-08-06",null,{"path":1628,"title":1629,"published":1630,"series":1626,"seriesOrder":1626},"\u002Farticles\u002Fsvg-tidy-local-svg-optimiser","SVG Tidy: Clean and Optimise SVGs Locally","2026-07-21",{"path":1632,"title":1633,"published":1634,"series":1626,"seriesOrder":1626},"\u002Farticles\u002Ftools-i-use","Tools I Use","2024-05-26",{"path":1636,"title":1637,"published":1638,"series":1626,"seriesOrder":1626},"\u002Farticles\u002Fwelcome","Welcome","2023-08-05",1786172129326]