---
title: "DI and actions — Credits for Laravel | Roundly"
description: "Inject CreditsManager instead of the facade, or call the single-purpose actions from jobs and services — modify, set, balance, total, format."
url: https://roundly-consulting.com/open-source/docs/credits-for-laravel/actions
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Credits for Laravel](https://roundly-consulting.com/open-source/docs/credits-for-laravel.md)

# DI and actions

Three equivalent entry points run the same code. The facade is the shortest and the recommended default. RoundlyConsulting\\Credits\\CreditsManager — the facade’s root, an injectable singleton — gives the identical API as an explicit constructor dependency, with no static calls:

```php
use RoundlyConsulting\Credits\CreditsManager;

final class RewardSignup
{
    public function __construct(private CreditsManager $credits) {}

    public function __invoke(User $user): void
    {
        $this->credits->for($user)->bucket('points')->add(100, 'Welcome');
    }
}
```

The manager also has the flat verbs every scope ends in:

```php
use RoundlyConsulting\Credits\DataTransferObjects\CreditChangeData;

$credits->modify($user, new CreditChangeData(amount: -30, description: 'purchase', bucket: 'points'));
$credits->setTo($user, 500, 'manual adjustment');
$credits->balance($user, bucket: 'points');
$credits->total($user, ['promotional', 'purchased']);           // null = every bucket
```

And each operation is a single-purpose action with one execute() method, for when you want to resolve and run it yourself — from a job, a command or your own action:

```php
use RoundlyConsulting\Credits\Actions\{FormatCreditsAction, GetCreditsBalanceAction,
    GetCreditsTotalAction, ModifyCreditsAction, ResolveBucketCurrencyAction, SetCreditsAction};

app(ModifyCreditsAction::class)->execute($user, new CreditChangeData(amount: -30, description: 'purchase'));
app(SetCreditsAction::class)->execute($user, 500, 'manual adjustment');
app(GetCreditsBalanceAction::class)->execute($user, bucket: 'points');
app(GetCreditsTotalAction::class)->execute($user, ['promotional', 'purchased']);
app(FormatCreditsAction::class)->execute(123450, scale: 2);
app(ResolveBucketCurrencyAction::class)->execute('store_credit');
```

## Facade method → action

| Facade / manager method | Returns | Action | Notes |
| --- | --- | --- | --- |
| `for($owner)` | CreditsScope | — | The entry point for one owner. |
| `format($amount, $scale, $rounding, $storedScale)` | string | `FormatCreditsAction` | Needs no model. |
| `currency($bucket)` | ?Currency | `ResolveBucketCurrencyAction` | null = plain credits. |
| `balance($owner, $bucket, $at)` | int | `GetCreditsBalanceAction` | One bucket. |
| `total($owner, $buckets, $at)` | int | `GetCreditsTotalAction` | null = every bucket; the list is de-duplicated; \[\] is 0. |
| `modify($owner, CreditChangeData $data)` | Credit | `ModifyCreditsAction` | Signed; overdraft-guarded; fires CreditsModified. |
| `setTo($owner, $amount, $description, $meta, $allowOverdraft, $bucket)` | ?Credit | `SetCreditsAction` | One delta row; null when it already matches. |
| `fake()` | CreditsFake | — | Static, on the facade — see Testing. |

The manager resolves each action from the container every time it is called, so binding your own implementation of an action changes the behaviour without forking the package. Credits::fake() swaps every injected CreditsManager too — the HasCredits methods and credits:modify included.

## ModifyCreditsAction

Records one ledger row inside a database transaction: it takes the owner lock (a bump of the owner’s credit\_locks row), reads the bucket’s balance under a row lock, applies the overdraft guard, appends the row and dispatches CreditsModified with the balance the change produced. It takes a CreditChangeData:

```php
use RoundlyConsulting\Credits\DataTransferObjects\CreditChangeData;

$change = new CreditChangeData(amount: 50, description: 'top-up');

$change->resolvedBucket(); // 'default' — config('credits.default_bucket') when bucket is null
$change->toAttributes();   // ['amount' => 50, 'description' => 'top-up', 'meta' => null, 'bucket' => 'default']
```

## SetCreditsAction

Adjusts one bucket to an exact amount through ModifyCreditsAction. It takes the owner lock and reads the balance under a row lock in the same transaction as the write, so racing calls serialise; it returns null when the delta is zero:

```php
// execute(Model&Creditable $creditable, int $amount, ?string $description = null,
//         ?array $meta = null, bool $allowOverdraft = false, ?string $bucket = null): ?Credit
app(SetCreditsAction::class)->execute($user, 500, 'adjustment', null, false, 'promotional');
```

## GetCreditsBalanceAction

Sums one bucket’s ledger rows. Pass lockForUpdate: true inside your own transaction to read under row locks — the same read the overdraft guard uses:

```php
// execute(Model&Creditable $creditable, ?CarbonInterface $at = null, bool $lockForUpdate = false, ?string $bucket = null): int
app(GetCreditsBalanceAction::class)->execute($user, at: now()->subWeek(), bucket: 'promotional');
```

## GetCreditsTotalAction

Sums several buckets, or every bucket the owner has:

```php
// execute(Model&Creditable $creditable, ?array $buckets = null, ?CarbonInterface $at = null): int
$total = app(GetCreditsTotalAction::class);

$total->execute($user, ['promotional', 'purchased']);   // de-duplicated; [] returns 0
$total->execute($user);                                  // null = every bucket
$total->execute($user, at: now()->subWeek());            // ... at a point in time
```

## FormatCreditsAction

The formatting core behind Credits::format() and the display helpers — a thin layer over money-for-laravel’s MinorUnits::rescale() and MinorUnits::toDecimal(). $storedScale defaults to credits.scale; a denominated bucket passes its currency exponent:

```php
use RoundlyConsulting\Credits\Actions\FormatCreditsAction;

// execute(int $amount, ?int $scale = null, ?\RoundingMode $rounding = null, ?int $storedScale = null): string
app(FormatCreditsAction::class)->execute(150, scale: 2, rounding: \RoundingMode::HalfEven); // "1.50" (credits.scale = 2)
app(FormatCreditsAction::class)->execute(1050, storedScale: 2);                            // "10.50" — read as cents
```

## ResolveBucketCurrencyAction

Resolves a bucket’s currency from credits.currencies against money’s registry, on every call. The money-typed helpers throw BucketNotDenominatedException for a bucket it resolves to null:

```php
use RoundlyConsulting\Credits\Actions\ResolveBucketCurrencyAction;

// execute(?string $bucket = null): ?Currency
app(ResolveBucketCurrencyAction::class)->execute('store_credit'); // ?Currency — null for a plain bucket
app(ResolveBucketCurrencyAction::class)->execute();               // null = credits.default_bucket
```

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/credits-for-laravel.md)
