Open source April 2026

Larasources

Larasources integrates external data sources into your Eloquent models, with caching, retry and rate limiting built in. You work with external APIs through model-like abstractions, without forcing those APIs into your own database tables.

Each source declares its fillable fields, casts, mappings and origin (the API client). The domain model stays clean, the integration layer stays separate, and the cached external state lives in one dedicated table.

Requirements: PHP 8.4+, Laravel 9+.

Installation

composer require edulazaro/larasources
php artisan vendor:publish --provider="EduLazaro\Larasources\LarasourcesServiceProvider"
php artisan migrate

Configuration

Origin credentials live in config/larasources.php under origins, keyed by the origin's alias:

'origins' => [
    'my_provider' => [
        'api_key' => env('MY_PROVIDER_API_KEY'),
        'sandbox' => env('MY_PROVIDER_SANDBOX', false),
    ],
],
MY_PROVIDER_API_KEY=your_api_key
MY_PROVIDER_SANDBOX=true

Retry and rate limiting are declared here too, and applied automatically to every origin call.

Attaching a source to a model

use Illuminate\Database\Eloquent\Model;
use EduLazaro\Larasources\Concerns\HasSources;
use App\Sources\WeatherSource;

class City extends Model
{
    use HasSources;

    protected array $sources = [
        'weather' => WeatherSource::class,
    ];
}

Reading and writing

$city = City::find(1);

// Autoloaded from cache, fetched on miss
$weather = $city->source('weather');
echo $weather->temperature;
echo $weather->humidity;

$city->source('weather')->save();    // push to the API and persist locally
$fresh = $city->source('weather')->fetch();  // force refresh, bypassing cache
$city->source('weather')->delete();  // delete remote and clear cache

Defining a Source

namespace App\Sources;

use EduLazaro\Larasources\Source;
use EduLazaro\Larasources\Attributes\UsesOrigin;
use App\Origins\MyProviderOrigin;

#[UsesOrigin(MyProviderOrigin::class)]
class WeatherSource extends Source
{
    protected $fillable = [
        'temperature',
        'humidity',
        'description',
    ];

    protected $casts = [
        'temperature' => 'float',
        'humidity'    => 'integer',
    ];

    protected function arguments(): array
    {
        return [
            'city_id' => 'external_id', // maps to $city->external_id
        ];
    }

    public function getFeelsLikeAttribute(): float
    {
        return $this->temperature - ($this->humidity / 10);
    }
}

Defining an Origin

The Origin is the API client, decoupled from the data shape:

namespace App\Origins;

use EduLazaro\Larasources\Origins\Origin;
use Illuminate\Support\Facades\Http;

class MyProviderOrigin extends Origin
{
    public static function getAlias(): string
    {
        return 'my_provider';
    }

    public function fetch(array $arguments = []): array
    {
        $response = Http::withToken($this->getConfig('api_key'))
            ->get('https://api.example.com/weather/' . $arguments['city_id']);

        return $response->json();
    }

    public function save(array $data): array
    {
        $response = Http::withToken($this->getConfig('api_key'))
            ->post('https://api.example.com/weather', $data);

        return $response->json();
    }

    public function delete(): bool
    {
        return true;
    }
}

Variants and runtime arguments

Variants handle multiple modes per source: sale vs rent for a listing, current vs forecast for weather:

$city->source('weather')->setVariant('forecast')->fetch();

$city->source('weather', ['city_id' => 'custom_id'])->fetch();

Caching

Sources are cached in the sources table through the SourceRecord model, keyed by (sourceable, name, variant).

if ($source->getRecord()) {
    // has been fetched or saved at least once
}

$source->clear();

Error handling

use EduLazaro\Larasources\Exceptions\OriginException;

try {
    $weather = $city->source('weather')->fetch();
} catch (OriginException $e) {
    Log::error('Provider error: ' . $e->getMessage());
}

Testing

$mock = new WeatherSource(['temperature' => 22.5, 'humidity' => 60]);
$city->mockSource(WeatherSource::class, $mock);

$weather = $city->source('weather'); // the mocked instance

API reference

Source

Method Does
fetch() Pull fresh data from the origin
save() Push current attributes to the origin and persist
saveToOrigin() Push without persisting locally
delete() Delete remote and clear cache
clear() Clear the cached record only
origin() Get the resolved Origin instance
getRecord() Get the underlying SourceRecord, or null
setVariant(string $variant) Set the variant
setVariantArguments(array $args) Pass runtime arguments

Origin

fetch(array $arguments): array · save(array $data): array · delete(): bool · regenerate(): array · getAlias(): string

Bundled abstract Origins

Class For
Origin Base class
RemoteOrigin Generic REST client base
AgentOrigin Agent-style integrations
ScraperOrigin HTML scraping, with a getHtml() helper

ScraperOrigin pairs naturally with Larascraper when the external source is a page rather than an API.

built and maintained by Edu Lazaro · MIT license