---
title: "DI and actions — Money for Laravel | Roundly"
description: "Inject ExchangeManager or the CurrencyRegistry contract instead of the facades, or call the three rate actions directly with their DTOs."
url: https://roundly-consulting.com/open-source/docs/money-for-laravel/without-facade
language: en
---

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

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

# DI and actions

The facades are the recommended default, not the only way in. There are three equivalent entry points to the same code:

- The Exchange facade — the shortest form, used throughout these docs.
- The manager — RoundlyConsulting\\Money\\Exchange\\ExchangeManager, the container singleton and the root of the Exchange facade. Inject it for the same API with an explicit dependency and no static calls.
- Actions — RefreshExchangeRatesAction, StoreExchangeRatesAction and PruneExchangeRatesAction: single-purpose classes with execute(), for composing into your own actions, jobs and commands.

## Injecting the manager

```php
use RoundlyConsulting\Money\Exchange\ExchangeManager;

final class RecordCzkRate
{
    public function __construct(private ExchangeManager $exchange) {}

    public function __invoke(string $rate): void
    {
        $this->exchange->rates()->manual('EUR', 'CZK', $rate); // same call as Exchange::rates()->manual()
    }
}
```

Exchange::fake() swaps the injected instance too — the fake extends ExchangeManager, so a class that type-hints the manager receives it in tests rather than a TypeError. For currencies, inject the RoundlyConsulting\\Money\\Contracts\\CurrencyRegistry contract:

```php
use RoundlyConsulting\Money\Contracts\CurrencyRegistry;

public function __construct(private CurrencyRegistry $currencies) {}

$this->currencies->get('EUR')->exponent;   // 2 — the same singleton Currencies resolves
```

## Calling an action

```php
use RoundlyConsulting\Money\Actions\StoreExchangeRatesAction;
use RoundlyConsulting\Money\Exchange\ExchangeRate;

app(StoreExchangeRatesAction::class)->execute([
    ExchangeRate::fromDecimal('EUR', 'CZK', '25.10', now(), source: 'manual'),
]);
```

```php
use Carbon\CarbonImmutable;
use RoundlyConsulting\Money\Actions\PruneExchangeRatesAction;
use RoundlyConsulting\Money\Actions\RefreshExchangeRatesAction;
use RoundlyConsulting\Money\DataTransferObjects\PruneExchangeRatesData;
use RoundlyConsulting\Money\DataTransferObjects\RefreshExchangeRatesData;
use RoundlyConsulting\Money\Enums\EcbFeed;

$result = app(RefreshExchangeRatesAction::class)->execute(
    new RefreshExchangeRatesData('ecb', EcbFeed::Recent),
);
$result->stored;                   // rows written
$result->skippedManual;            // manual rows left untouched
$result->skippedUnknownCurrency;   // feed currencies the registry does not know
$result->latestDate;               // ?CarbonImmutable

$deleted = app(PruneExchangeRatesAction::class)->execute(
    new PruneExchangeRatesData(CarbonImmutable::now()->subYear()),
);
```

## Facade method → action

| Facade method | Action | Takes | Note |
| --- | --- | --- | --- |
| `Exchange::rates()->refresh()` | `RefreshExchangeRatesAction` | `RefreshExchangeRatesData` | Runs now. |
| `Exchange::rates()->refreshLater()` | `RefreshExchangeRatesAction` | `RefreshExchangeRatesData` | Through the queued RefreshExchangeRates job. |
| `Exchange::rates()->store(...$rates)` | `StoreExchangeRatesAction` | `iterable<ExchangeRate>` | The one write path. |
| `Exchange::rates()->manual(…)` | `StoreExchangeRatesAction` | `iterable<ExchangeRate>` | One rate with source manual. |
| `Exchange::rates()->prune(…)` | `PruneExchangeRatesAction` | `PruneExchangeRatesData` | Returns the rows deleted or counted. |

- RefreshExchangeRatesData takes source = 'ecb', feed = EcbFeed::Daily and optional from and to dates; PruneExchangeRatesData takes before, includeManual = false and pretend = false.
- Refresh and store return a RefreshResult (source, stored, skippedManual, skippedUnknownCurrency, latestDate); prune returns an int.
- The commands and the queued RefreshExchangeRates job run the same code.
- Exchange::fake() records what goes through Exchange::rates() — on the facade or an injected manager. An action you call directly runs for real.

## 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/money-for-laravel.md)
