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

Microplate Laravel Package

mll-lab/microplate

mll-lab/microplate is a PHP package for modeling microplates, wells, and well coordinates. It supports defining plate formats, validating positions, and organizing assay layouts for lab automation and sample-tracking workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

This package models laboratory microplates: coordinate systems (which wells exist on a plate format) and microplate sets (groupings of plates). Two families of classes carry the domain logic:

  • CoordinateSystem*Well — one class per supported plate format (e.g. a 96-well, 384-well variant). Responsible for translating human-readable well references ("B7", "A1") into indices/positions and back.
  • MicroplateSet* — one class per supported plate-set size, responsible for enumerating and addressing the individual plates within a set.

First steps:

  1. composer require the package, then check for a publishable config (php artisan vendor:publish) — plate formats are usually registered or extended there.
  2. Open src/ and locate the CoordinateSystem*Well and MicroplateSet* classes. These are the two entry points you will touch 90% of the time; everything else is supporting value objects.
  3. First use case to get working: take a spreadsheet row of well references, resolve each to a position, and confirm round-tripping (well → position → well) works for your plate format before building anything on top.

As of v6.3.0, every CoordinateSystem*Well exposes POSITIONS_COUNT and every MicroplateSet* exposes PLATE_COUNT. Use these instead of hardcoding 96, 384, or 6 anywhere in your application code.

Implementation Patterns

Resolve, don't compute. Rather than deriving a well index from a row letter and column number yourself, let the coordinate system do it. Your application code should be about which wells to touch, not how a well maps to an index.

$system = new CoordinateSystem96Well();
$position = $system->positionOf('B7');   // per your version's API

Treat the constants as the contract. The new POSITIONS_COUNT / PLATE_COUNT constants are the cleanest way to express intent and to stay safe if you later swap plate formats:

$system = new CoordinateSystem96Well();
$all = range(0, CoordinateSystem96Well::POSITIONS_COUNT - 1); // full-plate sweep

$set = new MicroplateSet6();
foreach (range(0, MicroplateSet6::PLATE_COUNT - 1) as $plateIndex) {
    // dispatch per-plate work
}

They are especially useful in three places:

  • Pre-sizing and bulk fill. Allocating arrays keyed by position, or building a zero-filled grid for a plate heatmap/export.
  • Validation rules. Rule::in(range(0, CoordinateSystem96Well::POSITIONS_COUNT - 1)) beats a magic number scattered across form requests.
  • Test assertions. assertCount(MicroplateSet6::PLATE_COUNT, $plates) breaks loudly if the package changes its definition of a set, which is exactly what you want.

Keep plate-format selection in config, not in if statements. Register the formats you support in config, resolve the right class by key, and pass the class string (or a small factory) through your services. Pipelines, jobs, and imports then never mention a concrete class name, and adding a format later is a config change rather than a refactor.

Iterate set × position as a single flat plan. For bulk operations (imports, plate-reader ingestion, robot worklists) build one flat list of (plateIndex, position) tuples up front, then chunk it into queue jobs. This decouples "how many things are there" (constants) from "how much work per job" (a tunable you own).

Persist stable identifiers, not objects. Store the plate index and well position (or the canonical well string) in your schema; hydrate the coordinate system on read. Serialising the value objects into JSON columns makes schema migrations painful the moment a class name or shape changes.

Validation at the boundary. Well references arriving from CSVs or user input should be normalised and validated (" b7 " → "B7") in a form request or an import DTO before they ever reach the domain objects. Keep a Rule object per plate format so controllers and imports share it.

Gotchas and Tips

  • The constants are per-class, not polymorphic. POSITIONS_COUNT and PLATE_COUNT are declared on the concrete classes, so you cannot reach them through a shared interface/abstract type in a generic, format-agnostic service. If you need the count for "whatever format this is", either branch on the resolved class, wrap the object in a small adapter that exposes count(), or pass the count alongside the class from your config registry. Don't assume a static call on a class-string works until you've checked the hierarchy.
  • Counts are counts, not max indices. POSITIONS_COUNT - 1 is your highest valid zero-based position. Off-by-one here shows up as a mystery OutOfRangeException (or a silently dropped last well) only on full-plate runs — test a full-plate sweep, not just a handful of wells.
  • These constants changed in v6.3.0 — check your magic numbers. If you already hardcode 96/384/6 somewhere, this release is the prompt to replace them. It also means the package now publicly commits to those counts, so a future format addition is more likely to appear as a new class than to mutate an existing constant.
  • Static constants and config caching. Class constants are compile-time, so php artisan config:cache is unaffected — but if you're building an array of supported formats for a Rule::in or a select dropdown, make sure that array is built from the same source of truth as the runtime service, or the UI will offer formats the resolver rejects.
  • Debugging tips. When a well resolution looks wrong, dump the round trip (ref → position → ref) rather than the internals; orientation errors (row-major vs column-major, and the "A1 top-left vs bottom-left" physical convention) account for most surprises, and they're only obvious on asymmetric test data. Use B7 or H12, never A1, in fixtures.
  • Extension points. Subclassing a CoordinateSystem*Well to add a custom format is usually possible but easy to get wrong — if the base class derives positions from a declared row/column count, override those declarations rather than the lookup methods, and re-run the full-plate round-trip test over POSITIONS_COUNT positions to prove the subclass is internally consistent.
  • Test doubles. Because the counts are static, you can't easily mock "a plate with fewer wells". If you need that for edge-case tests, target a genuine small format (a 6- or 24-well class) instead of inventing a fake; it'll catch real assumptions in your code.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource