baks-dev/users-profile-balance
baks-dev/users-profile-balance provides user profile balance management for PHP/Symfony apps. Install via Composer, run baks:assets:install, apply Doctrine migrations, and test with the users-profile-balance PHPUnit group. Requires PHP 8.4+.
Reality check before you start: despite the ecosystem framing, this is a Symfony/Doctrine bundle, not a Laravel package. The README's only commands are bin/console baks:assets:install, baks:cache:clear, and doctrine:migrations:*, and the composer scripts use the symfony-cmd handler. There is no service provider, no facade, no artisan command, and no Eloquent model documented. Plan for an integration layer rather than a drop-in.
Prerequisites
laravel-doctrine/orm or a thin DBAL-only setup) or treat the package as a schema + reference implementation and write your own Eloquent repository over its tables.Install
composer require baks-dev/users-profile-balance
Then install config/assets. In a Symfony app this is:
php bin/console baks:assets:install
In Laravel there is no bin/console by default. Either add symfony/console and a minimal kernel, or (recommended for a first spike) inspect what baks:assets:install actually writes — typically config files and Doctrine mapping/asset resources — and copy those into config/ and database/ by hand.
Schema
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
In Laravel, use php artisan migrate for your own tables and run the Doctrine migration through a one-off console command, or hand-port the generated diff into an Eloquent/Blueprint migration. Always --dry-run the diff first — doctrine:migrations:diff will happily propose dropping or altering columns it doesn't recognise in tables you already own.
Where to look first, in this order
vendor/baks-dev/users-profile-balance/composer.json — the real dependency list (Doctrine version, Symfony components, whether it pulls in other baks-dev/* packages).vendor/baks-dev/users-profile-balance/src/ — entity/DTO definitions, the service that mutates balances, and the interfaces you can bind in a Laravel provider. This is your actual API documentation; the README has none.vendor/baks-dev/users-profile-balance/src/Resources/config/ (or equivalent) — service definitions, which reveal constructor dependencies and event dispatcher usage.profile_id/user_id.First use case: read a profile's balance and render it in a controller. Do only that before writing any credit/debit logic — it validates the DB connection, the mapping, and the container wiring in about an hour.
Bind the package's service, don't call statics. The assessment docs circulating internally show Balance::decrement($user->id, 10) — treat that as illustrative, not real. Find the concrete class in src/, and register it in a BalanceServiceProvider:
$this->app->bind(BalanceInterface::class, fn ($app) =>
new BalanceService(/* pull deps from the package's service definitions */)
);
Binding an interface rather than a concrete class is what lets you swap in a test double and what survives a package upgrade.
Wrap every mutation in a transaction with a row lock. Balance tables are the classic lost-update target. Whatever the package does internally, guard it at your boundary:
DB::transaction(function () use ($profileId, $amount) {
$row = DB::table('profile_balance')
->where('profile_id', $profileId)
->lockForUpdate()
->first();
// delegate the arithmetic/validation to the package service
});
If the package offers no locking of its own (nothing in the docs suggests it does), this wrapper is not optional — it is the difference between a working credit system and a support ticket generator.
Adapter over the package's persistence. Don't let Doctrine entities leak into your controllers and jobs. Write a BalanceRepository that speaks your domain language (credit, debit, balanceFor) and internally talks to the package. When the package's undocumented internals inevitably change in a 7.x bump, you edit one class.
Event-driven side effects. The Symfony-flavoured design almost certainly dispatches through symfony/event-dispatcher. Bridge it once:
// In a service provider or a small subscriber registered on the package's dispatcher
Event::listen(BalanceChanged::class, function ($event) {
// push to analytics, notify the user, invalidate a cached "unlocked features" flag
});
Keep listeners idempotent and non-throwing — a failing mailer should never roll back a balance mutation.
Gate features on balance, not on UI. Put the check in a policy or middleware so the same rule protects API routes, queued jobs, and console commands:
// app/Policies/FeaturePolicy.php
public function exportReports(User $user): bool
{
return $this->balances->balanceFor($user->profile_id) >= self::EXPORT_COST;
}
Then charge in the controller after the policy passes, inside the same transaction where possible, or accept a small double-spend window knowingly.
Composer scripts — merge, don't overwrite. The README's snippet replaces post-install-cmd/post-update-cmd wholesale. Laravel already uses those hooks for @php artisan package:discover. Merge them:
"scripts": {
"post-install-cmd": ["@php artisan package:discover --ansi", "@auto-scripts"],
"post-update-cmd": ["@php artisan vendor:publish --tag=laravel-assets --ansi --force", "@auto-scripts"],
"auto-scripts": { "baks:assets:install": "symfony-cmd" }
}
Be aware symfony-cmd requires symfony/flex; without Flex, composer install will error on an unknown handler. Simplest fix: drop the auto-scripts block and run asset install manually as part of your deploy script.
Testing. The README exposes php bin/phpunit --group=users-profile-balance. In a Laravel context, run the vendor suite once manually against your DB to see what actually passes, then write your own feature tests around the adapter: concurrent debits, debit-to-zero, debit-below-zero rejection, and a replayed event.
php -v on every environment including CI, staging, and the queue worker image before composer require. A mismatch surfaces at deploy time as an unhelpful platform-check failure."baks-dev/users-profile-balance": "7.4.6"), commit composer.lock, and consider vendoring or forking with a dev- patch branch. Release notes for 7.4.3/7.4.4/7.4.6 are literally just the version number — assume nothing about backwards compatibility between patch releases.doctrine:migrations:diff is destructive by default. It reads your whole schema. Scope it with an explicit --filter or an em configuration limited to the package's mapping, and diff into a throwaway database first.baks:assets:install output is not tracked by vendor:publish, so a fresh clone or a CI container won't have it. Either commit the generated files or call the command from your deploy script.SELECT ... FOR UPDATE. Prove it by running two concurrent debits against a test database. If they both succeed when only one should, your wrapper transaction is the only defence.(tenant_id, profile_id) rather than relying on whatever the package generates.storage/logs/var/log for Doctrine query errors (Doctrine's exception messages are far more informative than Eloquent's), and confirm the entity is actually flushed — Doctrine does not persist until flush(), which is the single most common "my balance didn't save" cause for developers coming from Eloquent's save().vendor/ will be silently lost on the next composer update, and with release notes this thin you will not be told what changed.App\Billing\Balance) and one service provider. If the maintainer abandons it — a real risk at this adoption level — replacing the implementation should touch two files, not twenty.How can I help you explore Laravel packages today?