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

Wallet Bundle Laravel Package

mkurc1/wallet-bundle

A Symfony bundle for integrating virtual wallets into your application. It provides balance tracking, credit and debit operations, and transaction history through reusable entities and services, making it easy to add account funding, ledger, and payment-wallet features.

View on GitHub
Deep Wiki
Context7

Getting Started

The 30-second reality check. mkurc1/wallet-bundle is a Symfony bundle, not a Laravel package. That means:

  • No ServiceProvider, no facade, no config/wallet.php, no publishable migrations.
  • It extends Symfony\Component\HttpKernel\Bundle\Bundle, ships a DependencyInjection\*Extension, and almost certainly maps a Doctrine entity onto your User.
  • composer require mkurc1/wallet-bundle in a Laravel app will install Symfony HTTP kernel/DI classes into vendor/ and then never boot anything. There is no auto-discovery path that connects it to Laravel's container.

So for a Laravel developer the realistic first step is: treat the repo as reference source, not a dependency. Clone it, read it, port the schema and the invariants.

Where to look first, in this order:

  1. composer.json — PHP and Symfony version constraints, and the autoload PSR-4 prefix. If it targets EOL PHP/Symfony, stop reading and build fresh; there is nothing to port that you can't design yourself in an hour.
  2. src/Entity/ (or src/Model/) — this is the most important file in the repo. It answers the only question that matters: is a wallet a single mutable balance column, or a set of wallet_transactions ledger rows? A balance column alone is toy-grade; a ledger is worth copying.
  3. src/Resources/config/services.* and src/DependencyInjection/Configuration.php — reveals whether balance mutation lives in a service (WalletManager, WalletService) or is done by the entity itself, and what the config tree exposes (currency, precision, per-user vs. multi-wallet).
  4. src/Resources/config/doctrine/*.orm.xml or entity annotations — the actual columns, indexes, and any @Version / optimistic-lock column.
  5. README.md — usually one paragraph of config/bundles.php registration. That's the "entire documentation."

First use case you would actually ship — a points/credits wallet on App\Models\User, for demos, internal tools, or an MVP. Not real money. Concretely, three things ported:

  • a wallets table (integer minor units, unique per user),
  • a HasWallet trait on User,
  • a WalletService that does the debit/credit inside a transaction with a row lock.

That's the same three moving parts the bundle gives a Symfony app; the only difference is DI container vs. app(...) resolution.

Implementation Patterns

Pattern 1 — Read-then-port, not adopt. The productive workflow is a 1–2 day spike: read src/Entity/ and the service, answer "ledger or balance column?", "is there locking?", "integer or float?", then write an Eloquent-native version. Keep the MIT header if you copy code.

Pattern 2 — Ledger-first schema. Port the ledger shape if the bundle has one; if it doesn't, still build one, because a bare balance column forces you to invent history later.

Schema::create('wallets', function (Blueprint $t) {
    $t->id();
    $t->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
    $t->char('currency', 3)->default('USD');
    $t->bigInteger('balance')->default(0);          // minor units
    $t->unsignedBigInteger('version')->default(0);  // optimistic lock
    $t->timestamps();
});

Schema::create('wallet_entries', function (Blueprint $t) {
    $t->id();
    $t->foreignId('wallet_id')->constrained()->cascadeOnDelete();
    $t->bigInteger('amount');                       // signed, minor units
    $t->string('type');                             // credit | debit | refund
    $t->string('idempotency_key')->nullable()->unique();
    $t->json('meta')->nullable();
    $t->timestamps();
    $t->index(['wallet_id', 'created_at']);
});

Pattern 3 — Trait on the user, mutations through the service. Mirror whatever trait/interface the bundle attaches to its User entity (usually HasWalletInterface + WalletTrait), but keep writes out of the model:

trait HasWallet
{
    public function wallet(): MorphOne
    {
        return $this->morphOne(Wallet::class, 'walletable');
    }

    public function credit(int $amount, array $meta = []): WalletEntry
    {
        return app(WalletService::class)->credit($this, $amount, $meta);
    }

    public function debit(int $amount, array $meta = []): WalletEntry
    {
        return app(WalletService::class)->debit($this, $amount, $meta);
    }
}

Pattern 4 — Atomic service, every time. Whatever the bundle does, reproduce the atomicity; the PHP-side $wallet->balance -= $x; $em->flush(); pattern (common in small bundles) is the bug you inherit.

final class WalletService
{
    public function debit(Model $owner, int $amount, array $meta = []): WalletEntry
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException('Amount must be positive.');
        }

        return DB::transaction(function () use ($owner, $amount, $meta) {
            $wallet = Wallet::where('user_id', $owner->id)->lockForUpdate()->firstOrFail();

            if ($wallet->balance < $amount) {
                throw new InsufficientFundsException();
            }

            $wallet->decrement('balance', $amount);
            $wallet->increment('version');

            return $wallet->entries()->create([
                'amount'          => -$amount,
                'type'            => 'debit',
                'idempotency_key' => $meta['idempotency_key'] ?? null,
                'meta'            => $meta,
            ]);
        });
    }
}

Pattern 5 — Events as the extension point. Symfony would give you the event dispatcher; the bundle may dispatch wallet events. In Laravel, fire your own from the service so listeners handle receipts, notifications, and analytics without touching the wallet code:

DB::afterCommit(fn () => WalletDebited::dispatch($wallet, $entry));

Pattern 6 — Using it in a Symfony project (day job scenario). If you actually are in a Symfony app, the workflow is: config/bundles.php → App\WalletBundle::class => ['all' => true], import the config tree into config/packages/wallet.yaml, add the wallet trait/interface to your User entity, bin/console doctrine:schema:update --force, then inject the manager service (__construct(WalletManager $wallets)) and call credit()/debit(). The container wiring is the entire value-add; the domain logic is small enough to read in one sitting.

Pattern 7 — Test the invariants, not the bundle. Port these tests, because they're the ones that catch real failures: concurrent debits (two processes, one wallet, assert no negative balance), idempotency (same key twice → one entry), and a reconciliation job that asserts wallets.balance === SUM(wallet_entries.amount) per wallet.

Gotchas and Tips

  • composer require is a trap. The package installs cleanly and then does nothing in Laravel. Don't burn time on class_alias, container bridges, or the Doctrine bridge — read the source instead. A Doctrine + Eloquent dual-ORM app is an operational debt you don't want for a 7-star bundle.
  • Verify money representation before you copy anything. If the entity uses float or decimal(10,2), do not replicate it — use bigInteger minor units (or bcmath if you must do decimal math). Rounding drift is the bug that appears six months later and is the hardest to unwind.
  • Look for a version / @Version column. If it's present, the author thought about concurrency — reproduce the optimistic-lock semantics alongside lockForUpdate(). If absent, the bundle offers no protection against double-spend and you must add it yourself.
  • Balance column vs. ledger is the fork in the road. A mutable balance with no transaction table means no audit trail, no refunds, no reconciliation. For anything beyond a demo, port the ledger idea even if the bundle doesn't have it.
  • Config quirks in the Symfony version. Bundle config is under a single root key (usually wallet:), consumed in Configuration.php. If you must inspect a running Symfony instance, bin/console config:dump-reference wallet and bin/console debug:container wallet are faster than reading the extension class. In Laravel there is no equivalent — you'd be writing config/wallet.php yourself.
  • The user relation is Doctrine-specific. The bundle likely assumes a User entity with a OneToOne/OneToMany wallet mapping and a join column against your users table. If your Laravel users table has a UUID or non-id primary key, port the FK type deliberately (foreignUuid vs foreignId) — schema drift here is silent until a write fails.
  • No idempotency key in the bundle is the norm. Retries, double-clicks, and webhook replays will double-credit. Add a unique idempotency_key column from day one; retrofitting it after you have duplicate rows is painful.
  • Adoption and bus-factor risk. 7 stars, 0 dependents, single maintainer, no vendor, no SLA. If you vendor code, keep the MIT attribution and assume permanent ownership — there will be no upstream to pull fixes from. Budget for a domain-aware reviewer (finance or payments) before any real funds move.
  • Debug checklist if it "doesn't work" in Symfony: bundle registered in config/bundles.php? config key present? entity mapping loaded (doctrine:mapping:info)? migrations run? If any of those is missing the service resolves but writes fail silently at flush time. In Laravel, none of this applies — the package simply isn't running, no matter what the container says.
  • Scale expectation. A single balance row per user is a hot row under concurrent debits. Plan for short transactions, lockForUpdate(), and eventually an event-sourced ledger with an async balance projection. Nothing in a bundle this small signals those concerns are addressed — assume you design them.
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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib