[{"data":1,"prerenderedAt":1827},["ShallowReactive",2],{"\u002Farticles\u002Fembrace-the-framework-order-processing-pipeline-that-can-grow":3,"article-series:embrace-the-framework":1773},{"id":4,"title":5,"body":6,"description":1756,"draft":1757,"extension":1758,"meta":1759,"navigation":94,"path":1760,"published":1761,"seo":1762,"seoDescription":1763,"series":1764,"seriesOrder":1765,"stem":1766,"tags":1767,"updated":1771,"__hash__":1772},"articles\u002Farticles\u002Fembrace-the-framework-order-processing-pipeline-that-can-grow.md","Embrace the Framework: Build an Order Processing Pipeline That Can Grow",{"type":7,"value":8,"toc":1747},"minimark",[9,13,16,21,24,52,55,59,62,308,323,326,488,504,508,511,895,898,902,905,1145,1148,1151,1267,1270,1538,1552,1556,1559,1637,1640,1643,1647,1650,1656,1659,1666,1712,1727,1730,1734,1740,1743],[10,11,12],"p",{},"In the previous Pipeline article, we looked at why a series of focused processing stages is often easier to maintain than one large service method.",[10,14,15],{},"This time, let us build the shape of a complete order-processing workflow. The aim is not a perfect checkout implementation for every application. It is a practical example of how Laravel Pipelines let a workflow grow without making one class responsible for every decision.",[17,18,20],"h2",{"id":19},"the-workflow","The workflow",[10,22,23],{},"A wholesale order needs to pass through these stages:",[25,26,27,31,34,37,40,43,46,49],"ol",{},[28,29,30],"li",{},"normalise the submitted data;",[28,32,33],{},"validate the delivery address;",[28,35,36],{},"check customer payment terms;",[28,38,39],{},"reserve stock;",[28,41,42],{},"apply contract pricing;",[28,44,45],{},"calculate shipping;",[28,47,48],{},"flag exceptions for review; and",[28,50,51],{},"persist the order.",[10,53,54],{},"Some steps may stop the workflow by throwing an exception. Others enrich the order and let it continue. The order is important, and it should be visible.",[17,56,58],{"id":57},"start-with-a-context-object","Start with a context object",[10,60,61],{},"Passing an Eloquent model alone is sometimes enough. For a richer process, a small context object gives each pipe one shared place to store calculated values and flags.",[63,64,69],"pre",{"className":65,"code":66,"language":67,"meta":68,"style":68},"language-php shiki shiki-themes one-light one-dark-pro","\u003C?php\n\nnamespace App\\OrderProcessing;\n\nuse App\\Models\\Order;\nuse Illuminate\\Support\\Collection;\n\nclass OrderProcessingContext\n{\n    public function __construct(\n        public array $submittedData,\n        public Order $order,\n        public Collection $exceptions,\n    ) {\n    }\n\n    public function addException(string $message): void\n    {\n        $this->exceptions->push($message);\n    }\n}\n","php","",[70,71,72,89,96,111,116,130,143,148,157,163,179,196,209,222,228,234,239,265,271,297,302],"code",{"__ignoreMap":68},[73,74,77,81,85],"span",{"class":75,"line":76},"line",1,[73,78,80],{"class":79},"sknuh","\u003C",[73,82,84],{"class":83},"sblXP","?",[73,86,88],{"class":87},"sxymB","php\n",[73,90,92],{"class":75,"line":91},2,[73,93,95],{"emptyLinePlaceholder":94},true,"\n",[73,97,99,103,107],{"class":75,"line":98},3,[73,100,102],{"class":101},"sLKXg","namespace",[73,104,106],{"class":105},"sC09Y"," App\\OrderProcessing",[73,108,110],{"class":109},"s5ixo",";\n",[73,112,114],{"class":75,"line":113},4,[73,115,95],{"emptyLinePlaceholder":94},[73,117,119,122,125,128],{"class":75,"line":118},5,[73,120,121],{"class":101},"use",[73,123,124],{"class":109}," App\\Models\\",[73,126,127],{"class":105},"Order",[73,129,110],{"class":109},[73,131,133,135,138,141],{"class":75,"line":132},6,[73,134,121],{"class":101},[73,136,137],{"class":109}," Illuminate\\Support\\",[73,139,140],{"class":105},"Collection",[73,142,110],{"class":109},[73,144,146],{"class":75,"line":145},7,[73,147,95],{"emptyLinePlaceholder":94},[73,149,151,154],{"class":75,"line":150},8,[73,152,153],{"class":101},"class",[73,155,156],{"class":105}," OrderProcessingContext\n",[73,158,160],{"class":75,"line":159},9,[73,161,162],{"class":109},"{\n",[73,164,166,169,172,176],{"class":75,"line":165},10,[73,167,168],{"class":101},"    public",[73,170,171],{"class":101}," function",[73,173,175],{"class":174},"s_Sar"," __construct",[73,177,178],{"class":109},"(\n",[73,180,182,185,189,193],{"class":75,"line":181},11,[73,183,184],{"class":101},"        public",[73,186,188],{"class":187},"sip94"," array",[73,190,192],{"class":191},"sJa8x"," $submittedData",[73,194,195],{"class":109},",\n",[73,197,199,201,204,207],{"class":75,"line":198},12,[73,200,184],{"class":101},[73,202,203],{"class":105}," Order",[73,205,206],{"class":191}," $order",[73,208,195],{"class":109},[73,210,212,214,217,220],{"class":75,"line":211},13,[73,213,184],{"class":101},[73,215,216],{"class":105}," Collection",[73,218,219],{"class":191}," $exceptions",[73,221,195],{"class":109},[73,223,225],{"class":75,"line":224},14,[73,226,227],{"class":109},"    ) {\n",[73,229,231],{"class":75,"line":230},15,[73,232,233],{"class":109},"    }\n",[73,235,237],{"class":75,"line":236},16,[73,238,95],{"emptyLinePlaceholder":94},[73,240,242,244,246,250,253,256,259,262],{"class":75,"line":241},17,[73,243,168],{"class":101},[73,245,171],{"class":101},[73,247,249],{"class":248},"sAdtL"," addException",[73,251,252],{"class":109},"(",[73,254,255],{"class":187},"string",[73,257,258],{"class":191}," $message",[73,260,261],{"class":109},"): ",[73,263,264],{"class":187},"void\n",[73,266,268],{"class":75,"line":267},18,[73,269,270],{"class":109},"    {\n",[73,272,274,278,281,284,286,289,291,294],{"class":75,"line":273},19,[73,275,277],{"class":276},"s2QsP","        $this",[73,279,280],{"class":109},"->",[73,282,283],{"class":191},"exceptions",[73,285,280],{"class":109},[73,287,288],{"class":248},"push",[73,290,252],{"class":109},[73,292,293],{"class":191},"$message",[73,295,296],{"class":109},");\n",[73,298,300],{"class":75,"line":299},20,[73,301,233],{"class":109},[73,303,305],{"class":75,"line":304},21,[73,306,307],{"class":109},"}\n",[10,309,310,311,314,315,318,319,322],{},"The context is not a second model. It is simply the object travelling through the workflow. ",[70,312,313],{},"submittedData"," gives ",[70,316,317],{},"NormaliseOrderData"," the actual input it is responsible for cleaning, while ",[70,320,321],{},"order"," is the draft Eloquent model that later pipes inspect and change.",[10,324,325],{},"The caller constructs that context deliberately and loads the relationships used by the workflow:",[63,327,329],{"className":65,"code":328,"language":67,"meta":68,"style":68},"$order = Order::query()\n    ->with([\n        'customer.activeContract',\n        'items.product',\n    ])\n    ->findOrFail($orderId);\n\n$context = new OrderProcessingContext(\n    submittedData: $request->validated(),\n    order: $order,\n    exceptions: collect(),\n);\n\n$processed = app(ProcessOrder::class)->handle($context);\n",[70,330,331,350,361,369,376,381,395,399,414,430,439,449,453,457],{"__ignoreMap":68},[73,332,333,336,339,341,344,347],{"class":75,"line":76},[73,334,335],{"class":191},"$order",[73,337,338],{"class":79}," =",[73,340,203],{"class":105},[73,342,343],{"class":109},"::",[73,345,346],{"class":248},"query",[73,348,349],{"class":109},"()\n",[73,351,352,355,358],{"class":75,"line":91},[73,353,354],{"class":109},"    ->",[73,356,357],{"class":248},"with",[73,359,360],{"class":109},"([\n",[73,362,363,367],{"class":75,"line":98},[73,364,366],{"class":365},"sDhpE","        'customer.activeContract'",[73,368,195],{"class":109},[73,370,371,374],{"class":75,"line":113},[73,372,373],{"class":365},"        'items.product'",[73,375,195],{"class":109},[73,377,378],{"class":75,"line":118},[73,379,380],{"class":109},"    ])\n",[73,382,383,385,388,390,393],{"class":75,"line":132},[73,384,354],{"class":109},[73,386,387],{"class":248},"findOrFail",[73,389,252],{"class":109},[73,391,392],{"class":191},"$orderId",[73,394,296],{"class":109},[73,396,397],{"class":75,"line":145},[73,398,95],{"emptyLinePlaceholder":94},[73,400,401,404,406,409,412],{"class":75,"line":150},[73,402,403],{"class":191},"$context",[73,405,338],{"class":79},[73,407,408],{"class":101}," new",[73,410,411],{"class":105}," OrderProcessingContext",[73,413,178],{"class":109},[73,415,416,419,422,424,427],{"class":75,"line":159},[73,417,418],{"class":109},"    submittedData:",[73,420,421],{"class":191}," $request",[73,423,280],{"class":109},[73,425,426],{"class":248},"validated",[73,428,429],{"class":109},"(),\n",[73,431,432,435,437],{"class":75,"line":165},[73,433,434],{"class":109},"    order:",[73,436,206],{"class":191},[73,438,195],{"class":109},[73,440,441,444,447],{"class":75,"line":181},[73,442,443],{"class":109},"    exceptions:",[73,445,446],{"class":248}," collect",[73,448,429],{"class":109},[73,450,451],{"class":75,"line":198},[73,452,296],{"class":109},[73,454,455],{"class":75,"line":211},[73,456,95],{"emptyLinePlaceholder":94},[73,458,459,462,464,467,469,472,474,476,479,482,484,486],{"class":75,"line":224},[73,460,461],{"class":191},"$processed",[73,463,338],{"class":79},[73,465,466],{"class":248}," app",[73,468,252],{"class":109},[73,470,471],{"class":105},"ProcessOrder",[73,473,343],{"class":109},[73,475,153],{"class":101},[73,477,478],{"class":109},")->",[73,480,481],{"class":248},"handle",[73,483,252],{"class":109},[73,485,403],{"class":191},[73,487,296],{"class":109},[10,489,490,492,493,495,496,499,500,503],{},[70,491,317],{}," can now trim and standardise values in ",[70,494,313],{},", then fill the permitted fields on the draft order before the validation pipes run. The eager-loading list is equally deliberate: ",[70,497,498],{},"ApplyContractPricing"," needs the customer's active contract, the order items, and each item's product. The stock pipe should still reload and lock the relevant product rows with ",[70,501,502],{},"lockForUpdate()",", as shown in the earlier Pipeline article, because eager loading is not a concurrency control.",[17,505,507],{"id":506},"make-the-pipeline-explicit","Make the pipeline explicit",[10,509,510],{},"The service that starts the process stays small:",[63,512,514],{"className":65,"code":513,"language":67,"meta":68,"style":68},"\u003C?php\n\nnamespace App\\Services;\n\nuse App\\OrderProcessing\\OrderProcessingContext;\nuse App\\Pipelines\\Orders\\ApplyContractPricing;\nuse App\\Pipelines\\Orders\\CalculateShipping;\nuse App\\Pipelines\\Orders\\CheckCustomerTerms;\nuse App\\Pipelines\\Orders\\FlagExceptions;\nuse App\\Pipelines\\Orders\\NormaliseOrderData;\nuse App\\Pipelines\\Orders\\PersistOrder;\nuse App\\Pipelines\\Orders\\ReserveStock;\nuse App\\Pipelines\\Orders\\ValidateDeliveryAddress;\nuse Illuminate\\Pipeline\\Pipeline;\nuse Illuminate\\Support\\Facades\\DB;\n\nclass ProcessOrder\n{\n    public function handle(OrderProcessingContext $context): OrderProcessingContext\n    {\n        return DB::transaction(fn () =>\n            app(Pipeline::class)\n                ->send($context)\n                ->through([\n                    NormaliseOrderData::class,\n                    ValidateDeliveryAddress::class,\n                    CheckCustomerTerms::class,\n                    ReserveStock::class,\n                    ApplyContractPricing::class,\n                    CalculateShipping::class,\n                    FlagExceptions::class,\n                    PersistOrder::class,\n                ])\n                ->thenReturn()\n        );\n    }\n}\n",[70,515,516,524,528,537,541,553,564,575,586,597,607,618,629,640,652,664,668,675,679,700,704,725,742,757,767,779,791,803,815,827,839,851,863,869,879,885,890],{"__ignoreMap":68},[73,517,518,520,522],{"class":75,"line":76},[73,519,80],{"class":79},[73,521,84],{"class":83},[73,523,88],{"class":87},[73,525,526],{"class":75,"line":91},[73,527,95],{"emptyLinePlaceholder":94},[73,529,530,532,535],{"class":75,"line":98},[73,531,102],{"class":101},[73,533,534],{"class":105}," App\\Services",[73,536,110],{"class":109},[73,538,539],{"class":75,"line":113},[73,540,95],{"emptyLinePlaceholder":94},[73,542,543,545,548,551],{"class":75,"line":118},[73,544,121],{"class":101},[73,546,547],{"class":109}," App\\OrderProcessing\\",[73,549,550],{"class":105},"OrderProcessingContext",[73,552,110],{"class":109},[73,554,555,557,560,562],{"class":75,"line":132},[73,556,121],{"class":101},[73,558,559],{"class":109}," App\\Pipelines\\Orders\\",[73,561,498],{"class":105},[73,563,110],{"class":109},[73,565,566,568,570,573],{"class":75,"line":145},[73,567,121],{"class":101},[73,569,559],{"class":109},[73,571,572],{"class":105},"CalculateShipping",[73,574,110],{"class":109},[73,576,577,579,581,584],{"class":75,"line":150},[73,578,121],{"class":101},[73,580,559],{"class":109},[73,582,583],{"class":105},"CheckCustomerTerms",[73,585,110],{"class":109},[73,587,588,590,592,595],{"class":75,"line":159},[73,589,121],{"class":101},[73,591,559],{"class":109},[73,593,594],{"class":105},"FlagExceptions",[73,596,110],{"class":109},[73,598,599,601,603,605],{"class":75,"line":165},[73,600,121],{"class":101},[73,602,559],{"class":109},[73,604,317],{"class":105},[73,606,110],{"class":109},[73,608,609,611,613,616],{"class":75,"line":181},[73,610,121],{"class":101},[73,612,559],{"class":109},[73,614,615],{"class":105},"PersistOrder",[73,617,110],{"class":109},[73,619,620,622,624,627],{"class":75,"line":198},[73,621,121],{"class":101},[73,623,559],{"class":109},[73,625,626],{"class":105},"ReserveStock",[73,628,110],{"class":109},[73,630,631,633,635,638],{"class":75,"line":211},[73,632,121],{"class":101},[73,634,559],{"class":109},[73,636,637],{"class":105},"ValidateDeliveryAddress",[73,639,110],{"class":109},[73,641,642,644,647,650],{"class":75,"line":224},[73,643,121],{"class":101},[73,645,646],{"class":109}," Illuminate\\Pipeline\\",[73,648,649],{"class":105},"Pipeline",[73,651,110],{"class":109},[73,653,654,656,659,662],{"class":75,"line":230},[73,655,121],{"class":101},[73,657,658],{"class":109}," Illuminate\\Support\\Facades\\",[73,660,661],{"class":105},"DB",[73,663,110],{"class":109},[73,665,666],{"class":75,"line":236},[73,667,95],{"emptyLinePlaceholder":94},[73,669,670,672],{"class":75,"line":241},[73,671,153],{"class":101},[73,673,674],{"class":105}," ProcessOrder\n",[73,676,677],{"class":75,"line":267},[73,678,162],{"class":109},[73,680,681,683,685,688,690,692,695,697],{"class":75,"line":273},[73,682,168],{"class":101},[73,684,171],{"class":101},[73,686,687],{"class":248}," handle",[73,689,252],{"class":109},[73,691,550],{"class":105},[73,693,694],{"class":191}," $context",[73,696,261],{"class":109},[73,698,699],{"class":105},"OrderProcessingContext\n",[73,701,702],{"class":75,"line":299},[73,703,270],{"class":109},[73,705,706,709,712,714,717,719,722],{"class":75,"line":304},[73,707,708],{"class":101},"        return",[73,710,711],{"class":105}," DB",[73,713,343],{"class":109},[73,715,716],{"class":248},"transaction",[73,718,252],{"class":109},[73,720,721],{"class":101},"fn",[73,723,724],{"class":109}," () =>\n",[73,726,728,731,733,735,737,739],{"class":75,"line":727},22,[73,729,730],{"class":248},"            app",[73,732,252],{"class":109},[73,734,649],{"class":105},[73,736,343],{"class":109},[73,738,153],{"class":101},[73,740,741],{"class":109},")\n",[73,743,745,748,751,753,755],{"class":75,"line":744},23,[73,746,747],{"class":109},"                ->",[73,749,750],{"class":248},"send",[73,752,252],{"class":109},[73,754,403],{"class":191},[73,756,741],{"class":109},[73,758,760,762,765],{"class":75,"line":759},24,[73,761,747],{"class":109},[73,763,764],{"class":248},"through",[73,766,360],{"class":109},[73,768,770,773,775,777],{"class":75,"line":769},25,[73,771,772],{"class":105},"                    NormaliseOrderData",[73,774,343],{"class":109},[73,776,153],{"class":101},[73,778,195],{"class":109},[73,780,782,785,787,789],{"class":75,"line":781},26,[73,783,784],{"class":105},"                    ValidateDeliveryAddress",[73,786,343],{"class":109},[73,788,153],{"class":101},[73,790,195],{"class":109},[73,792,794,797,799,801],{"class":75,"line":793},27,[73,795,796],{"class":105},"                    CheckCustomerTerms",[73,798,343],{"class":109},[73,800,153],{"class":101},[73,802,195],{"class":109},[73,804,806,809,811,813],{"class":75,"line":805},28,[73,807,808],{"class":105},"                    ReserveStock",[73,810,343],{"class":109},[73,812,153],{"class":101},[73,814,195],{"class":109},[73,816,818,821,823,825],{"class":75,"line":817},29,[73,819,820],{"class":105},"                    ApplyContractPricing",[73,822,343],{"class":109},[73,824,153],{"class":101},[73,826,195],{"class":109},[73,828,830,833,835,837],{"class":75,"line":829},30,[73,831,832],{"class":105},"                    CalculateShipping",[73,834,343],{"class":109},[73,836,153],{"class":101},[73,838,195],{"class":109},[73,840,842,845,847,849],{"class":75,"line":841},31,[73,843,844],{"class":105},"                    FlagExceptions",[73,846,343],{"class":109},[73,848,153],{"class":101},[73,850,195],{"class":109},[73,852,854,857,859,861],{"class":75,"line":853},32,[73,855,856],{"class":105},"                    PersistOrder",[73,858,343],{"class":109},[73,860,153],{"class":101},[73,862,195],{"class":109},[73,864,866],{"class":75,"line":865},33,[73,867,868],{"class":109},"                ])\n",[73,870,872,874,877],{"class":75,"line":871},34,[73,873,747],{"class":109},[73,875,876],{"class":248},"thenReturn",[73,878,349],{"class":109},[73,880,882],{"class":75,"line":881},35,[73,883,884],{"class":109},"        );\n",[73,886,888],{"class":75,"line":887},36,[73,889,233],{"class":109},[73,891,893],{"class":75,"line":892},37,[73,894,307],{"class":109},[10,896,897],{},"This class owns the boundaries of the process: the order of the stages and the database transaction. It does not need to contain the detailed implementation of every rule.",[17,899,901],{"id":900},"keep-individual-pipes-narrow","Keep individual pipes narrow",[10,903,904],{},"For example, contract pricing can be isolated in its own pipe:",[63,906,908],{"className":65,"code":907,"language":67,"meta":68,"style":68},"\u003C?php\n\nnamespace App\\Pipelines\\Orders;\n\nuse App\\OrderProcessing\\OrderProcessingContext;\nuse Closure;\n\nclass ApplyContractPricing\n{\n    public function handle(OrderProcessingContext $context, Closure $next): mixed\n    {\n        $contract = $context->order->customer->activeContract;\n\n        foreach ($context->order->items as $item) {\n            $item->unit_price_in_cents = $contract\n                ? $contract->priceFor($item->product)\n                : $item->product->standard_price_in_cents;\n        }\n\n        return $next($context);\n    }\n}\n",[70,909,910,918,922,931,935,945,954,958,965,969,997,1001,1026,1030,1058,1073,1097,1116,1121,1125,1137,1141],{"__ignoreMap":68},[73,911,912,914,916],{"class":75,"line":76},[73,913,80],{"class":79},[73,915,84],{"class":83},[73,917,88],{"class":87},[73,919,920],{"class":75,"line":91},[73,921,95],{"emptyLinePlaceholder":94},[73,923,924,926,929],{"class":75,"line":98},[73,925,102],{"class":101},[73,927,928],{"class":105}," App\\Pipelines\\Orders",[73,930,110],{"class":109},[73,932,933],{"class":75,"line":113},[73,934,95],{"emptyLinePlaceholder":94},[73,936,937,939,941,943],{"class":75,"line":118},[73,938,121],{"class":101},[73,940,547],{"class":109},[73,942,550],{"class":105},[73,944,110],{"class":109},[73,946,947,949,952],{"class":75,"line":132},[73,948,121],{"class":101},[73,950,951],{"class":105}," Closure",[73,953,110],{"class":109},[73,955,956],{"class":75,"line":145},[73,957,95],{"emptyLinePlaceholder":94},[73,959,960,962],{"class":75,"line":150},[73,961,153],{"class":101},[73,963,964],{"class":105}," ApplyContractPricing\n",[73,966,967],{"class":75,"line":159},[73,968,162],{"class":109},[73,970,971,973,975,977,979,981,983,986,989,992,994],{"class":75,"line":165},[73,972,168],{"class":101},[73,974,171],{"class":101},[73,976,687],{"class":248},[73,978,252],{"class":109},[73,980,550],{"class":105},[73,982,694],{"class":191},[73,984,985],{"class":109},", ",[73,987,988],{"class":105},"Closure",[73,990,991],{"class":191}," $next",[73,993,261],{"class":109},[73,995,996],{"class":187},"mixed\n",[73,998,999],{"class":75,"line":181},[73,1000,270],{"class":109},[73,1002,1003,1006,1008,1010,1012,1014,1016,1019,1021,1024],{"class":75,"line":198},[73,1004,1005],{"class":191},"        $contract",[73,1007,338],{"class":79},[73,1009,694],{"class":191},[73,1011,280],{"class":109},[73,1013,321],{"class":191},[73,1015,280],{"class":109},[73,1017,1018],{"class":191},"customer",[73,1020,280],{"class":109},[73,1022,1023],{"class":191},"activeContract",[73,1025,110],{"class":109},[73,1027,1028],{"class":75,"line":211},[73,1029,95],{"emptyLinePlaceholder":94},[73,1031,1032,1035,1038,1040,1042,1044,1046,1049,1052,1055],{"class":75,"line":224},[73,1033,1034],{"class":101},"        foreach",[73,1036,1037],{"class":109}," (",[73,1039,403],{"class":191},[73,1041,280],{"class":109},[73,1043,321],{"class":191},[73,1045,280],{"class":109},[73,1047,1048],{"class":191},"items",[73,1050,1051],{"class":109}," as ",[73,1053,1054],{"class":191},"$item",[73,1056,1057],{"class":109},") {\n",[73,1059,1060,1063,1065,1068,1070],{"class":75,"line":230},[73,1061,1062],{"class":191},"            $item",[73,1064,280],{"class":109},[73,1066,1067],{"class":191},"unit_price_in_cents",[73,1069,338],{"class":79},[73,1071,1072],{"class":191}," $contract\n",[73,1074,1075,1078,1081,1083,1086,1088,1090,1092,1095],{"class":75,"line":236},[73,1076,1077],{"class":83},"                ?",[73,1079,1080],{"class":191}," $contract",[73,1082,280],{"class":109},[73,1084,1085],{"class":248},"priceFor",[73,1087,252],{"class":109},[73,1089,1054],{"class":191},[73,1091,280],{"class":109},[73,1093,1094],{"class":191},"product",[73,1096,741],{"class":109},[73,1098,1099,1102,1105,1107,1109,1111,1114],{"class":75,"line":241},[73,1100,1101],{"class":83},"                :",[73,1103,1104],{"class":191}," $item",[73,1106,280],{"class":109},[73,1108,1094],{"class":191},[73,1110,280],{"class":109},[73,1112,1113],{"class":191},"standard_price_in_cents",[73,1115,110],{"class":109},[73,1117,1118],{"class":75,"line":267},[73,1119,1120],{"class":109},"        }\n",[73,1122,1123],{"class":75,"line":273},[73,1124,95],{"emptyLinePlaceholder":94},[73,1126,1127,1129,1131,1133,1135],{"class":75,"line":299},[73,1128,708],{"class":101},[73,1130,991],{"class":191},[73,1132,252],{"class":109},[73,1134,403],{"class":191},[73,1136,296],{"class":109},[73,1138,1139],{"class":75,"line":304},[73,1140,233],{"class":109},[73,1142,1143],{"class":75,"line":727},[73,1144,307],{"class":109},[10,1146,1147],{},"The pipe answers one question: what price should this customer receive for each item? It does not need to know how the delivery address was checked or how the order will be saved.",[10,1149,1150],{},"A pipe that identifies a non-blocking concern can simply record it:",[63,1152,1154],{"className":65,"code":1153,"language":67,"meta":68,"style":68},"class FlagExceptions\n{\n    public function handle(OrderProcessingContext $context, Closure $next): mixed\n    {\n        if ($context->order->total_in_cents >= 100000) {\n            $context->addException('High-value order requires review.');\n        }\n\n        return $next($context);\n    }\n}\n",[70,1155,1156,1163,1167,1191,1195,1222,1239,1243,1247,1259,1263],{"__ignoreMap":68},[73,1157,1158,1160],{"class":75,"line":76},[73,1159,153],{"class":101},[73,1161,1162],{"class":105}," FlagExceptions\n",[73,1164,1165],{"class":75,"line":91},[73,1166,162],{"class":109},[73,1168,1169,1171,1173,1175,1177,1179,1181,1183,1185,1187,1189],{"class":75,"line":98},[73,1170,168],{"class":101},[73,1172,171],{"class":101},[73,1174,687],{"class":248},[73,1176,252],{"class":109},[73,1178,550],{"class":105},[73,1180,694],{"class":191},[73,1182,985],{"class":109},[73,1184,988],{"class":105},[73,1186,991],{"class":191},[73,1188,261],{"class":109},[73,1190,996],{"class":187},[73,1192,1193],{"class":75,"line":113},[73,1194,270],{"class":109},[73,1196,1197,1200,1202,1204,1206,1208,1210,1213,1216,1220],{"class":75,"line":118},[73,1198,1199],{"class":101},"        if",[73,1201,1037],{"class":109},[73,1203,403],{"class":191},[73,1205,280],{"class":109},[73,1207,321],{"class":191},[73,1209,280],{"class":109},[73,1211,1212],{"class":191},"total_in_cents",[73,1214,1215],{"class":79}," >=",[73,1217,1219],{"class":1218},"sAGMh"," 100000",[73,1221,1057],{"class":109},[73,1223,1224,1227,1229,1232,1234,1237],{"class":75,"line":132},[73,1225,1226],{"class":191},"            $context",[73,1228,280],{"class":109},[73,1230,1231],{"class":248},"addException",[73,1233,252],{"class":109},[73,1235,1236],{"class":365},"'High-value order requires review.'",[73,1238,296],{"class":109},[73,1240,1241],{"class":75,"line":145},[73,1242,1120],{"class":109},[73,1244,1245],{"class":75,"line":150},[73,1246,95],{"emptyLinePlaceholder":94},[73,1248,1249,1251,1253,1255,1257],{"class":75,"line":159},[73,1250,708],{"class":101},[73,1252,991],{"class":191},[73,1254,252],{"class":109},[73,1256,403],{"class":191},[73,1258,296],{"class":109},[73,1260,1261],{"class":75,"line":165},[73,1262,233],{"class":109},[73,1264,1265],{"class":75,"line":181},[73,1266,307],{"class":109},[10,1268,1269],{},"The final pipe makes the persistence boundary explicit:",[63,1271,1273],{"className":65,"code":1272,"language":67,"meta":68,"style":68},"\u003C?php\n\nnamespace App\\Pipelines\\Orders;\n\nuse App\\Models\\OrderItem;\nuse App\\OrderProcessing\\OrderProcessingContext;\nuse Closure;\n\nclass PersistOrder\n{\n    public function handle(OrderProcessingContext $context, Closure $next): mixed\n    {\n        $context->order->save();\n\n        $context->order->items->each(\n            fn (OrderItem $item) => $item->save()\n        );\n\n        $context->order->exceptions()->createMany(\n            $context->exceptions\n                ->map(fn (string $message) => ['message' => $message])\n                ->all()\n        );\n\n        return $next($context);\n    }\n}\n",[70,1274,1275,1283,1287,1295,1299,1310,1320,1328,1332,1339,1343,1367,1371,1388,1392,1411,1433,1437,1441,1461,1470,1501,1510,1514,1518,1530,1534],{"__ignoreMap":68},[73,1276,1277,1279,1281],{"class":75,"line":76},[73,1278,80],{"class":79},[73,1280,84],{"class":83},[73,1282,88],{"class":87},[73,1284,1285],{"class":75,"line":91},[73,1286,95],{"emptyLinePlaceholder":94},[73,1288,1289,1291,1293],{"class":75,"line":98},[73,1290,102],{"class":101},[73,1292,928],{"class":105},[73,1294,110],{"class":109},[73,1296,1297],{"class":75,"line":113},[73,1298,95],{"emptyLinePlaceholder":94},[73,1300,1301,1303,1305,1308],{"class":75,"line":118},[73,1302,121],{"class":101},[73,1304,124],{"class":109},[73,1306,1307],{"class":105},"OrderItem",[73,1309,110],{"class":109},[73,1311,1312,1314,1316,1318],{"class":75,"line":132},[73,1313,121],{"class":101},[73,1315,547],{"class":109},[73,1317,550],{"class":105},[73,1319,110],{"class":109},[73,1321,1322,1324,1326],{"class":75,"line":145},[73,1323,121],{"class":101},[73,1325,951],{"class":105},[73,1327,110],{"class":109},[73,1329,1330],{"class":75,"line":150},[73,1331,95],{"emptyLinePlaceholder":94},[73,1333,1334,1336],{"class":75,"line":159},[73,1335,153],{"class":101},[73,1337,1338],{"class":105}," PersistOrder\n",[73,1340,1341],{"class":75,"line":165},[73,1342,162],{"class":109},[73,1344,1345,1347,1349,1351,1353,1355,1357,1359,1361,1363,1365],{"class":75,"line":181},[73,1346,168],{"class":101},[73,1348,171],{"class":101},[73,1350,687],{"class":248},[73,1352,252],{"class":109},[73,1354,550],{"class":105},[73,1356,694],{"class":191},[73,1358,985],{"class":109},[73,1360,988],{"class":105},[73,1362,991],{"class":191},[73,1364,261],{"class":109},[73,1366,996],{"class":187},[73,1368,1369],{"class":75,"line":198},[73,1370,270],{"class":109},[73,1372,1373,1376,1378,1380,1382,1385],{"class":75,"line":211},[73,1374,1375],{"class":191},"        $context",[73,1377,280],{"class":109},[73,1379,321],{"class":191},[73,1381,280],{"class":109},[73,1383,1384],{"class":248},"save",[73,1386,1387],{"class":109},"();\n",[73,1389,1390],{"class":75,"line":224},[73,1391,95],{"emptyLinePlaceholder":94},[73,1393,1394,1396,1398,1400,1402,1404,1406,1409],{"class":75,"line":230},[73,1395,1375],{"class":191},[73,1397,280],{"class":109},[73,1399,321],{"class":191},[73,1401,280],{"class":109},[73,1403,1048],{"class":191},[73,1405,280],{"class":109},[73,1407,1408],{"class":248},"each",[73,1410,178],{"class":109},[73,1412,1413,1416,1418,1420,1422,1425,1427,1429,1431],{"class":75,"line":236},[73,1414,1415],{"class":101},"            fn",[73,1417,1037],{"class":109},[73,1419,1307],{"class":105},[73,1421,1104],{"class":191},[73,1423,1424],{"class":109},") => ",[73,1426,1054],{"class":191},[73,1428,280],{"class":109},[73,1430,1384],{"class":248},[73,1432,349],{"class":109},[73,1434,1435],{"class":75,"line":241},[73,1436,884],{"class":109},[73,1438,1439],{"class":75,"line":267},[73,1440,95],{"emptyLinePlaceholder":94},[73,1442,1443,1445,1447,1449,1451,1453,1456,1459],{"class":75,"line":273},[73,1444,1375],{"class":191},[73,1446,280],{"class":109},[73,1448,321],{"class":191},[73,1450,280],{"class":109},[73,1452,283],{"class":248},[73,1454,1455],{"class":109},"()->",[73,1457,1458],{"class":248},"createMany",[73,1460,178],{"class":109},[73,1462,1463,1465,1467],{"class":75,"line":299},[73,1464,1226],{"class":191},[73,1466,280],{"class":109},[73,1468,1469],{"class":191},"exceptions\n",[73,1471,1472,1474,1477,1479,1481,1483,1485,1487,1490,1493,1496,1498],{"class":75,"line":304},[73,1473,747],{"class":109},[73,1475,1476],{"class":248},"map",[73,1478,252],{"class":109},[73,1480,721],{"class":101},[73,1482,1037],{"class":109},[73,1484,255],{"class":187},[73,1486,258],{"class":191},[73,1488,1489],{"class":109},") => [",[73,1491,1492],{"class":365},"'message'",[73,1494,1495],{"class":109}," => ",[73,1497,293],{"class":191},[73,1499,1500],{"class":109},"])\n",[73,1502,1503,1505,1508],{"class":75,"line":727},[73,1504,747],{"class":109},[73,1506,1507],{"class":248},"all",[73,1509,349],{"class":109},[73,1511,1512],{"class":75,"line":744},[73,1513,884],{"class":109},[73,1515,1516],{"class":75,"line":759},[73,1517,95],{"emptyLinePlaceholder":94},[73,1519,1520,1522,1524,1526,1528],{"class":75,"line":769},[73,1521,708],{"class":101},[73,1523,991],{"class":191},[73,1525,252],{"class":109},[73,1527,403],{"class":191},[73,1529,296],{"class":109},[73,1531,1532],{"class":75,"line":781},[73,1533,233],{"class":109},[73,1535,1536],{"class":75,"line":793},[73,1537,307],{"class":109},[10,1539,1540,1541,1543,1544,1547,1548,1551],{},"Saving the parent order does not automatically save dirty ",[70,1542,1307],{}," models, so the pipe persists them explicitly. The example assumes the order has an ",[70,1545,1546],{},"exceptions()"," relationship; applications that treat exceptions as transient can instead return them to the caller and route the order into a review queue. Because the whole Pipeline is inside ",[70,1549,1550],{},"DB::transaction()",", a failure while saving any part rolls the entire order-processing change back.",[17,1553,1555],{"id":1554},"adding-a-new-rule-stays-local","Adding a new rule stays local",[10,1557,1558],{},"Suppose the company begins shipping dangerous goods. A new check can be introduced without reopening every existing pipe:",[63,1560,1562],{"className":65,"code":1561,"language":67,"meta":68,"style":68},"->through([\n    NormaliseOrderData::class,\n    ValidateDeliveryAddress::class,\n    ValidateDangerousGoodsDestination::class,\n    CheckCustomerTerms::class,\n    ReserveStock::class,\n    \u002F\u002F ...\n])\n",[70,1563,1564,1572,1583,1594,1605,1616,1627,1633],{"__ignoreMap":68},[73,1565,1566,1568,1570],{"class":75,"line":76},[73,1567,280],{"class":109},[73,1569,764],{"class":248},[73,1571,360],{"class":109},[73,1573,1574,1577,1579,1581],{"class":75,"line":91},[73,1575,1576],{"class":105},"    NormaliseOrderData",[73,1578,343],{"class":109},[73,1580,153],{"class":101},[73,1582,195],{"class":109},[73,1584,1585,1588,1590,1592],{"class":75,"line":98},[73,1586,1587],{"class":105},"    ValidateDeliveryAddress",[73,1589,343],{"class":109},[73,1591,153],{"class":101},[73,1593,195],{"class":109},[73,1595,1596,1599,1601,1603],{"class":75,"line":113},[73,1597,1598],{"class":105},"    ValidateDangerousGoodsDestination",[73,1600,343],{"class":109},[73,1602,153],{"class":101},[73,1604,195],{"class":109},[73,1606,1607,1610,1612,1614],{"class":75,"line":118},[73,1608,1609],{"class":105},"    CheckCustomerTerms",[73,1611,343],{"class":109},[73,1613,153],{"class":101},[73,1615,195],{"class":109},[73,1617,1618,1621,1623,1625],{"class":75,"line":132},[73,1619,1620],{"class":105},"    ReserveStock",[73,1622,343],{"class":109},[73,1624,153],{"class":101},[73,1626,195],{"class":109},[73,1628,1629],{"class":75,"line":145},[73,1630,1632],{"class":1631},"sW2Sy","    \u002F\u002F ...\n",[73,1634,1635],{"class":75,"line":150},[73,1636,1500],{"class":109},[10,1638,1639],{},"The new pipe has a clear position in the workflow, and its own tests can focus on that one rule.",[10,1641,1642],{},"This is where a Pipeline becomes more valuable than a long service method. The workflow grows by adding a well-named step rather than adding another branch to a central class that already knows too much.",[17,1644,1646],{"id":1645},"use-the-right-boundaries","Use the right boundaries",[10,1648,1649],{},"There are two details worth keeping clear.",[10,1651,1652,1653,1655],{},"First, a Pipeline does not make database changes safe by itself. The transaction in ",[70,1654,471],{}," is still important when stock, pricing, and order records must remain consistent.",[10,1657,1658],{},"Second, not every side effect belongs inside the transaction. Sending an email or calling a third-party shipping provider is often better handled after the order has been committed, using an event and queued listener.",[10,1660,1661,1662,1665],{},"In Laravel 13, merely making the listener queued does not guarantee that timing. The listener can implement ",[70,1663,1664],{},"ShouldQueueAfterCommit",":",[63,1667,1669],{"className":65,"code":1668,"language":67,"meta":68,"style":68},"use Illuminate\\Contracts\\Queue\\ShouldQueueAfterCommit;\n\nclass SendOrderConfirmation implements ShouldQueueAfterCommit\n{\n    \u002F\u002F Handle the OrderProcessed event...\n}\n",[70,1670,1671,1682,1686,1699,1703,1708],{"__ignoreMap":68},[73,1672,1673,1675,1678,1680],{"class":75,"line":76},[73,1674,121],{"class":101},[73,1676,1677],{"class":109}," Illuminate\\Contracts\\Queue\\",[73,1679,1664],{"class":105},[73,1681,110],{"class":109},[73,1683,1684],{"class":75,"line":91},[73,1685,95],{"emptyLinePlaceholder":94},[73,1687,1688,1690,1693,1696],{"class":75,"line":98},[73,1689,153],{"class":101},[73,1691,1692],{"class":105}," SendOrderConfirmation",[73,1694,1695],{"class":101}," implements",[73,1697,1698],{"class":105}," ShouldQueueAfterCommit\n",[73,1700,1701],{"class":75,"line":113},[73,1702,162],{"class":109},[73,1704,1705],{"class":75,"line":118},[73,1706,1707],{"class":1631},"    \u002F\u002F Handle the OrderProcessed event...\n",[73,1709,1710],{"class":75,"line":132},[73,1711,307],{"class":109},[10,1713,1714,1715,1718,1719,1722,1723,1726],{},"Alternatively, the queue connection can set ",[70,1716,1717],{},"after_commit"," to ",[70,1720,1721],{},"true",", or an individually dispatched job can use ",[70,1724,1725],{},"->afterCommit()",". These mechanisms make Laravel wait for the surrounding database transaction to commit; if it rolls back, the follow-up work is not dispatched.",[10,1728,1729],{},"The Pipeline coordinates the core decision-making process. Laravel's events, jobs, and queues can then handle the follow-up work in their own appropriate places.",[17,1731,1733],{"id":1732},"a-process-that-explains-itself","A process that explains itself",[10,1735,1736,1737,1739],{},"A good pipeline should read like a concise description of what happens to an order. If a new developer can look at the ",[70,1738,764],{}," list and understand the business flow, it is doing its job.",[10,1741,1742],{},"That clarity is the real benefit. The framework provides a small, familiar tool; the application uses it to make a complex process easier to reason about and safer to change.",[1744,1745,1746],"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 .sip94, html code.shiki .sip94{--shiki-default:#A626A4;--shiki-dark:#E5C07B}html pre.shiki code .sJa8x, html code.shiki .sJa8x{--shiki-default:#E45649;--shiki-dark:#E06C75}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 .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);}html pre.shiki code .sDhpE, html code.shiki .sDhpE{--shiki-default:#50A14F;--shiki-dark:#98C379}html pre.shiki code .sAGMh, html code.shiki .sAGMh{--shiki-default:#986801;--shiki-dark:#D19A66}html pre.shiki code .sW2Sy, html code.shiki .sW2Sy{--shiki-default:#A0A1A7;--shiki-default-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic}",{"title":68,"searchDepth":91,"depth":91,"links":1748},[1749,1750,1751,1752,1753,1754,1755],{"id":19,"depth":91,"text":20},{"id":57,"depth":91,"text":58},{"id":506,"depth":91,"text":507},{"id":900,"depth":91,"text":901},{"id":1554,"depth":91,"text":1555},{"id":1645,"depth":91,"text":1646},{"id":1732,"depth":91,"text":1733},"A complete Laravel Pipeline example for validating, pricing, reserving, and persisting an order without creating an oversized service class.",false,"md",{},"\u002Farticles\u002Fembrace-the-framework-order-processing-pipeline-that-can-grow","2026-08-06",{"title":5,"description":1756},"Build an extensible Laravel order processing Pipeline with focused validation, pricing, stock, and persistence stages.","embrace-the-framework","7","articles\u002Fembrace-the-framework-order-processing-pipeline-that-can-grow",[1768,67,1769,1770],"laravel","pipelines","ecommerce","2026-08-05","yE2HHiRwBbZPWfrwCkygBqEUB_ZYHSY9fM2qUCWOtJQ",[1774,1779,1784,1789,1794,1795,1800,1805,1810,1815,1819,1823],{"path":1775,"title":1776,"published":1777,"series":1764,"seriesOrder":1778},"\u002Farticles\u002Fembrace-the-framework-custom-collections-for-subscriptions","Embrace the Framework: Put Subscription Business Rules in a Custom Collection","2026-07-28","3",{"path":1780,"title":1781,"published":1782,"series":1764,"seriesOrder":1783},"\u002Farticles\u002Fembrace-the-framework-customer-eligibility-with-custom-collections","Embrace the Framework: Make Eligibility Rules Read Like Business Policy","2026-08-04","6",{"path":1785,"title":1786,"published":1787,"series":1764,"seriesOrder":1788},"\u002Farticles\u002Fembrace-the-framework-operational-reports-with-collections","Embrace the Framework: Build Clear Operational Reports With Collections","2026-08-02","5",{"path":1790,"title":1791,"published":1792,"series":1764,"seriesOrder":1793},"\u002Farticles\u002Fembrace-the-framework-order-fulfilment-with-collections","Embrace the Framework: Make Order Fulfilment Clearer With Collections","2026-07-25","2",{"path":1760,"title":5,"published":1761,"series":1764,"seriesOrder":1765},{"path":1796,"title":1797,"published":1798,"series":1764,"seriesOrder":1799},"\u002Farticles\u002Fembrace-the-framework-order-processing-with-pipelines","Embrace the Framework: Break Order Processing Into Steps With Pipelines","2026-07-31","4",{"path":1801,"title":1802,"published":1803,"series":1764,"seriesOrder":1804},"\u002Farticles\u002Fembrace-the-framework-supplier-imports-with-collections-and-pipelines","Embrace the Framework: Combine Collections and Pipelines for Reliable Supplier Imports","2026-08-08","8",{"path":1806,"title":1807,"published":1808,"series":1764,"seriesOrder":1809},"\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":1811,"title":1812,"published":1813,"series":1814,"seriesOrder":1814},"\u002Farticles\u002Fsimple-search-helper","A Simple Laravel Search Helper","2023-08-06",null,{"path":1816,"title":1817,"published":1818,"series":1814,"seriesOrder":1814},"\u002Farticles\u002Fsvg-tidy-local-svg-optimiser","SVG Tidy: Clean and Optimise SVGs Locally","2026-07-21",{"path":1820,"title":1821,"published":1822,"series":1814,"seriesOrder":1814},"\u002Farticles\u002Ftools-i-use","Tools I Use","2024-05-26",{"path":1824,"title":1825,"published":1826,"series":1814,"seriesOrder":1814},"\u002Farticles\u002Fwelcome","Welcome","2023-08-05",1785961325616]