Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Boxpacker Laravel Package

dvdoug/boxpacker

A PHP library for 3D bin packing: given a set of items and boxes, it works out which boxes to use and how to arrange the items inside them, minimising waste. Suited to e-commerce shipping, with support for multiple box sizes, weight and dimension limits, and item rotation.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the version

composer require dvdoug/boxpacker:^4.3

4.3.0 is a patch-level release on the 4.x line: no signature changes, no deprecations, no config changes. Upgrading from 4.2.x is a composer update with no code edits. If you are still on 3.x, treat it as a migration (the Packer API moved from setter-based setItemList()/setBoxList() to pack(ItemList, BoxList)), not as a patch bump.

The five objects you actually touch

  • Item — one thing being shipped: three dimensions plus weight.
  • Box — one carton: outer dimensions for display, inner dimensions for packing, plus max weight and empty weight.
  • ItemList / BoxList — typed collections; build these, don't pass arrays.
  • Packer — the entry point; returns a PackedBoxList.
  • PackedBox — one carton with getItems(), getWeight(), getUsedVolumePercentage(), getRemainingSpace().

First use case: "how many cartons for this order?"

use DVDoug\BoxPacker\Box;
use DVDoug\BoxPacker\BoxList;
use DVDoug\BoxPacker\Item;
use DVDoug\BoxPacker\ItemList;
use DVDoug\BoxPacker\Packer;

$boxes = new BoxList();
$boxes->insert(Box::create(
    reference: 'SMALL',
    outerWidth: 200, outerLength: 150, outerDepth: 100,
    emptyWeight: 120,
    innerWidth: 190, innerLength: 140, innerDepth: 90,
    maxWeight: 5000,
));

$items = new ItemList();
foreach ($order->lines as $line) {
    $items->insert(Item::create(
        description: $line->sku,
        width: (int) $line->width_mm,
        length: (int) $line->length_mm,
        depth: (int) $line->depth_mm,
        weight: (int) $line->weight_g,
    ));
}

$packed = (new Packer())->pack($items, $boxes);

$packed->count();                       // number of cartons
$packed->getWeight();                   // total shipped weight

Where to look first

  1. PackedBox::getRemainingSpace() — returns an ItemList of void-fill blocks. This is the fastest win for dunnage calculation and for showing "87% full" in an admin UI.
  2. NoBoxesAvailableException — the exception you must catch on day one. It is thrown when no box in your list fits, which is a business problem, not a bug.
  3. The Item/Box constructor argument order. Get this wrong and everything "works" while producing nonsense.

Units: pick millimetres and grams and never mix them. The packer has no unit awareness; a single order in centimetres silently produces a box 10x too small.

Implementation Patterns

Wrap it in one service, not in controllers

The packer is stateless and cheap to construct but expensive to run. Bind a single PackagingService in the container that owns box-list construction and exception handling, and inject it. Keep Packer itself out of your application layer so you can swap in a caching decorator later.

final class PackagingService
{
    public function __construct(private BoxRepository $boxes) {}

    public function pack(Order $order): PackedBoxList
    {
        try {
            return (new Packer())->pack(
                $this->itemList($order),
                $this->boxList(),
            );
        } catch (NoBoxesAvailableException $e) {
            throw new CannotPackageOrder($order, previous: $e);
        }
    }
}

Build the box list from the database, cache it

Boxes change rarely. Read them from a boxes table (so ops can edit dimensions without a deploy), map rows to Box objects, and cache the resulting BoxList under a key you bust on save. BoxList is serialisable, so Cache::remember('box-list', ..., fn () => $this->buildBoxList()) works.

Insert linked items consecutively

4.3.0 sorts linked items so they have the best chance of landing in the same carton. The practical implication for your code is that the packer now reorders your input around linked groups. If you group items that must ship together (a chassis and its PSU, a gift set), insert them adjacently in one pass and mark them as a linked group; do not interleave them with unrelated lines. If you were previously working around "linked items split across boxes" by manually chunking the ItemList into sub-orders and packing each separately, re-test that workaround — the sorter may make it unnecessary, and it was always costing you cartons.

Choose the strategy per order type, not globally

Three cheap knobs, worth exposing as config:

  • Single-box-first: pack once; if count() === 1, done. Only for the fast path.
  • Weight balancing: if you ship multi-carton orders and weight distribution matters (courier per-box limits, manual handling), let the packer redistribute. WeightRedistributor now sorts and no longer drops items, but it is still extra work — enable it only when count() > 1.
  • Keep-flat boxes: for liquids and fragile goods, restrict the box list to boxes flagged keep-flat and let the packer treat them differently. Build a separate cached BoxList per restriction rather than branching inside the pack call.

Persist the result, don't recompute

Packing is NP-hard and gets slow fast as count($items) grows. Store the chosen box references, per-box item assignments, weights, and getUsedVolumePercentage() on the shipment record at checkout/pick-time. Downstream (labels, manifests, rate quoting) should read the stored decision. Recomputing on every page load is the single most common performance mistake with this library.

Queue anything catalog-scale

For a real order (≤ ~50 lines), packing is milliseconds. For "what box fits this 500-SKU pallet" or "simulate 10,000 orders after a box-table change", push it to a job. If you must do it inline, cap the ItemList and fall back to a conservative large box above the cap rather than timing out a request.

Fallback ladder for NoBoxesAvailableException

Handle it as a decision tree, not a 500: (1) retry with the next box size up; (2) split the order into two shipments and pack each half; (3) mark the order for manual packing. Log the offending item dimensions — nine times out of ten it's one line item with a data-entry error (a 3000mm "depth").

Gotchas and Tips

Verify item counts after upgrading to 4.3.0. The release fixes a WeightRedistributor bug that could silently drop items — the pack call succeeded, returned a PackedBoxList, and some of your items simply were not in any box. There was no exception. Two consequences:

  • If you carry a reconciliation check ("sum of items across packed boxes == count of input items"), keep it. It just got cheaper to pass, but it is still your only guard against a future regression.
  • If you don't have such a check, add one now, and audit historical shipments from the 4.2.x era for missing lines. Because the failure was silent, downstream manifests and weight quotes were wrong without any error surface. This is the reason to take the upgrade promptly rather than batching it.

Assert completeness defensively

$inputCount = iterator_count(clone $items);
$packedCount = $packed->getItemsPacked(); // or sum count($box->getItems())
abort_if($inputCount !== $packedCount, 500, 'Packing lost items');

Cheap insurance; catches both regressions and duplicate-item bugs in your own ItemList construction.

Do not depend on output ordering. 4.3.0 added sorting of linked items. Any code that assumes box N contains item N — or that the first returned PackedBox holds your heaviest item — was already fragile and now may break. Iterate and match on your own identifier (Item::getDescription()), never on position.

Outer vs inner dimensions is not a detail. The packer packs to inner dimensions but reports weight and volume using outer. If you only populate outer and leave inner at zero, you get NoBoxesAvailableException for everything. If you populate inner identically to outer, you'll systematically overfill cartons and get real-world damage. Model the wall thickness once in your Box factory.

Empty weight counts. Box::emptyWeight is added to the shipment weight for every carton used. Forget it and your carrier rates are off by ~120g per box — negligible on one parcel, a real reconciliation problem across a thousand.

PackedBoxList is not a list of PackedBox in insertion order you can trust, and getWeight() is aggregate. For per-box display, iterate foreach ($packed as $packedBox) and read getBox()->getReference().

Performance levers, in order of payoff: (1) cache the BoxList; (2) shrink the box list to plausible candidates before packing (filter by max weight and by a bounding-box check against the largest item) — this is nearly free and often a 10x win; (3) restrict WeightRedistributor to multi-box results; (4) persist results. Avoid tuning packer internals.

Testing: packing is deterministic given identical inputs and ordering, so snapshot tests are viable — but they will churn on minor-version releases like this one (sorting changed in 4.3.0). Assert properties instead: every item is packed, no box exceeds maxWeight, no box exceeds inner volume, carton count is within an expected band. Those tests survive upgrades; exact-assignment snapshots do not.

Extension points: if your rules outgrow the model (e.g. "box A only for hazmat SKUs"), don't fork the packer — implement Box/Item interfaces with your own classes carrying the extra metadata, and pre-filter the BoxList per order. Subclassing BoxList to insert()-guard against misconfigured cartons is also a clean place to fail loudly in dev.

Debugging a surprising result: print the box's inner volume and total item volume first. Most "the packer is broken" reports are a single item whose dimensions were entered in cm.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings