lamoda/lamoda-b2b-platform.dto
A collection of data transfer objects for the Lamoda B2B platform, offering typed, structured value objects for orders, products, and API payloads. Keeps business data consistent and validated across services with a lightweight, framework-agnostic PHP implementation.
Install and boot
composer require <vendor>/<package>
php artisan vendor:publish --provider="<Vendor>\<Package>\<Package>ServiceProvider"
php artisan migrate
Publish tags individually if you only need part of the surface — --tag=config, --tag=migrations, --tag=views. Publishing everything on a first pass dumps migrations you may not want to own.
Where to look first
config/<package>.php — the connective tissue. Read every key before writing code; most "why isn't this working" tickets resolve here.register()/boot() — shows what is bound in the container, which interfaces you can swap, and which events are listened to.src/Models/ — the package ships Eloquent models (InboundShipment and friends). Treat these as the primary API; the repository/service classes are usually thin wrappers over them.database/migrations/ — the schema is the real documentation for relationships and nullability.First use case
Create an inbound shipment with its document number, then read it back:
use <Vendor>\<Package>\Models\InboundShipment;
$shipment = InboundShipment::create([
'reference' => 'INB-2024-0001',
'document_number' => 'DOC-99812', // added in 1.7.0
'warehouse_id' => $warehouse->id,
]);
$shipment->document_number; // 'DOC-99812'
If that round-trips, the install is sound and you can move on to the shipping/line-item workflows.
Resolve through the container, not new
Bind-dependent classes should be injected or resolved via the facade so config and test overrides apply:
public function __construct(private InboundShipmentService $shipments) {}
Type-hint the package's interface, not the concrete class, if one exists — that is the seam the package expects you to use for swapping behaviour.
Wrap multi-model writes in transactions
Inbound shipments almost always touch line items, stock movements and status history. The package does not open a transaction for you:
DB::transaction(function () use ($payload) {
$shipment = InboundShipment::create($payload);
$shipment->lines()->createMany($payload['lines']);
});
Set the document number at creation time, not after
Adding a field in a minor release usually means a nullable column. Prefer populating it in the same create()/update() call as the rest of the payload so validation, events, and any observers see the final state exactly once. Avoid a create-then-patch pattern — it fires model events twice and can trip uniqueness checks against a half-built record.
Validate upstream with Form Requests
The package validates at the model/database layer only (types and constraints). Put business rules — document number format, uniqueness scoped to a supplier or fiscal year — in a FormRequest or a dedicated Rule object so failures surface as 422s rather than QueryExceptions.
Lean on the event system for side effects
Anything downstream — notifying a warehouse, pushing to an ERP, generating PDFs — belongs in a listener on the package's shipment events, not inline in your controller. This keeps the package upgradeable and lets you queue the work:
Event::listen(ShipmentConfirmed::class, SyncToErp::class);
Implement ShouldQueue on the listener and make it idempotent — events can be retried.
Testing
Use the shipped factories with RefreshDatabase, and Http::fake()/Queue::fake() for outbound calls. Assert on the model state (assertDatabaseHas) rather than on internal service method calls, otherwise a patch release that refactors internals breaks your suite for no reason.
Feature-flag the new field if you roll out gradually
Because 1.7.0 is additive, you can deploy the schema and start populating document_number for new shipments while backfilling historical rows in a queued job. Gate reads with $shipment->document_number ?? $shipment->reference during the transition.
Nullable does not mean optional
The new document_number column will be nullable for backwards compatibility, so nothing fails if you forget it. If your business requires it, enforce that in validation — the database will happily accept null.
Existing rows and backfill
Migrations run but existing shipments get null. Write a chunked backfill (->chunkById(500, ...)) rather than a single UPDATE, and keep it idempotent so a re-run is harmless. Do not put data transformation in the migration itself.
Uniqueness needs an index decision
If document numbers must be unique, add a unique index in a separate migration after the backfill completes. Adding it in the same migration as the column will fail on any environment with duplicate or null rows depending on your DB driver's null semantics (MySQL and Postgres both allow multiple NULLs).
Mass assignment
Check $fillable/$guarded on the package model. New columns are sometimes not added to $fillable in the same release. If create(['document_number' => ...]) silently drops the value, that is the cause — set it explicitly or extend the model.
Config caching
php artisan config:cache in deploy scripts will freeze the package config. If behaviour differs between local and production, clear and re-cache before you start debugging application code.
N+1 on shipment listings
Shipment index screens are the classic N+1 trap. Eager load relationships explicitly:
InboundShipment::with(['lines', 'warehouse'])->paginate(50);
Enable Model::preventLazyLoading() outside production to catch these early.
Events inside transactions
Listeners registered with ShouldHandleEventsAfterCommit (or dispatched after commit) avoid notifying downstream systems about shipments that then roll back. Verify the package's events are dispatched at the right point before wiring up external integrations.
Extension points worth knowing
AppServiceProvider rather than editing package code.Debugging
DB::listen() plus Telescope will show you the exact queries the package issues; almost every performance complaint is visible there. Wrap suspicious calls in DB::enableQueryLog() when Telescope isn't available. For state machines and status transitions, log every transition in an observer — silent no-op updates are the most common "the package ignored my change" report.
How can I help you explore Laravel packages today?