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
saloon
codebar-ag/coding-guidelines
architect
implement
refactor
review
saloon
api-integrations
connectors
request-classes
dto-mapping
service-layer
testing

Saloon-based service layer pattern for all external API integrations. Every new external API integration must use Saloon — no raw HTTP calls.

Install
php artisan boost:add-skill codebar-ag/coding-guidelines

Save this content to: .claude/skills/saloon/SKILL.md

# Saloon

## When to Use

- For all external API integrations from application code.
- When an integration needs request classes, typed DTO mapping, and testable connector wiring.
- When building or refactoring service-layer API clients under `app/Services/{ServiceName}/`.

## When NOT to Use

- For internal Laravel-to-Laravel calls inside the same app boundary.
- For one-off, simple binary file downloads (for example downloading a public PDF by URL), where inline `Http::get()` is acceptable.
- For LLM integrations that already use Prism conventions.

## Preconditions

- `saloonphp/saloon` is installed and configured.
- API credentials and endpoints are defined in `config/services.php`.
- A service class exists (or is planned) to wrap connector usage and DTO mapping.

## Rules

- **All** new external API integrations must use the Saloon pattern
- No raw HTTP calls (`Http::get(...)`, `file_get_contents`, `curl`)
- Organize each external integration under `app/Services/{ServiceName}/`
- The connector extends `Saloon\Http\Connector` and handles authentication and base URL
- One class per API endpoint, extending `Saloon\Http\Request`
- Use constructor promotion for request parameters
- GET requests define `defaultQuery()` for query parameters
- POST/PUT requests implement `HasBody` + use `HasJsonBody` trait with `defaultBody()`
- The service class wraps the connector, sends requests, and returns typed DTOs
- Register the connector as a singleton in a service provider when needed
- Accept an optional connector in the service constructor for testability
- Exceptions: **Prism** is acceptable for LLM integrations; inline `Http::get()` is acceptable for simple binary file downloads (not API integrations)
- Follow general service conventions from `Services/SKILL.md` for naming and class boundaries

## Examples

**Directory Structure:**
```
app/Services/Stripe/
  StripeConnector.php
  StripeService.php
  Requests/
    Charges/
      CreateChargeRequest.php
      ListChargesRequest.php
  DataObjects/
    ChargeData.php
```

```php
// Connector
namespace App\Services\Stripe;

use Saloon\Http\Connector;

class StripeConnector extends Connector
{
    public function resolveBaseUrl(): string
    {
        return 'https://api.stripe.com/v1';
    }

    protected function defaultHeaders(): array
    {
        return [
            'Authorization' => 'Bearer ' . config('services.stripe.secret'),
            'Content-Type'  => 'application/json',
        ];
    }
}
```

```php
// GET Request
use Saloon\Enums\Method;
use Saloon\Http\Request;

class ListChargesRequest extends Request
{
    protected Method $method = Method::GET;

    public function __construct(
        protected int $limit = 10,
        protected ?string $customer = null,
    ) {}

    public function resolveEndpoint(): string
    {
        return '/charges';
    }

    protected function defaultQuery(): array
    {
        return array_filter([
            'limit'    => $this->limit,
            'customer' => $this->customer,
        ]);
    }
}
```

```php
// POST Request
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;

class CreateChargeRequest extends Request implements HasBody
{
    use HasJsonBody;

    protected Method $method = Method::POST;

public function __construct(
        protected int $amount,
        protected string $currency,
        protected string $customerId,
    ) {}

    public function resolveEndpoint(): string
    {
        return '/charges';
    }

    protected function defaultBody(): array
    {
        return [
            'amount'   => $this->amount,
            'currency' => $this->currency,
            'customer' => $this->customerId,
        ];
    }
}
```

```php
// Service class
use Saloon\Exceptions\Request\FatalRequestException;
use Saloon\Exceptions\Request\RequestException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;

class StripeService
{
    public function __construct(?StripeConnector $connector = null)
    {
        $this->connector = $connector ?? new StripeConnector();
    }

    /** @return Collection<int, ChargeData> */
    public function listCharges(string $customerId): Collection
    {
        try {
            $response = $this->connector->send(
                new ListChargesRequest(customer: $customerId)
            );
        } catch (RequestException|FatalRequestException $exception) {
            Log::warning('Stripe request failed.', [
                'customer_id' => $customerId,
                'message' => $exception->getMessage(),
            ]);
            throw $exception;
        }

return collect($response->json('data'))
            ->map(fn (array $item) => ChargeData::fromArray($item));
    }
}
```

## Testing Guidance

- Inject a fake or mocked connector into the service constructor.
- Assert request class behavior (`resolveEndpoint()`, `defaultQuery()`, `defaultBody()`) independently.
- Service tests should verify DTO mapping and error propagation, not raw HTTP internals.

## Anti-Patterns

- Using `Http::get(...)` or `file_get_contents()` for API integrations
- Using `curl` directly
- Creating one massive connector class with all API logic — separate requests into individual classes
- Not returning typed DTOs from the service class (returning raw arrays)
- Not caching expensive or frequently accessed API responses
- Hard-coding API credentials in the connector — always use `config()`

## References

- [Saloon Documentation](https://docs.saloon.dev/)
- Related: `DTO/SKILL.md` — service classes return typed DTOs
- Related: `Services/SKILL.md` — general service class conventions

Saloon

When to Use

  • For all external API integrations from application code.
  • When an integration needs request classes, typed DTO mapping, and testable connector wiring.
  • When building or refactoring service-layer API clients under app/Services/{ServiceName}/.

When NOT to Use

  • For internal Laravel-to-Laravel calls inside the same app boundary.
  • For one-off, simple binary file downloads (for example downloading a public PDF by URL), where inline Http::get() is acceptable.
  • For LLM integrations that already use Prism conventions.

Preconditions

  • saloonphp/saloon is installed and configured.
  • API credentials and endpoints are defined in config/services.php.
  • A service class exists (or is planned) to wrap connector usage and DTO mapping.

Rules

  • All new external API integrations must use the Saloon pattern
  • No raw HTTP calls (Http::get(...), file_get_contents, curl)
  • Organize each external integration under app/Services/{ServiceName}/
  • The connector extends Saloon\Http\Connector and handles authentication and base URL
  • One class per API endpoint, extending Saloon\Http\Request
  • Use constructor promotion for request parameters
  • GET requests define defaultQuery() for query parameters
  • POST/PUT requests implement HasBody + use HasJsonBody trait with defaultBody()
  • The service class wraps the connector, sends requests, and returns typed DTOs
  • Register the connector as a singleton in a service provider when needed
  • Accept an optional connector in the service constructor for testability
  • Exceptions: Prism is acceptable for LLM integrations; inline Http::get() is acceptable for simple binary file downloads (not API integrations)
  • Follow general service conventions from Services/SKILL.md for naming and class boundaries

Examples

Directory Structure:

app/Services/Stripe/
  StripeConnector.php
  StripeService.php
  Requests/
    Charges/
      CreateChargeRequest.php
      ListChargesRequest.php
  DataObjects/
    ChargeData.php
// Connector
namespace App\Services\Stripe;

use Saloon\Http\Connector;

class StripeConnector extends Connector
{
    public function resolveBaseUrl(): string
    {
        return 'https://api.stripe.com/v1';
    }

    protected function defaultHeaders(): array
    {
        return [
            'Authorization' => 'Bearer ' . config('services.stripe.secret'),
            'Content-Type'  => 'application/json',
        ];
    }
}
// GET Request
use Saloon\Enums\Method;
use Saloon\Http\Request;

class ListChargesRequest extends Request
{
    protected Method $method = Method::GET;

    public function __construct(
        protected int $limit = 10,
        protected ?string $customer = null,
    ) {}

    public function resolveEndpoint(): string
    {
        return '/charges';
    }

    protected function defaultQuery(): array
    {
        return array_filter([
            'limit'    => $this->limit,
            'customer' => $this->customer,
        ]);
    }
}
// POST Request
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;

class CreateChargeRequest extends Request implements HasBody
{
    use HasJsonBody;

    protected Method $method = Method::POST;

public function __construct(
        protected int $amount,
        protected string $currency,
        protected string $customerId,
    ) {}

    public function resolveEndpoint(): string
    {
        return '/charges';
    }

    protected function defaultBody(): array
    {
        return [
            'amount'   => $this->amount,
            'currency' => $this->currency,
            'customer' => $this->customerId,
        ];
    }
}
// Service class
use Saloon\Exceptions\Request\FatalRequestException;
use Saloon\Exceptions\Request\RequestException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;

class StripeService
{
    public function __construct(?StripeConnector $connector = null)
    {
        $this->connector = $connector ?? new StripeConnector();
    }

    /** @return Collection<int, ChargeData> */
    public function listCharges(string $customerId): Collection
    {
        try {
            $response = $this->connector->send(
                new ListChargesRequest(customer: $customerId)
            );
        } catch (RequestException|FatalRequestException $exception) {
            Log::warning('Stripe request failed.', [
                'customer_id' => $customerId,
                'message' => $exception->getMessage(),
            ]);
            throw $exception;
        }

return collect($response->json('data'))
            ->map(fn (array $item) => ChargeData::fromArray($item));
    }
}

Testing Guidance

  • Inject a fake or mocked connector into the service constructor.
  • Assert request class behavior (resolveEndpoint(), defaultQuery(), defaultBody()) independently.
  • Service tests should verify DTO mapping and error propagation, not raw HTTP internals.

Anti-Patterns

  • Using Http::get(...) or file_get_contents() for API integrations
  • Using curl directly
  • Creating one massive connector class with all API logic — separate requests into individual classes
  • Not returning typed DTOs from the service class (returning raw arrays)
  • Not caching expensive or frequently accessed API responses
  • Hard-coding API credentials in the connector — always use config()

References

  • Saloon Documentation
  • Related: DTO/SKILL.md — service classes return typed DTOs
  • Related: Services/SKILL.md — general service class conventions
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
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
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core