Download the PHP package eznix86/laravel-remote-models without Composer
On this page you can find all versions of the php package eznix86/laravel-remote-models. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download eznix86/laravel-remote-models
More information about eznix86/laravel-remote-models
Files in eznix86/laravel-remote-models
Package laravel-remote-models
Short Description Eloquent models for remote APIs
License MIT
Homepage https://github.com/eznix86/laravel-remote-models
Informations about the package laravel-remote-models
Remote Models for Laravel
Use your APIs Eloquently.
A RemoteModel is an Eloquent model that reads and writes an HTTP API. Casts,
scopes, accessors, events, serialization, relations and route binding all work
the way you already know them. Only the storage changes.
Installation
Connections
Each api is a connection in config/remote.php.
A model finds its connection in this order:
- The
$connectionproperty, or#[Connection('stripe')]. - The namespace segment after
Remote, kebab cased.App\Models\Remote\Stripe\Customerreadsstripe. remote.default.
A model
This writes app/Models/Remote/Stripe/Customer.php, writes
database/factories/Remote/Stripe/CustomerFactory.php, and adds the stripe
connection to config/remote.php when it is missing.
Every line there answers something Stripe actually does. #[Persist(method: 'post')]
because Stripe updates with POST and has no PATCH. #[BracketFilters] because
Stripe reads created[gte]=. #[Paging(perPage: 'limit', page: null)] because
Stripe pages with limit and has no page number. 'created' => 'immutable_datetime'
because Stripe sends unix timestamps, which Eloquent already understands.
The same model without attributes:
A method always wins over the matching attribute.
Routes
Five routes cover the model. Each one has a rest default built from #[Endpoint],
or from the plural kebab name of the class when there is no #[Endpoint].
| Route | Default | Attribute | Method |
|---|---|---|---|
| List | GET /v1/customers |
#[Index] |
index(PendingRequest $http, array $query) |
| Read | GET /v1/customers/{key} |
#[Show] |
show(PendingRequest $http, string\|int $id) |
| Create | POST /v1/customers |
#[Store] |
store(PendingRequest $http, array $attributes) |
| Update | PATCH /v1/customers/{key} |
#[Persist] |
persist(PendingRequest $http, array $dirty) |
| Delete | DELETE /v1/customers/{key} |
#[Remove] |
remove(PendingRequest $http) |
A uri can hold placeholders. {key} and {id} read the model key. Any other
name reads an attribute, so /v1/accounts/{account}/customers works.
Action apis
Not every api is resource shaped. When the routes are actions and the key travels in the body, name each route and set its method.
A uri with no {key} placeholder cannot carry the key, so the key goes into the
body instead, under the model key name. Person::find(7) posts {"id": 7} to
/api/users/get, $person->save() posts the key with the dirty attributes to
/api/users/update, and $person->delete() posts {"id": 7} to
/api/users/delete. A uri that does hold {key} keeps the rest behaviour and
sends no extra field.
Anything that is not one of the five routes is a plain method. newRemoteRequest()
is public and already carries the connection, the token and the headers.
Reading and writing
on() takes a connection name, an inline config array, or an Eloquent model. A
model answers with toRemoteConnection() when it has that method, and with its
url, token and headers attributes when it does not.
Writing goes through a model instance. Customer::query()->update([...]) throws.
Queries
You query a remote model with the Eloquent builder you already use. Each chain is one request, and it is sent when you ask for the result, not before.
What comes back is an Eloquent collection of models, so the rest is what you would do with any other collection, and the casts on the model have already run:
Scopes compose the same way:
when() builds a filter only when you have one to apply:
Single records and counts read as usual. find() and findOrFail() go to the
read route, the others read the list route and work on what comes back:
cursor() walks everything a page at a time and stops as soon as you stop:
Everything else is Eloquent too: casts, accessors, #[Scope], model events,
toArray(), toJson(), and route model binding through resolveRouteBinding().
What the builder sends
| Builder | Sent |
|---|---|
where('status', 'open') |
status=open |
whereIn('status', [Status::Open, Status::Draft]) |
status=open,draft |
orderBy('created', 'desc'), latest(), oldest() |
sort=created&direction=desc |
limit(100), offset(200), forPage(3, 100) |
per_page=100&page=3 |
when(), with(), count(), first(), find() |
no parameters of their own |
Values are formatted for the wire: a backed enum sends its value, a DateTime
sends ISO 8601, a boolean sends true or false. A value that cannot go in a
query string throws rather than disappearing. Stripe wants unix seconds on
created, so pass $date->getTimestamp() there.
Nested fields are dotted columns. where('metadata.user_id', 7) sends
metadata.user_id=7, and orderBy('metadata.rank') sorts on it.
Filter styles
Comparisons, null checks and negated lists have no single spelling across apis, so a model names the one its api uses. Without an attribute those clauses throw.
| Attribute | where('created', '>=', $at) |
whereNull('paid_at') |
|---|---|---|
#[BracketFilters] |
created[gte]=... |
paid_at[null]=true |
#[BracketFilters(prefix: '$')] |
created[$gte]=... |
paid_at[$null]=true |
#[SuffixFilters] |
created__gte=... |
paid_at__null=true |
#[SuffixFilters(separator: '.')] |
created.gte=... |
paid_at.null=true |
Stripe reads the bracket form, so:
The style covers !=, <, <=, >, >=, like, not like, whereNull,
whereNotNull, whereNotIn and whereBetween. A nested group of and clauses
is flattened, so where(fn ($q) => $q->where(...)->where(...)) works.
Page size
#[Paging] names the parameters. The default is per_page and page. Pass
page: null for an api that has no page number, and an offset() then throws
instead of sending something the api ignores.
Sparse fieldsets
select() is ignored unless the model names the parameter its api reads.
Select only what you read, and keep the key in the list. A model without its key cannot be updated or deleted.
What cannot work
| Builder | Why |
|---|---|
orWhere() |
a query string ands its parameters |
two orderBy() calls |
sort takes one column |
whereHas(), has(), doesntHave() |
relation existence is a database join |
withCount(), withSum() |
same |
Customer::query()->update(), ->delete() |
write through a model instance |
Each of these throws RemoteModels\Exceptions\UnsupportedQuery with the reason.
Override toQuery() on the model when your api can express something the
defaults cannot.
Relations
| Direction | Method | Attribute |
|---|---|---|
| Remote to remote | remoteHasOne(), remoteHasMany() |
#[RemoteHasOne], #[RemoteHasMany] |
| Remote to Eloquent | belongsToLocal() |
#[BelongsToLocal] |
| Eloquent to remote | hasOneRemote(), hasManyRemote() |
#[HasOneRemote], #[HasManyRemote] |
A customer holds its invoices, and the same customer points back at the local user that owns it:
Add RemoteModels\Concerns\InteractsWithRemoteModels to an Eloquent model to give
it hasOneRemote() and hasManyRemote().
The second argument is the query parameter the api filters on, the third is the local column that fills it. A relation sends no filter unless you name one, because a query parameter cannot be guessed from a class name.
via() sets the uri of the relation instead. It takes a template string, a
closure that receives the parent model, or an Illuminate\Support\Uri. A query
string in the uri becomes default query parameters, and a where() on the
relation overrides them.
on() sets the connection, and takes a closure that receives the parent model,
which is how one model serves many tenants:
Eager loading sends one request per parent, all of them concurrent.
Customer::with('user') stays one request and one database query.
A via uri that is eager loaded must be a template such as
/v1/invoices/{key}/lines, or a closure. A uri the relation method already
interpolated only holds for the parent that built it, so eager loading it throws.
Pages
cursor() walks the pages one record at a time and stops as soon as the caller
stops reading.
Pick how the next page is found:
| Attribute | Next page |
|---|---|
#[LinkPagination] (default) |
Link: <...>; rel="next" |
#[PagePagination] |
?page= until a short page comes back |
#[CursorPagination] |
a token read from the payload |
Stripe fits none of the three, because its next cursor is the id of the last
record. Override nextPage() and return where to go:
paginate() needs a total. It reads total, total_count or meta.total, and
throws when none of them is there. Override total() on the model, or use
simplePaginate().
A next page uri is split with Illuminate\Support\Uri, so the filters you set
before paging survive the page turn even when the api leaves them out of its own
next link.
Cache
#[CacheFor(300)] or protected ?int $cacheFor = 300; caches read requests for
that many seconds. The key covers the connection, the uri and the query
parameters. A write forgets the cached record of that model.
Testing
A factory writes into Http::fake() instead of a database.
create() answers the list route and the record route of the model. for($user)
fills the remote key that belongsToLocal() reads. make() sends nothing.
Custom clients
Stripe takes form encoded bodies rather than json. A driver settles that once for every model on the connection:
Publishing
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Thank you for considering contributing to Remote Models for Laravel! Please review our contributing guide to get started.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
- Bruno Bernard
- All Contributors
License
Remote Models for Laravel is open-sourced software licensed under the MIT license.
All versions of laravel-remote-models with dependencies
guzzlehttp/guzzle Version ^7.8.2||^8.0
illuminate/console Version ^13.0
illuminate/database Version ^13.0
illuminate/http Version ^13.0
illuminate/support Version ^13.0
league/uri Version ^7.5