Linked items

In some scenarios you may have two or more items that must always be packed into the same box — for example, a product and its mandatory accessory, or a set of items that are sold together and must ship as a unit.

To express this constraint, implement the BoxPacker\LinkedItem interface on your item class. The interface extends BoxPacker\Item and adds a single method:

public function getLinkedItemGroup(): string;

Any items that return the same non-empty string from this method are treated as a linked group: the packer guarantees they will all end up in the same PackedBox.

<?php
    use DVDoug\BoxPacker\LinkedItem;
    use DVDoug\BoxPacker\Rotation;

    class MyLinkedItem implements LinkedItem
    {
        public function __construct(
            private readonly string $description,
            private readonly int $width,
            private readonly int $length,
            private readonly int $depth,
            private readonly int $weight,
            private readonly Rotation $allowedRotation,
            private readonly string $linkedItemGroup,
        ) {}

        // ... standard Item methods ...

        public function getLinkedItemGroup(): string
        {
            return $this->linkedItemGroup;
        }
    }

    $packer = new Packer();
    $packer->addBox(new MyBox(...));

    // item1 and item2 share the same group identifier and will always be packed together
    $packer->addItem(new MyLinkedItem('Product', ..., linkedItemGroup: 'order-line-42'));
    $packer->addItem(new MyLinkedItem('Accessory', ..., linkedItemGroup: 'order-line-42'));

    $packedBoxes = $packer->pack();

Quantities

If you call addItem($linkedItem, qty: N) for a LinkedItem, all N inserted instances share the same group identifier and are therefore all considered part of the same linked group. They will all be packed into the same box.

$packer->addItem(new MyLinkedItem('Part', ..., linkedItemGroup: 'kit-99'), 4);
// All 4 instances are part of group 'kit-99' and will always ship together

Non-adjacency

The linked-group guarantee is about box assignment only. Items in a linked group will be in the same PackedBox, but the packer does not guarantee that they are physically adjacent or contiguous inside that box. If you additionally need adjacency, implement Custom constraints alongside LinkedItem.

Behaviour when a linked group cannot fit

If no available box can hold all members of a linked group simultaneously, the packer treats the whole group as unpackable — the same way it handles any individual item that is too large to fit. The behaviour is controlled by throwOnUnpackableItem():

  • throwOnUnpackableItem(true) (the default) — throws NoBoxesAvailableException.

  • throwOnUnpackableItem(false) — the group is skipped; all remaining packable items (including other linked groups that can fit) are still packed.

Weight redistribution

Linked groups are preserved during the automatic Weight distribution pass. The redistributor skips all linked group members entirely — they are never moved between boxes during weight balancing. This ensures group integrity is always maintained at the cost of potentially less optimal weight distribution.

Invalid group identifiers

An empty string ('') is not a valid group identifier and will cause an \InvalidArgumentException when the item is added to the packer. Use a non-empty string such as a UUID, order-line ID, or SKU combination.

Performance note

As with Custom constraints, only implement LinkedItem on item classes that genuinely need this feature. The packer has to perform additional group-completeness checks for every candidate box when linked items are present, which adds a small overhead compared to packing ordinary items.