Quick start

The example reads the weather for a city from some provider's API. You write the model, the source and the origin, and after that the weather is one more attribute of the city.

1. The 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,
    ];
}

The map is a property, next to $fillable and the rest. The trait deliberately does not declare it: PHP only allows a class to redeclare a trait property with the same default, so a trait that declared $sources would stop every model from writing its own map. When the map has to be computed, override getSources() instead.

The key is the name you will use everywhere: source('weather'), the name column on every row, and the config block for that source.

2. The source

What the resource looks like, and how your record becomes their payload.

namespace App\Sources;

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

#[UsesOrigin(WeatherOrigin::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'];
    }

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

arguments() is the map the origin receives: city_id is filled with $city->external_id when the arguments are resolved, so the client never reaches into your model.

3. The origin

namespace App\Origins;

use EduLazaro\Larasources\Origins\Origin;

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

    public function fetch(array $arguments = []): array
    {
        return $this->http()
            ->get("https://api.example.com/weather/{$arguments['city_id']}")
            ->throw()
            ->json();
    }
}

fetch() is the only abstract method: a source you only read needs nothing else. http() is a Laravel PendingRequest with this integration's timeout and retries already applied, and ->throw() is what turns a bad response into a failure the package understands.

4. Read it

$weather = $city->source('weather');

$weather->temperature;   // 21.5
$weather->feels_like;    // 15.5, the accessor over cached data

The first read calls the provider and writes the result to the sources table. Every read after that serves the row.

$city->source('weather')->fetch();   // live from the provider, row refreshed
$city->source('weather')->record();  // the row, or null if there is none
$city->source('weather')->clear();   // drop the row, leave the provider alone

5. Write it

Add save() to the origin and a build() to the source, and the same object pushes:

// WeatherSource
public function build(): void
{
    $this->fill([
        'temperature' => $this->sourceable->last_reading,
    ]);
}

// WeatherOrigin
public function save(array $data): array
{
    return $this->http()
        ->post('https://api.example.com/weather', $data)
        ->throw()
        ->json();
}
$city->source('weather')->save();

build() runs first, then the push, and the row is written only once the push has returned. A failed write leaves the stored copy untouched and throws, which is what a queued job wants. When the caller is a screen rather than a job, trySave() reports the outcome instead of throwing.

Where to go next

  • Sources for casts, accessors, variants and build().
  • Origins for the failure contract, the HTTP client and credentials.
  • Reading and writing for the cache rules and what a write reports back.
  • Testing for mocking a source and faking the provider.