PHP code example of sanchescom / laravel-rest

1. Go to this page and download the library: Download sanchescom/laravel-rest library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

sanchescom / laravel-rest example snippets


class Post extends Model
{
    protected ?string $endpoint = 'posts';

    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class, 'user_id');
    }
}

Post::where('status', 'published')->orderBy('created_at')->with('author')->paginate(15);



use Sanchescom\Rest\Model;

class Post extends Model
{
    // endpoint is inferred from the class name: "posts"
    protected array $casts = ['id' => 'int', 'userId' => 'int'];
}

// config/rest.php
'clients' => [
    'placeholder' => [
        'provider' => 'guzzle',
        'base_uri' => 'https://jsonplaceholder.typicode.com/',
    ],
],

$post  = Post::get(1);                  // GET posts/1        -> Post
$posts = Post::get();                   // GET posts          -> Collection<Post>
$mine  = $posts->where('userId', 1);    // in-memory collection filter (server-side: see Query Builder)
$page  = $posts->paginate(10);          // LengthAwarePaginator

$some  = Post::getMany([3, 1, 2]);      // concurrent requests, results in [3, 1, 2] order

$new   = Post::post(['title' => 'Hi', 'userId' => 1]);   // POST posts
$upd   = Post::put(1, ['title' => 'Updated']);           // PUT posts/1
Post::delete(1);                                          // DELETE posts/1

// ...or update/delete through an instance using its own primary key:
$post = Post::get(2);
$post->title = 'Changed';
$post->put();                           // PUT posts/2
$post->delete();                        // DELETE posts/2

Post::get(987654);                      // 404 -> throws ModelNotFoundException



return [
    'default' => env('REST_CLIENT', 'localhost'),

    'clients' => [
        'localhost' => [
            'provider' => 'guzzle',
            'base_uri' => 'https://localhost/',
            'options' => [
                'headers' => [
                    'Accept' => 'application/json',
                ],
            ],
        ],
        'billing' => [
            'provider' => 'guzzle',
            'base_uri' => 'https://billing.example.com/api/',
            'options' => [
                'headers' => [
                    'Authorization' => 'Bearer '.env('BILLING_TOKEN'),
                ],
            ],
        ],
    ],
];



use Sanchescom\Rest\Model;

class User extends Model
{
    /** Client name from config/rest.php; null uses the default client. */
    protected ?string $client = null;

    /** Endpoint; defaults to the snake-cased plural of the class name ("users"). */
    protected ?string $endpoint = 'users';

    /** Key that wraps payloads in responses (e.g. {"data": ...}); null for bare payloads. */
    protected ?string $dataKey = 'data';

    /** Whitelist of fillable attributes; empty array allows everything. */
    protected array $fillable = ['id', 'first_name', 'last_name', 'email'];

    /** Attribute casts applied on read: int|float|bool|string. */
    protected array $casts = ['id' => 'int'];

    /** Per-model HTTP client options merged over the configured ones. */
    protected array $options = [];
}

use Sanchescom\Rest\Exceptions\ValidationException;

try {
    User::post(['email' => 'not-an-email']);
} catch (ValidationException $e) {
    $errors = $e->errors();
}

$posts = Post::where('status', 'active')->paginate(15);        // page from ?page=
$posts = Post::orderBy('id')->simplePaginate(15, 'p', 3);      // explicit page name and page

'clients' => [
    'crm' => [
        'base_uri'   => 'https://crm.example.com/api/',
        'pagination' => 'laravel',   // total: meta.total, next: links.next
        // 'pagination' => 'django',  // total: count,      next: next
        // 'pagination' => 'jsonapi', //                    next: links.next
        // 'pagination' => [
        //     'style'    => 'offset',          // 'page' (default) or 'offset'
        //     'total'    => 'meta.count',
        //     'has_more' => 'meta.has_more',   // alternative to 'next'
        // ],
    ],
],

foreach (Post::where('status', 'active')->lazy() as $post) {
    // ...
}

// Mirror an API resource into a local table
Post::lazy(50)->chunk(500)->each(
    fn ($posts) => DbPost::upsert($posts->map->toArray()->values()->all(), ['id']),
);

$paginator = User::get()->paginate(15); // slices an already-fetched collection

// Plain grammar (default) — ?status=active&sort=-created_at&limit=20
$posts = Post::where('status', 'active')
    ->orderBy('created_at', 'desc')
    ->limit(20)
    ->get();

// Pagination
Post::page(2)->limit(15)->get();   // ?page=2&limit=15
Post::offset(30)->limit(15)->get(); // ?offset=30&limit=15

// Extra / non-standard params
Post::withQuery(['ching items

'clients' => [
    'myapi' => [
        'provider' => 'guzzle',
        'base_uri' => 'https://api.example.com/',
        'query'    => 'jsonapi',   // filter[field]=..., page[size]=..., page[number]=...
    ],
],

// config/rest.php
'clients' => [
    'catalog' => [
        'provider' => 'guzzle',
        'base_uri'  => 'https://api.example.com/v2/',
        'query' => [
            'names' => [
                'limit'  => 'page.size',    // page[size]=...
                'page'   => 'page.number',  // page[number]=...
                'offset' => 'page.offset',  // page[offset]=...
            ],
        ],
    ],
],

Article::page(2)->limit(25)->get();
// GET articles?page[size]=25&page[number]=2

'query' => ['sort' => 'dash'],

Product::orderBy('price', 'desc')->orderBy('name')->get();
// GET products?sort=-price,name

'query' => ['sort' => 'suffix'],              // uses ':' separator
'query' => ['sort' => 'suffix', 'sort_suffix' => '|'],  // custom separator

Product::orderBy('price', 'desc')->orderBy('name')->get();
// GET products?sort=price:desc,name:asc
// (or price|desc,name|asc with the custom separator)

'query' => [
    'sort'       => 'separate',
    'sort_names' => ['field' => 'sort_by', 'direction' => 'sort_dir'],
],

Product::orderBy('price', 'desc')->get();
// GET products?sort_by=price&sort_dir=desc

'query' => ['sort' => 'array'],

Product::orderBy('price', 'desc')->orderBy('name')->get();
// GET products?sort[price]=desc&sort[name]=asc

'query' => ['filters' => 'plain'],

Order::where('status', 'shipped')->where('total', 'gte', 100)->get();
// GET orders?status=shipped&total[gte]=100

'query' => ['filters' => 'brackets'],

Order::where('status', 'shipped')->where('total', 'gte', 100)->get();
// GET orders?filter[status]=shipped&filter[total][gte]=100

'query' => ['filters' => 'django'],

Order::where('status', 'shipped')->where('total', 'gte', 100)->get();
// GET orders?status=shipped&total__gte=100

// JSON:API preset — brackets filters, dash sort, page[size]/page[number]/page[offset]
'query' => 'jsonapi',

// Django DRF preset — django filters, dash sort, ordering/page_size
'query' => 'django',

// Preset with overrides — start from jsonapi and change just the sort style
'query' => [
    'preset' => 'jsonapi',
    'sort'   => 'suffix',
],

// jsonapi preset in action
Article::where('published', true)->orderBy('created_at', 'desc')->page(2)->limit(15)->get();
// GET articles?filter[published]=true&sort=-created_at&page[size]=15&page[number]=2

// django preset in action
Article::where('published', true)->orderBy('created_at', 'desc')->limit(15)->get();
// GET articles?published=true&ordering=-created_at&page_size=15

'query' => ['casing' => 'snake'],   // camel → snake
'query' => ['casing' => 'camel'],   // snake → camel

// 'casing' => 'snake'
Report::where('reportDate', '2024-01-01')->orderBy('totalRevenue', 'desc')->get();
// GET reports?report_date=2024-01-01&sort=-total_revenue

class Tenant extends Model
{
    protected ?string $client   = 'platform';
    protected ?string $endpoint = 'tenants';

    /** @var array<string, string> */
    protected array $headers = [
        'X-Tenant-ID' => 'acme',
    ];
}

// GET tenants   (headers: X-Tenant-ID: acme)
Tenant::get();

Tenant::withHeaders([
    'X-Tenant-ID'      => 'betacorp',   // overrides the model default
    'Accept-Language'  => 'fr-FR',
])->where('active', true)->get();
// GET tenants?active=true   (headers: X-Tenant-ID: betacorp, Accept-Language: fr-FR)

'clients' => [
    'myapi' => [
        'provider'   => 'guzzle',
        'base_uri'   => 'https://api.example.com/',
        'errors_key' => 'meta.validation_errors',
    ],
],

// Response body from the API:
// {
//   "message": "Unprocessable",
//   "meta": {
//     "validation_errors": {"email": ["already taken"]}
//   }
// }

try {
    User::post(['email' => '[email protected]']);
} catch (\Sanchescom\Rest\Exceptions\ValidationException $e) {
    $e->errors(); // ['email' => ['already taken']]
}

'clients' => [
    'myapi' => [
        'provider'      => 'guzzle',
        'base_uri'      => 'https://api.example.com/',
        'update_method' => 'patch',
    ],
],

User::put(42, ['email' => '[email protected]']);
// PATCH users/42   {"email":"[email protected]"}

class Invoice extends Model
{
    protected ?string $endpoint       = 'invoices';
    protected ?string $requestDataKey = 'data';  // write envelope
    protected ?string $dataKey        = 'data';  // read unwrap key
}

Invoice::post(['number' => 'INV-001', 'total' => 500]);
// POST invoices
// Body: {"data":{"number":"INV-001","total":500}}

Invoice::get(1);
// GET invoices/1
// Response: {"data":{"id":1,"number":"INV-001","total":500}}
// → Invoice with attributes {id:1, number:"INV-001", total:500}

use Sanchescom\Rest\ClientResolver;
use Sanchescom\Rest\Clients\GuzzleClient;
use Sanchescom\Rest\Model;

$resolver = new ClientResolver;
$resolver->addClient('catalog', GuzzleClient::fromConfig([
    'base_uri' => 'https://api.example.com/',
]));
$resolver->setDefaultClient('catalog');
$resolver->setQueryConfig('catalog', 'jsonapi');   // preset string

Model::setClientResolver($resolver);

// Or with a full config array:
$resolver->setQueryConfig('catalog', [
    'preset' => 'jsonapi',
    'sort'   => 'suffix',
]);

// config/rest.php
'clients' => [
    'commerce' => [
        'provider'      => 'guzzle',
        'base_uri'      => 'https://api.commerce.example/v3/',
        'update_method' => 'patch',
        'errors_key'    => 'errors.fields',
        'query' => [
            'preset'  => 'jsonapi',  // brackets filters, dash sort, page[size]/page[number]
            'sort'    => 'suffix',   // override sort to suffix style
            'casing'  => 'snake',    // normalise camelCase PHP fields to snake_case on the wire
        ],
        'auth' => [
            'driver' => 'bearer',
            'token'  => env('COMMERCE_TOKEN'),
        ],
    ],
],

class Order extends Model
{
    protected ?string $client         = 'commerce';
    protected ?string $endpoint       = 'orders';
    protected ?string $dataKey        = 'data';
    protected ?string $requestDataKey = 'data';

    /** @var array<string, string> */
    protected array $headers = ['X-Store-ID' => 'eu-west-1'];
}

// List — filters, sort, pagination, casing all applied automatically
Order::where('status', 'pending')
     ->where('totalAmount', 'gte', 50)
     ->orderBy('createdAt', 'desc')
     ->page(2)
     ->limit(20)
     ->get();
// GET orders?filter[status]=pending&filter[total_amount][gte]=50
//            &sort=created_at:desc&page[size]=20&page[number]=2
// Headers: Authorization: Bearer <token>, X-Store-ID: eu-west-1

// Per-chain header override for a specific locale
Order::withHeaders(['Accept-Language' => 'de-DE'])
     ->where('status', 'pending')
     ->get();
// GET orders?filter[status]=pending
// Headers: Authorization: Bearer <token>, X-Store-ID: eu-west-1, Accept-Language: de-DE

// Create — body wrapped in 'data'
Order::post(['customerEmail' => '[email protected]', 'totalAmount' => 99.90]);
// POST orders
// Body: {"data":{"customerEmail":"[email protected]","totalAmount":99.9}}

// Validation error — errors extracted from nested key
try {
    Order::post(['customerEmail' => 'bad']);
} catch (\Sanchescom\Rest\Exceptions\ValidationException $e) {
    // Response: {"errors":{"fields":{"customer_email":["invalid email"]}}}
    $e->errors(); // ['customer_email' => ['invalid email']]
}

// config/rest.php
'cache' => [
    'store' => null,   // null = default Laravel store; 'redis', 'memcached', etc.
    'ttl'   => 300,    // default TTL in seconds used by withCache() without args
],

class Post extends Model
{
    protected ?int $cacheTtl = 60; // seconds; null (default) = caching disabled
}

// Enable caching for one chain (uses model $cacheTtl, or default TTL if not set)
Post::withCache()->get();

// Use a specific TTL just for this chain
Post::withCache(30)->get();

// Force a fresh hit even if the model has $cacheTtl
Post::withoutCache()->get();

$page1a = Post::withCache()->page(1)->get(); // HTTP request
$page2  = Post::withCache()->page(2)->get(); // HTTP request
$page1b = Post::withCache()->page(1)->get(); // cache hit — zero HTTP

Post::post(['title' => 'New']);  // POST + flushCache() on success
Post::put(1, ['title' => 'Hi']); // PUT  + flushCache() on success
Post::delete(1);                 // DELETE + flushCache() on success

Post::flushCache(); // safe no-op when no store is configured

Model::setCacheStore(null);
CachedPost::get(); // throws RestException: "no cache store configured"

use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
use Sanchescom\Rest\Model;

Model::setCacheStore(
    new Psr16Cache(new FilesystemAdapter),
    defaultTtl: 120,
);

// Now any model with $cacheTtl or withCache() will use the file cache
Post::withCache(60)->get(1);

// config/rest.php
'memoize' => (bool) env('REST_MEMOIZE', false),

// or at runtime
Rest::memoize();

$comments = Comment::where('postId', 1)->get();

foreach ($comments as $comment) {
    echo $comment->author->name; // one request per distinct author, not per comment
}

use Sanchescom\Rest\Rest;
use Sanchescom\Rest\Model;

// ArrayCache is a test double from this package's tests/ — in your app wire any PSR-16 store instead
Model::setCacheStore(new \Sanchescom\Rest\Tests\Support\ArrayCache);

Rest::fake(['posts' => Rest::response([['id' => 1]])]);

Post::withCache()->page(1)->get();
Post::withCache()->page(2)->get();
Post::withCache()->page(1)->get(); // cache hit

Rest::assertSentCount(2); // page1 + page2 — page1 repeat was served from cache

Rest::restore();
Model::setCacheStore(null);

class Post extends Model
{
    // FK filter: GET comments?postId=1
    public function comments(): \Sanchescom\Rest\Relations\HasMany
    {
        return $this->hasMany(Comment::class);
    }

    // Nested URL: GET posts/1/thumbnail
    public function thumbnail(): \Sanchescom\Rest\Relations\HasOne
    {
        return $this->hasOne(Thumbnail::class)->nested();
    }

    // Parent lookup: GET users/{post->userId}
    public function author(): \Sanchescom\Rest\Relations\BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

$post = Post::get(1);
$post->comments;          // Collection<Comment>  — loaded once, cached
$post->thumbnail;         // ?Thumbnail
$post->author;            // ?User

// Relations also chain onto the builder:
$post->comments()->where('approved', true)->get();

$posts = Post::with(['comments.author', 'author'])->get();
$posts = Post::with(['comments' => fn (Builder $query) => $query->orderBy('createdAt', 'desc')])->paginate(20);

$posts->load('comments');   // on an already fetched collection

public function comments(): HasMany
{
    return $this->hasMany(Comment::class)->batch();   // GET comments?postId=1,2,3
}

public function author(): BelongsTo
{
    return $this->belongsTo(User::class)->batch();    // GET users?id=7,8
}

// Bearer token
'auth' => ['driver' => 'bearer', 'token' => env('API_TOKEN')],

// HTTP Basic
'auth' => ['driver' => 'basic', 'username' => '…', 'password' => '…'],

// Arbitrary header(s) — name must match what the API expects exactly
'auth' => [
    'driver'  => 'header',
    'headers' => ['X-API-Key' => env('API_KEY')],
],

// Custom driver — any class implementing AuthInterface
'auth' => ['driver' => \App\Auth\HmacAuth::class, 'secret' => env('HMAC_SECRET')],

'retry' => [
    'times'              => 3,       // max attempts (excluding the first)
    'delay'              => 100,     // base delay in ms
    'multiplier'         => 2.0,     // exponential multiplier
    'statuses'           => [429, 500, 502, 503, 504],
    'respect_retry_after' => true,   // honour Retry-After response header
],

Post::creating(function (Post $post): bool|void {
    if ($post->title === '') {
        return false; // cancel
    }
});

Post::created(function (Post $post): void {
    Cache::forget('posts');
});

Post::updating(fn (Post $post) => /* return false to cancel */ null);
Post::updated(fn (Post $post) => null);
Post::deleting(fn (Post $post) => /* return false to cancel */ null);
Post::deleted(fn (Post $post) => null);

use Illuminate\Events\Dispatcher;

Model::setEventDispatcher(app(Dispatcher::class));

use Sanchescom\Rest\Rest;

Rest::fake([
    'posts/*' => Rest::response(['id' => 1, 'title' => 'Hello']),
    'posts'   => Rest::response(['id' => 2, 'title' => 'Created']),
]);

$post = Post::get(1);                  // GET posts/1  -> {'id': 1, ...} from the fake
Post::post(['title' => 'Created']);    // POST posts   -> {'id': 2, ...} from the fake

Rest::assertSentCount(2);

Rest::assertSent(function ($request) {
    return $request->method() === 'GET' && $request->uri() === 'posts/1';
});

Rest::assertNotSent(function ($request) {
    return $request->method() === 'DELETE';
});

// Inspect all recorded requests
$requests = Rest::recorded(); // list<RecordedRequest>

// Restore the real resolver in teardown
Rest::restore();

use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Sanchescom\Rest\Clients\GuzzleClient;

config()->set('rest.default', 'testing');
config()->set('rest.clients.testing', ['provider' => 'mock']);

app('rest')->extend('mock', function () {
    $mock = new MockHandler([
        new Response(200, [], '{"data":[{"id":1}]}'),
    ]);

    return new GuzzleClient(new Client([
        'handler' => HandlerStack::create($mock),
        'base_uri' => 'https://api.test/',
        'http_errors' => false,
    ]));
});

$users = User::get(); // served from the mock

use Sanchescom\Rest\ClientResolver;
use Sanchescom\Rest\Clients\GuzzleClient;
use Sanchescom\Rest\Model;

$resolver = new ClientResolver([
    'placeholder' => GuzzleClient::fromConfig([
        'base_uri' => 'https://jsonplaceholder.typicode.com/',
        'options' => ['headers' => ['Accept' => 'application/json']],
    ]),
]);
$resolver->setDefaultClient('placeholder');

Model::setClientResolver($resolver);

Post::get(1); // works — no Laravel container involved
bash
php artisan vendor:publish --tag=rest-config