Sources

A Source looks like a model because it behaves like one, minus the table. It declares the remote resource and how to build it from your record, and nothing about HTTP.

Fields, casts and accessors

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

    protected $casts = [
        'temperature' => 'float',
        'humidity' => 'integer',
        'measured_at' => 'datetime',
        'alerts' => 'collection',
        'stations' => 'collection:App\Data\Station',
    ];

    protected $hidden = ['internal_code'];

    protected $appends = ['feels_like'];

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

fill(), forceFill(), toArray(), toJson(), $fillable, $guarded, $hidden, $visible, $appends, array access and the getXAttribute() accessors all behave as you expect from Eloquent.

The collection:Class cast is the one addition. An array inside the payload becomes instances of a class of yours:

protected $casts = ['stations' => 'collection:App\Data\Station'];

$source->stations->first()->distance_km;

That keeps a nested shape typed without inventing a second Source for something that is not a resource of its own.

A Source is a document, not a relational model. There are no relations and nothing to eager load, because what it holds is one payload. What plays that role is the cast above for nesting, and a pointer argument for depending on another source.

Building the payload

build() is where your record becomes their shape. It runs before every write, and it is the only place the mapping lives:

public function build(): void
{
    $this->fill([
        'title' => $this->sourceable->advert_title,
        'price' => (int) $this->sourceable->sale_price,
        'rooms' => $this->sourceable->double_bedroom + $this->sourceable->single_bedroom,
        'features' => $this->buildFeatures(),
    ]);
}

protected function buildFeatures(): array
{
    return $this->sourceable->features
        ->map(fn ($feature) => ['id' => $feature->provider_code])
        ->all();
}

$this->sourceable is the model the source hangs from. Everything the origin sends is what build() left in the attributes, so the client never sees your columns and the mapping is one method you can read top to bottom.

Breaking it into protected helpers like buildFeatures() is worth doing early. Those are the methods that break when a provider changes something, and they are the ones worth testing on their own.

Variants

A variant is the same shape in a different mode. A property published for sale and for rent is two listings on the provider side, each with its own id. Weather has current and forecast.

$property->source('listing')->setVariant('sale')->save();
$property->source('listing')->setVariant('rent')->save();

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

Each variant is cached apart: the row key is the model, the source name and the variant, so the two listings keep their own payloads, their own signatures and their own external ids without a second class.

Inside the source, the variant is yours to read:

public function build(): void
{
    $this->fill([
        'operation' => $this->variant,
        'price' => $this->variant === 'rent'
            ? $this->sourceable->rent_price
            : $this->sourceable->sale_price,
    ]);
}

When each mode is an independent resource on the other side, give each one its own job. Pushing both in one call only works when the provider accepts them in one payload, and that is a property of their API, not of this package:

SyncListing::dispatch($property->id, 'sale');
SyncListing::dispatch($property->id, 'rent')->delay(now()->addSeconds(10));

Variants are optional. Leave them alone and you have one row per model and source, which is the usual case.

Its name

A source's name is how its owner calls it: the key the model maps the class under. It identifies the stored row and the config block, so renaming it orphans existing rows.

$city->source('weather')->name();   // 'weather'

Without a model there is no map to ask, so the source falls back to a name it declares, and then to the class basename:

class ListingSource extends Source
{
    protected ?string $sourceName = 'listing';
}

That matters for sources that exist before their model, where the row is written while nothing is naming it yet.

Its own settings

A Source reads larasources.sources.{name} the way an origin reads its own block:

$source->config('language');          // larasources.sources.weather.language
$source->config('units', 'metric');   // with a default
$source->config();                    // the whole block

Use it for what belongs to the shape rather than to the connection: a language, a unit system, how many items to request. Credentials and timeouts belong to the origin, which is covered in Origins.

Without a model at all

A source does not have to hang from anything:

$rates = ExchangeRatesSource::make()->fetch();

$rates->eur_usd;

make() is a declared static that returns an instance. With no model and no external id nothing is stored, so this is a read you use and throw away. Give it an identity and it becomes storable, which is Identity.