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

Laravel Stock Laravel Package

appstract/laravel-stock

appstract/laravel-stock is a lightweight stock management package for Laravel. Add inventory tracking to Eloquent models with a simple trait, record stock mutations, and increase, decrease, or check available quantities. Ideal for simple product inventories and small shops.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up (5 minutes). The package ships a service provider and a single migration for a stock_mutations ledger table.

composer require appstract/laravel-stock

php artisan vendor:publish --provider="Appstract\Stock\StockServiceProvider" --tag=migrations
php artisan migrate

Add the trait to any model that needs inventory:

namespace App\Models;

use Appstract\Stock\HasStock;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasStock;
}

Where to look first. Read these three files before writing any code — the package is small and the whole contract lives here:

File Why
src/HasStock.php The entire public API (addStock, subtractStock, setStock, getStock, mutation relation). This is the source of truth for your installed version.
src/StockMutation.php The ledger row: stockable_id, stockable_type, quantity, reason, timestamps.
database/migrations/*_create_stock_mutations_table.php Column types and indexes — check whether reason is nullable and whether the polymorphic index exists.

First use case. Treat stock as a running balance: receiving goods adds, fulfilling an order subtracts, and the current level is read back from the model.

$product->addStock(25, 'initial delivery');
$product->subtractStock(2, 'order #1042');

$product->stock;      // 23  (accessor on the trait)
$product->getStock(); // 23  (explicit method)

A realistic controller flow:

public function ship(Order $order)
{
    DB::transaction(function () use ($order) {
        $product = Product::lockForUpdate()->findOrFail($order->product_id);

        if ($product->stock < $order->quantity) {
            throw ValidationException::withMessages([
                'stock' => 'Not enough stock to fulfil this order.',
            ]);
        }

        $product->subtractStock($order->quantity, "order:{$order->id}");
        $order->update(['shipped_at' => now()]);
    });
}

Implementation Patterns

1. Treat the table as an append-only ledger, not a counter. Never update a mutation row or delete one to "fix" stock — add a compensating mutation. This gives you a free audit trail and makes reconciliation trivial:

$product->mutation(+10, 'stock count correction'); // if exposed in your version
// or: addStock/subtractStock with a reason string

2. Standardise reason strings. The column is free text, so invent a convention early (purchase:{po_id}, order:{order_id}, refund:{order_id}, count:{date}) and put it behind constants or an enum. Reporting later depends entirely on this discipline.

3. Wrap inventory writes in a domain action. Don't call subtractStock from controllers and jobs and listeners alike. One class, one place to add locking, validation, and events:

class AdjustStock
{
    public function __invoke(Product $product, int $delta, string $reason): void
    {
        $delta >= 0
            ? $product->addStock($delta, $reason)
            : $product->subtractStock(abs($delta), $reason);
    }
}

4. Expose stock in API responses deliberately. The trait's accessor is not in $appends by default. Add it, or map it explicitly in a resource:

protected $appends = ['stock'];
// or, in ProductResource: 'stock' => $this->getStock(),

5. Eager-load history only when you need it. $product->stockMutations is a normal relation, so the usual N+1 rules apply. For a product list, prefer an aggregated query over loading mutations:

Product::withSum('stockMutations as stock_total', 'quantity')->get();

6. Emit your own events. The package is deliberately thin. Fire StockDepleted / LowStockReached from your AdjustStock action and listen for it to send alerts. Don't try to hook the package's internals.

7. Testing. Use RefreshDatabase; the migration is a normal package migration. Assert against the ledger rather than only the accessor — it catches double-writes:

$product->addStock(5, 'test');
$this->assertDatabaseHas('stock_mutations', [
    'stockable_id'   => $product->id,
    'stockable_type' => Product::class,
    'quantity'       => 5,
]);
$this->assertSame(5, $product->getStock());

8. Refund / restock pattern. Mirror the original mutation with a positive entry referencing the same order id. This keeps SUM(quantity) correct without any special-case code.


Gotchas and Tips

No atomic decrement — race conditions are real. The package records mutations; it does not lock the parent row or enforce non-negative stock. Two concurrent checkouts can both read stock = 1 and both succeed. Always lockForUpdate() on the product inside a transaction, as shown above, and validate the balance yourself. If you skip this, you will ship oversold inventory.

Negative stock is allowed. subtractStock(10) on a product with 3 in stock happily writes -7. There is no guard, no exception, no validation rule. Add the check in your action class.

stock is computed, so you cannot orderBy('stock'). It isn't a column. Use withSum('stockMutations', 'quantity') or a subquery/join on the ledger, and alias the result. where('stock', '>', 0) will error or silently misbehave.

Performance degrades as the ledger grows. Every getStock() call is an aggregate over that model's rows. Fine for hundreds; painful for millions. Mitigations: an index on (stockable_type, stockable_id) (verify the published migration has one), a cached stock column updated in the same transaction, or periodic snapshots plus a since cutoff.

Maintenance risk. Last release was May 2021. Check it against your Laravel and PHP versions before committing — Laravel 10/11 and PHP 8.2+ may need a fork or a replace entry in composer.json. The package is small enough (~4 files) that vendoring or copying the trait into app/Support is a legitimate option and removes the upgrade risk entirely.

Orphaned mutations. The polymorphic relation has no cascade. Deleting a product leaves its stock_mutations rows behind and skews global reports. Clean up in a deleting observer, or add ->cascadeOnDelete() to a custom migration.

Writes outside the trait are invisible. A raw DB::table('products')->update(['stock' => ...]) or a seeder that sets a column bypasses the ledger and desynchronises the audit trail. Enforce the action class as the only writer, and grep for stock_mutations before shipping.

Quantity types. The column is an integer in the published migration. If you sell by weight or volume, you cannot use this as-is without altering the column to a decimal and auditing every comparison in your own code.

Debugging tips. When a balance looks wrong, dump the ledger ordered by created_at — it almost always shows a duplicate mutation from a retried job. Wrap your AdjustStock calls in idempotency (e.g. skip if a mutation with the same reason already exists for that model) if jobs can be retried.

Extension points. You can swap the mutation model by binding your own subclass in a service provider, override any trait method in your model, and hook Eloquent model events on StockMutation for cross-cutting concerns like broadcasting or audit logging. There is no config file — everything is convention-driven, which is pleasant until you need to change the table name.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
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