Non-Eloquent Resources
Introduction
Some application may have objects that are not Eloquent models, that need to be represented in your API as JSON:API resources. This is possible using our laravel-json-api/non-eloquent
package. This chapter describes how to use that package to add Non-Eloquent Resources to your API.
TIP
If you are using non-Eloquent resources, we assume you are comfortable with exploring classes and interfaces via your IDE. This chapter does not document everything you can do with non-Eloquent resources: so you will need to explore the classes and interfaces as you build your non-Eloquent resources.
Example Scenario
To illustrate the capability in this chapter, we are going to use the following example scenario. Imagine our Laravel application has a Site
class which looks something like this:
namespace App\Entities;
use Illuminate\Contracts\Support\Arrayable;
class Site implements Arrayable
{
/**
* @var string
*/
private string $slug;
/**
* @var string|null
*/
private ?string $domain;
/**
* @var string|null
*/
private ?string $name;
/**
* Create a new site entity from an array.
*
* @param string $slug
* @param array $values
* @return Site
*/
public static function fromArray(string $slug, array $values)
{
$site = new self($slug);
$site->setDomain($values['domain'] ?? null);
$site->setName($values['name'] ?? null);
return $site;
}
/**
* Site constructor.
*
* @param string $slug
*/
public function __construct(string $slug)
{
$this->slug = $slug;
}
/**
* Get the site slug.
*
* @return string
*/
public function getSlug(): string
{
return $this->slug;
}
/**
* Get the site domain.
*
* @param string|null $domain
* @return $this
*/
public function setDomain(?string $domain): self
{
$this->domain = $domain ?: null;
return $this;
}
/**
* Get the site domain.
*
* @return string|null
*/
public function getDomain(): ?string
{
return $this->domain;
}
/**
* Set the site name.
*
* @param string|null $name
* @return $this
*/
public function setName(?string $name): self
{
$this->name = $name ?: null;
return $this;
}
/**
* Get the site's name.
*
* @return string|null
*/
public function getName(): ?string
{
return $this->name;
}
// ...Other Getters & Setters...
/**
* @inheritDoc
*/
public function toArray()
{
return [
'domain' => $this->getDomain(),
'name' => $this->getName(),
];
}
}
You'll notice the Site
entity is Arrayable
. This is because our application stores the site entities in a file in our application's storage
directory. Retrieving and storing Site
entities occurs via a SiteStorage
class, which might look something like this:
namespace App\Entities;
use Illuminate\Contracts\Filesystem\Filesystem;
class SiteStorage
{
/**
* @var Filesystem
*/
private Filesystem $files;
/**
* @var array
*/
private array $sites;
/**
* SiteStorage constructor.
*
* @param Filesystem $files
*/
public function __construct(Filesystem $files)
{
$this->files = $files;
$this->sites = json_decode($files->get('sites.json'), true);
}
/**
* Find a site by its slug.
*
* @param string $slug
* @return Site|null
*/
public function find(string $slug): ?Site
{
if (isset($this->sites[$slug])) {
return Site::fromArray($slug, $this->sites[$slug]);
}
return null;
}
/**
* @return \Generator
*/
public function cursor(): \Generator
{
foreach ($this->sites as $slug => $values) {
yield $slug => Site::fromArray($slug, $values);
}
}
/**
* Get all sites.
*
* @return array
*/
public function all(): array
{
return iterator_to_array($this->cursor());
}
/**
* Store a site.
*
* @param Site $site
* @return void
*/
public function store(Site $site): void
{
$this->sites[$site->getSlug()] = $site->toArray();
$this->write();
}
/**
* Remove a site.
*
* @param Site|string $site
* @return void
*/
public function remove($site): void
{
if ($site instanceof Site) {
$site = $site->getSlug();
}
unset($this->sites[$site]);
$this->write();
}
/**
* @return void
*/
private function write(): void
{
$this->files->put('sites.json', json_encode($this->sites));
}
}
Installation
The first thing we need to do is add the laravel-json-api/non-eloquent
package to our application, via Composer:
composer require laravel-json-api/non-eloquent
Concept & Initial Setup
For each Non-Eloquent resource, we need a Schema
and a Resource
. We are also likely to need a Repository
.
In our Eloquent implementation, the Resource
class is optional. This is because we can use the information from the Eloquent schema to serialize a model to its JSON:API resource representation. However, this is not possible for a non-Eloquent resource, because we do not know how to get values from your non-Eloquent class. This means the Resource
class is compulsory.
The Repository
class allows our implementation to retrieve objects by their JSON:API resource id
. It is also used for validating any JSON:API resource identifiers that occur within JSON:API documents received from an API client.
In our Eloquent implementation, you do not need a repository class - because we have implemented a generic repository that works with any Eloquent model. However, this is not the case with non-Eloquent resources, as we do not know how to retrieve your non-Eloquent class using a JSON:API resource id.
The Repository
class is therefore compulsory if your non-Eloquent resource can be retrieved by its JSON:API resource id
. The only circumstance in which you will not have a repository will be if your non-Eloquent resource can never be retrieved by its id
, and never referenced in a JSON:API document by its resource identifier.
We will now create these three classes for our Site
entity.
Schema
To generate a schema for a non-Eloquent resource, using the jsonapi:schema
generator command with the --non-eloquent
flag. For example:
php artisan jsonapi:schema sites --non-eloquent --model "\App\Entities\Site"
This will generate the following schema, which we will add some attributes to:
namespace App\JsonApi\V1\Sites;
use App\Entities\Site;
use LaravelJsonApi\Core\Schema\Schema;
use LaravelJsonApi\NonEloquent\Fields\Attribute;
use LaravelJsonApi\NonEloquent\Fields\ID;
use LaravelJsonApi\NonEloquent\Fields\ToMany;
use LaravelJsonApi\NonEloquent\Fields\ToOne;
use LaravelJsonApi\NonEloquent\Filters\Filter;
class SiteSchema extends Schema
{
/**
* The model the schema corresponds to.
*
* @var string
*/
public static string $model = Site::class;
/**
* @inheritDoc
*/
public function fields(): iterable
{
return [
ID::make(),
Attribute::make('domain'),
Attribute::make('name'),
ToOne::make('owner'),
ToMany::make('tags'),
];
}
/**
* @inheritDoc
*/
public function filters(): iterable
{
return [
Filter::make('slugs'),
];
}
}
There are a few things to note about this class. Unlike the Eloquent schema, this schema extends LaravelJsonApi\Core\Schema\Schema
, which is our base abstract schema. Notice also that the fields use classes from the LaravelJsonApi\NonEloquent\Fields
namespace. These help you to define the JSON:API fields for a non-Eloquent resource. You can use the following fields:
ID
: Similar to the EloquentID
class, you can callmatchAs()
to set the ID pattern.Attribute
: A generic attribute that takes the JSON:API field name for the attribute.ToOne
: A generic relationship field for a to-one relationship.ToMany
: A generic relationship field for a to-many relationship.
We also use the LaravelJsonApi\NonEloquent\Filters\Filter
class to list any filters that our resource supports. Note that this class is very basic: it just lists the JSON:API filter key that is supported. You will need to implement filtering yourself, as shown in the Query All Capability later in this chapter.
Resource
Generate a Resource
class for your non-Eloquent resource using the jsonapi:resource
command:
php artisan jsonapi:resource sites
Our SiteResource
will look like this:
namespace App\JsonApi\V1\Sites;
use Illuminate\Http\Request;
use LaravelJsonApi\Core\Resources\JsonApiResource;
class SiteResource extends JsonApiResource
{
/**
* Get the resource id.
*
* @return string
*/
public function id(): string
{
return $this->resource->getSlug();
}
/**
* Get the resource's attributes.
*
* @param Request|null $request
* @return iterable
*/
public function attributes($request): iterable
{
return [
'domain' => $this->resource->getDomain(),
'name' => $this->resource->getName(),
];
}
/**
* Get the resource's relationships.
*
* @param Request|null $request
* @return iterable
*/
public function relationships($request): iterable
{
return [
$this->relation('owner')->withData($this->resource->getOwner()),
$this->relation('tags')->withData($this->resource->getTags()),
];
}
}
The only significant differences here are to do with the resource id
and the relationship data
member.
In the non-Eloquent resource shown above, we have implemented the id()
method. Unlike our Eloquent implementation, the resource class does not know how to read the id
value from the Site
class, so the id()
method must be implemented.
In the relationships, we have also specified the data
member of the relationship using the withData()
method. Again, this is omitted in Eloquent resources because the Eloquent implementation knows how to get the value from a model. However the non-Eloquent relationship does not know how to do this automatically, so the data must be manually set.
Repository
The final class we need to implement is our SiteRepository
. We provide an AbstractRepository
for you to extend, so you'll need to create the class that will look like this for our sites
resource:
namespace App\JsonApi\V1\Sites;
use App\Entities\SiteStorage;
use LaravelJsonApi\NonEloquent\AbstractRepository;
class SiteRepository extends AbstractRepository
{
/**
* @var SiteStorage
*/
private SiteStorage $storage;
/**
* SiteRepository constructor.
*
* @param SiteStorage $storage
*/
public function __construct(SiteStorage $storage)
{
$this->storage = $storage;
}
/**
* @inheritDoc
*/
public function find(string $resourceId): ?object
{
return $this->storage->find($resourceId);
}
}
This class is pretty simple because the AbstractRepository
implements the majority of the compulsory repository methods. All you need to implement is the find
method. This receives a JSON:API resource ID and is expected to return the object it represents (our Site
class in this example) or null
if the resource with that id does not exist.
For our Site
class we have used constructor dependency injection to inject the SiteStorage
class into our repository, so that we can retrieve the Site
object.
TIP
If your non-Eloquent resource is not retrievable by a resource id
, just return null
from the find()
method.
Once we have created our SiteRepository
, we need to register it by adding it to the repository()
method on our SiteSchema
:
class SiteSchema extends Schema
{
// ...other methods
public function repository(): SiteRepository
{
return SiteRepository::make()
->withServer($this->server)
->withSchema($this);
}
}
There are a few things to note here. Firstly, the static make()
method takes care of using the Laravel container to construct the class, allowing dependencies to be injected via the constructor. Secondly, we use the withServer()
and withSchema()
methods to inject the JSON:API server instance and the schema into the repository. This is because these may be needed by the Capabilities that we can add to a repository.
TIP
It is worth checking the implementation of the exists()
and findMany()
methods on the AbstractRepository
class. Both these methods are implemented by using the find
method. Depending on how you store and retrieve your non-Eloquent classes (e.g. the Site
class), you may be able to implement these methods in a more efficient way. If this is the case, overload the methods on your repository class.
Capabilities
Once we have completed the above steps, the following minimum capabilities have been implemented for our sites
resource:
- Retrieving a resource by its
id
, i.e.GET /api/v1/sites/{slug}
. - Retrieving resource relationship values, i.e.
GET /api/v1/sites/{slug}/{field}
andGET /api/v1/sites/{slug}/relationships/{field}
. - Parsing resource identifiers in JSON:API documents that have the
sites
resourcetype
.
This is because our implementation now understands how to retrieve a sites
resource via the SiteRepository::find()
method, and can retrieve the value of a relationship by reading it from the SiteResource
class.
All other capabilities, e.g. creating, updating and deleting a sites
resource, can be added to your SiteRepository
class. You only need to add the capabilities you want to support. For example, if a sites
resource can be created via your API you will need to add the create capability. However, if you do not need sites
resources to be deleted via the API, you never need to implement a delete capability.
TIP
Adding capabilities to your resource's repository allows you to use our generic JSON:API controller actions. It is also forward-compatible with our intent to support JSON:API Atomic Operations.
An alternative approach would be to write your own JSON:API controller actions for your non-Eloquent resource. If you take this approach, you do not need to use the capabilities listed in this chapter.
Query All Capability
This capability allows your resource to be retrieved on the index route, i.e. this request:
GET /api/v1/sites HTTP/1.1
Accept: application/vnd.api+json
To implement this capability, create a new class that extends the LaravelJsonApi\NonEloquent\Capabilities\QueryAll
class. For example:
namespace App\JsonApi\V1\Sites\Capabilities;
use App\Entities\SiteStorage;
use LaravelJsonApi\NonEloquent\Capabilities\QueryAll;
class QuerySites extends QueryAll
{
/**
* @var SiteStorage
*/
private SiteStorage $sites;
/**
* QuerySites constructor.
*
* @param SiteStorage $sites
*/
public function __construct(SiteStorage $sites)
{
parent::__construct();
$this->sites = $sites;
}
/**
* @inheritDoc
*/
public function get(): iterable
{
return $this->sites->all();
}
}
Once you have written this class, you must add it to your repository, by:
- Adding the
LaravelJsonApi\Contracts\Store\QueriesAll
interface to your repository; AND - Returning the capability from the
queryAll()
method.
For example, on our SiteRepository
:
namespace App\JsonApi\V1\Sites;
use LaravelJsonApi\Contracts\Store\QueriesAll;
use LaravelJsonApi\NonEloquent\AbstractRepository;
class SiteRepository extends AbstractRepository implements QueriesAll
{
// ...
/**
* @inheritDoc
*/
public function queryAll(): Capabilities\QuerySites
{
return Capabilities\QuerySites::make()
->withServer($this->server())
->withSchema($this->schema());
}
}
TIP
The make()
method on the capability class takes care of any constructor dependency injection, and we use the withServer()
and withSchema()
methods to inject the JSON:API server and schema in case these are needed in the capability.
Our QuerySites
capability implements the get()
method. At its most basic implementation, this returns all the resources that should be listed for an index request. In the above example, we return all the Site
objects from our SiteStorage
class.
You can use the get()
method to implement other features as needed. For example, our SiteSchema
had a slugs
filter on it. We could implement that in our QuerySites
class by updating the get()
method:
public function get(): iterable
{
$sites = collect($this->sites->all());
$filters = $this->queryParameters->filter();
if ($filters && is_array($slugs = $filters->value('slugs'))) {
$sites = $sites->filter(
fn(Site $site) => in_array($site->getSlug(), $slugs)
);
}
return $sites;
}
This would mean the following request would be supported:
GET /api/v1/sites?filter[slugs][]=foo&filter[slugs][]=bar HTTP/1.1
Accept: application/vnd.api+json
TIP
The QuerySites
capability is also where support for pagination is added, if needed. See the Pagination section for examples.
Resource Capabilities
If your resource can be created, updated and/or deleted, you will need to add a CRUD capability to your repository. This capability also allows you to customise reading a specific resource.
Writing the CRUD Capability
Firstly you will need to create a new class that extends the LaravelJsonApi\NonEloquent\Capabilities\CrudResource
capability class.
For example, we would add the following CrudSite
class for our sites
resource. We inject our SiteStorage
class using constructor dependency injection:
namespace App\JsonApi\V1\Sites\Capabilities;
use App\Entities\SiteStorage;
use LaravelJsonApi\NonEloquent\Capabilities\CrudResource;
class CrudSite extends CrudResource
{
/**
* @var SiteStorage
*/
private SiteStorage $storage;
/**
* CrudSite constructor.
*
* @param SiteStorage $storage
*/
public function __construct(SiteStorage $storage)
{
parent::__construct();
$this->storage = $storage;
}
}
This CrudSite
class is where we will implement the methods to create, update and delete a sites
resource. It is worth noting that we will only need to add the methods for the capabilities we want to support. For example, if our resource was not deletable in our API, then we would not need to add a delete method.
Once this class is created, you will need to add it to your repository, by:
- Adding the
LaravelJsonApi\NonEloquent\Concerns\HasCrudCapability
trait to your repository; AND - Returning your CRUD class from the
crud()
method.
For example, we would update our SiteRepository
as follows:
namespace App\JsonApi\V1\Sites;
use LaravelJsonApi\NonEloquent\AbstractRepository;
use LaravelJsonApi\NonEloquent\Concerns\HasCrudCapability;
class SiteRepository extends AbstractRepository
{
use HasCrudCapability;
// ...
/**
* @inheritDoc
*/
protected function crud(): Capabilities\CrudSite
{
return Capabilities\CrudSite::make();
}
}
TIP
As with other capabilities, the make()
method takes care of constructor dependency injection. You do not need to use the withServer()
or withSchema()
methods as our HasCrudCapability
trait takes care of injecting those dependencies for you.
Create Capability
This capability allows API clients to create resources, i.e. this request:
POST /api/v1/sites HTTP/1.1
Accept: application/vnd.api+json
Content-Type: application/vnd.api+json
{
//...
}
To add this capability we need to do two things:
- Add the
LaravelJsonApi\Contracts\Store\CreatesResources
interface to our repository; AND - Implement the
create()
method on ourCrudSite
capability class.
This is what our updated SiteRepository
would look like:
namespace App\JsonApi\V1\Sites;
use LaravelJsonApi\Contracts\Store\CreatesResources;
use LaravelJsonApi\NonEloquent\AbstractRepository;
use LaravelJsonApi\NonEloquent\Concerns\HasCrudCapability;
class SiteRepository extends AbstractRepository implements CreatesResources
{
use HasCrudCapability;
// ...
}
And we would add the create()
method to our CrudSite
capability class, for example:
namespace App\JsonApi\V1\Sites\Capabilities;
use App\Entities\Site;
use LaravelJsonApi\NonEloquent\Capabilities\CrudResource;
class CrudSite extends CrudResource
{
// ...
/**
* Create a new site.
*
* @param array $validatedData
* @return Site
*/
public function create(array $validatedData): Site
{
$owner = $this->toOne($validatedData['owner'] ?? null);
$tags = $this->toMany($validatedData['tags'] ?? []);
$site = new Site($validatedData['slug']);
$site->setDomain($validatedData['domain'] ?? null);
$site->setName($validatedData['name'] ?? null);
$site->setOwner($owner);
$site->setTags(...$tags);
$this->storage->store($site);
return $site;
}
}
The create()
method receives the validated data sent by the API client, and returns the created Site
object.
Note that if the validated data contains JSON:API resource identifiers for relationships, you can use the toOne()
and toMany()
methods to convert the identifiers to actual instances of the related classes. This makes it easy to set relationships when creating new resources - as shown in the CrudSite::create()
example above.
Read Capability
This capability is already implemented on the abstract repository class, and allows a resource to be retrieved using its resource id
, i.e. this request:
GET /api/v1/sites/<SLUG> HTTP/1.1
Accept: application/vnd.api+json
You only need to implement this capability yourself if you want to add additional features. For example, you might want to support filters, such as this request:
GET /api/v1/sites/<SLUG>?filter[name]=Test HTTP/1.1
Accept: application/vnd.api+json
In this example, the client is asking whether the site with the provided slug has a name of Test
. To support this, we will need to implement the read()
method on our CrudSite
capability class. (We do not need to add any interfaces to our repository, as the abstract repository already implements the relevant interface.)
Here is our example read()
method implemented on our CrudSite
class:
namespace App\JsonApi\V1\Sites\Capabilities;
use App\Entities\Site;
use Illuminate\Support\Str;
use LaravelJsonApi\NonEloquent\Capabilities\CrudResource;
class CrudSite extends CrudResource
{
// ...
/**
* Read the supplied site.
*
* @param Site $site
* @return Site|null
*/
public function read(Site $site): ?Site
{
$filters = $this->queryParameters->filter();
if ($filters && $name = $filters->value('name')) {
return Str::contains($site->getName(), $name) ? $site : null;
}
return $site;
}
}
The read()
method receives the Site
object that is subject of the request, and can return either the Site
or null
. In our example, we return null
if the name
filter does not match our Site
class. Otherwise we return the Site
object.
Update Capability
This capability allows API clients to update resources, i.e. this request:
PATCH /api/v1/sites/<SLUG> HTTP/1.1
Accept: application/vnd.api+json
Content-Type: application/vnd.api+json
{
//...
}
To add this capability we need to do two things:
- Add the
LaravelJsonApi\Contracts\Store\UpdatesResources
interface to our repository; AND - Implement the
update()
method on ourCrudSite
capability class.
This is what our updated SiteRepository
would look like:
namespace App\JsonApi\V1\Sites;
use LaravelJsonApi\Contracts\Store\UpdatesResources;
use LaravelJsonApi\NonEloquent\AbstractRepository;
use LaravelJsonApi\NonEloquent\Concerns\HasCrudCapability;
class SiteRepository extends AbstractRepository implements UpdatesResources
{
use HasCrudCapability;
// ...
}
And we would add the update()
method to our CrudSite
capability class, for example:
namespace App\JsonApi\V1\Sites\Capabilities;
use App\Entities\Site;
use LaravelJsonApi\NonEloquent\Capabilities\CrudResource;
class CrudSite extends CrudResource
{
// ...
/**
* Update the site.
*
* @param Site $site
* @param array $validatedData
* @return Site
*/
public function update(Site $site, array $validatedData): Site
{
if (array_key_exists('domain', $validatedData)) {
$site->setDomain($validatedData['domain']);
}
if (array_key_exists('name', $validatedData)) {
$site->setName($validatedData['name']);
}
if (array_key_exists('owner', $validatedData)) {
$site->setOwner($this->toOne($validatedData['owner']));
}
if (isset($validatedData['tags'])) {
$site->setTags(...$this->toMany($validatedData['tags']));
}
$this->storage->store($site);
return $site;
}
}
The update()
method receives the Site
being updated and the validated data sent by the API client.
Note that if the validated data contains JSON:API resource identifiers for relationships, you can use the toOne()
and toMany()
methods to convert the identifiers to actual instances of the related classes. This makes it easy to set relationships when update resources - as shown in the CrudSite::update()
example above.
Delete Capability
This capability allows API clients to delete resource, i.e. this request:
DELETE /api/v1/sites/<SLUG> HTTP/1.1
Accept: application/vnd.api+json
To add this capability we need to do two things:
- Add the
LaravelJsonApi\Contracts\Store\DeletesResources
interface to our repository; AND - Implement the
delete()
method on ourCrudSite
capability class.
This is what our updated SiteRepository
would look like:
namespace App\JsonApi\V1\Sites;
use LaravelJsonApi\Contracts\Store\DeletesResources;
use LaravelJsonApi\NonEloquent\AbstractRepository;
use LaravelJsonApi\NonEloquent\Concerns\HasCrudCapability;
class SiteRepository extends AbstractRepository implements DeletesResources
{
use HasCrudCapability;
// ...
}
And we would add the delete()
method to our CrudSite
capability class, for example:
namespace App\JsonApi\V1\Sites\Capabilities;
use App\Entities\Site;
use LaravelJsonApi\NonEloquent\Capabilities\CrudResource;
class CrudSite extends CrudResource
{
// ...
/**
* Delete the site.
*
* @param Site $site
* @return void
*/
public function delete(Site $site): void
{
$this->storage->remove($site);
}
}
The delete()
method receives the Site
being deleted, and removes it from storage.
Relationship Capabilities
If your resource allows relationships to be modified via relationship endpoints, you will need to add a CRUD Relations capability to your repository. This capability also allows you to customise reading a specific resource's relationships.
Writing the CRUD Relations Capability
Firstly you will need to create a new class that extends the LaravelJsonApi\NonEloquent\Capabilities\CrudRelations
capability class.
For example, we would add the following CrudSiteRelations
class for our sites
resource. We inject our SiteStorage
class using constructor dependency injection:
namespace App\JsonApi\Sites\Capabilities;
use App\Entities\SiteStorage;
use LaravelJsonApi\NonEloquent\Capabilities\CrudRelations;
class CrudSiteRelations extends CrudRelations
{
/**
* @var SiteStorage
*/
private SiteStorage $storage;
/**
* ModifySiteRelationships constructor.
*
* @param SiteStorage $storage
*/
public function __construct(SiteStorage $storage)
{
parent::__construct();
$this->storage = $storage;
}
}
The CrudSiteRelations
class is where we will implement the methods to read or set a to-one relation, and the methods to read, sync, attach or detach a to-many relation. It is worth noting that we only need to add the methods for the relationships and capabilities that we need to support.
Once this class is created, you will need to add it to your repository, by:
- Adding the
LaravelJsonApi\NonEloquent\Concerns\HasRelationsCapability
trait to your repository; AND - Returning the CRUD Relations class from the
relations()
method.
For example, we would update our SiteRepository
as follows:
namespace App\JsonApi\V1\Sites;
use LaravelJsonApi\NonEloquent\AbstractRepository;
use LaravelJsonApi\NonEloquent\Concerns\HasRelationsCapability;
class SiteRepository extends AbstractRepository
{
use HasRelationsCapability;
// ...
/**
* @inheritDoc
*/
protected function relations(): Capabilities\CrudSiteRelations
{
return Capabilities\CrudSiteRelations::make();
}
}
TIP
As with other capabilities, the make()
method takes care of constructor dependency injection. You do not need to use the withServer()
or withSchema()
methods as our HasRelationsCapability
trait takes care of injecting those dependencies for you.
Read Relations Capability
This capability is already implemented on the abstract repository, and allows the value of a resource's relationship to be retrieved by both of these requests:
GET /api/v1/sites/<SLUG>/<RELATION> HTTP/1.1
Accept: application/vnd.api+json
And
GET /api/v1/sites/<SLUG>/relationships/<RELATION> HTTP/1.1
Accept: application/vnd.api+json
You only need to implement this capability yourself if you want to add additional features. For example, you might want to allow a to-many relationship to be filtered.
If we wanted to do this for the tags
relationship of our sites
resource, we would add the getTags()
method on our CrudSiteRelations
capability class. (We do not need to add any interfaces to our repository, as the abstract repository already implements the relevant interface.)
Here is our example getTags()
method implemented on our CrudSiteRelations
class:
namespace App\JsonApi\Sites\Capabilities;
use App\Entities\Site;
use App\Entities\User;
use LaravelJsonApi\NonEloquent\Capabilities\CrudRelations;
class CrudSiteRelations extends CrudRelations
{
// ...
/**
* Get the tags relationship
*
* @param Site $site
* @return iterable
*/
public function getTags(Site $site): iterable
{
$tags = collect($site->getTags());
$filters = $this->queryParameters->filter();
if ($filters && $name = $filters->value('name')) {
$tags = $tags->filter(
fn(Tag $tag) => Str::contains($tag->getName(), $name)
);
}
return $tags;
}
}
Modify To-One Capability
This capability allows API clients to modify a resource's to-one relationships via relationship endpoints. For example:
PATCH /api/v1/sites/<SLUG>/relationships/owner HTTP/1.1
Accept: application/vnd.api+json
Content-Type: application/vnd.api+json
{
// ...
}
To add this capability we need to do two things:
- Add the
LaravelJsonApi\Contracts\Store\ModifiesToOne
interface to our repository; AND - Add the
set<Relation>
method to ourCrudSiteRelations
capability class.
This is what our updated SiteRepository
would look like:
namespace App\JsonApi\V1\Sites;
use LaravelJsonApi\Contracts\Store\ModifiesToOne;
use LaravelJsonApi\NonEloquent\AbstractRepository;
use LaravelJsonApi\NonEloquent\Concerns\HasRelationsCapability;
class SiteRepository extends AbstractRepository implements ModifiesToOne
{
use HasRelationsCapability;
// ...
}
And we would add the setOwner()
method to our CrudSiteRelations
capability class, for example:
namespace App\JsonApi\Sites\Capabilities;
use App\Entities\Site;
use App\Entities\User;
use LaravelJsonApi\NonEloquent\Capabilities\CrudRelations;
class CrudSiteRelations extends CrudRelations
{
// ...
/**
* Set the owner relationship.
*
* @param Site $site
* @param User|null $user
* @return void
*/
public function setOwner(Site $site, ?User $user): void
{
$site->setOwner($user);
$this->storage->store($site);
}
}
The set<Relation>
method receives the Site
class that the relationship is on, and the related class that was provided by the client or null
if an empty to-one relationship was provided.
Our example implementation modifies the relationship and then stores the updated Site
object.
Modify To-Many Capability
This capability allows API clients to modify a resource's to-many relationships via relationship endpoints. For example:
PATCH /api/v1/sites/<SLUG>/relationships/tags HTTP/1.1
Accept: application/vnd.api+json
Content-Type: application/vnd.api+json
{
// ...
}
To add this capability we need to do two things:
- Add the
LaravelJsonApi\Contracts\Store\ModifiesToMany
interface to our repository; AND - Add the
set<Relation>
,attach<Relation>
,detach<Relation>
methods to ourCrudSiteRelations
capability class.
This is what our updated SiteRepository
would look like:
namespace App\JsonApi\V1\Sites;
use LaravelJsonApi\Contracts\Store\ModifiesToMany;
use LaravelJsonApi\NonEloquent\AbstractRepository;
use LaravelJsonApi\NonEloquent\Concerns\HasRelationsCapability;
class SiteRepository extends AbstractRepository implements ModifiesToMany
{
use HasRelationsCapability;
// ...
}
And we would add the following methods to our CrudSiteRelations
capability class for the tags
relationship:
namespace App\JsonApi\Sites\Capabilities;
use App\Entities\Site;
use App\Entities\Tag;
use LaravelJsonApi\NonEloquent\Capabilities\CrudRelations;
class CrudSiteRelations extends CrudRelations
{
// ...
/**
* Set the tags relationship.
*
* @param Site $site
* @param array $tags
*/
public function setTags(Site $site, array $tags): void
{
$tags = collect($tags)->unique(
fn(Tag $tag) => $tag->getSlug()
);
$site->setTags(...$tags);
$this->storage->store($site);
}
/**
* Attach tags to the provided site.
*
* @param Site $site
* @param array $tags
*/
public function attachTags(Site $site, array $tags): void
{
$all = collect($site->getTags())
->merge($tags)
->unique(fn(Tag $tag) => $tag->getSlug());
$site->setTags(...$all);
$this->storage->store($site);
}
/**
* Detach tags from the provided site.
*
* @param Site $site
* @param array $tags
* @return void
*/
public function detachTags(Site $site, array $tags): void
{
$remove = collect($tags)
->map(fn(Tag $tag) => $tag->getSlug());
$all = collect($site->getTags())
->reject(fn(Tag $tag) => $remove->contains($tag->getSlug()))
->unique(fn(Tag $tag) => $tag->getSlug());
$site->setTags(...$all);
$this->storage->store($site);
}
}
The set<Relation>
methods receive the Site
class that the relationship is on, and an array of the related classes that the client specified in the request. In the above example, this is an array of Tag
classes.
Note that the setTags()
method completely replaces the entire relationship, while the attachTags()
and detachTags()
just attach and detach tags respectively. Note that the JSON:API specification says that a related resource can only appear in the relationship once, so you should ensure there are no duplicates when attaching and detaching related resources.
Pagination
If you want to support pagination, you can add this to your Query All capability. For ease of use, we provide an EnumerablePagination
implementation, that paginates your data using the Laravel's collection forPage()
method. However, you can also use your own pagination implementation if required.
Enumerable Pagination
This pagination uses Laravel's Enumerable::forPage()
method to paginate your resources. Effectively this works by loading all matching resources, then using the forPage()
method to reduce the collection to a specific page.
Firstly, you will need to return an instance of the LaravelJsonApi\NonEloquent\Pagination\EnumerablePagination
class from your schema's pagination
method. For example, on our SiteSchema
:
use LaravelJsonApi\Core\Schema\Schema;
use LaravelJsonApi\NonEloquent\Pagination\EnumerablePagination;
class SiteSchema extends Schema
{
// ...
/**
* @inheritDoc
*/
public function pagination(): EnumerablePagination
{
return EnumerablePagination::make();
}
}
The EnumerablePagination
class has the same methods as our Eloquent PagePagination
class, so you can refer to the documentation for that class for available methods. For example, if we wanted to customise the page keys:
EnumerablePagination::make()
->withPageKey('page')
->withPerPageKey('limit');
Once we've added the paginator to our schema, we then need to update our query-all class, making the following changes:
- Adding the
LaravelJsonApi\Contracts\Store\HasPagination
interface; AND - Adding the
LaravelJsonApi\NonEloquent\Concerns\PaginatesEnumerables
trait.
For example, on our QuerySites
class:
namespace App\JsonApi\Sites\Capabilities;
use App\Entities\Site;
use App\Entities\SiteStorage;
use LaravelJsonApi\Contracts\Store\HasPagination;
use LaravelJsonApi\NonEloquent\Capabilities\QueryAll;
use LaravelJsonApi\NonEloquent\Concerns\PaginatesEnumerables;
class QuerySites extends QueryAll implements HasPagination
{
use PaginatesEnumerables;
/**
* @var SiteStorage
*/
private SiteStorage $sites;
/**
* QuerySites constructor.
*
* @param SiteStorage $sites
*/
public function __construct(SiteStorage $sites)
{
parent::__construct();
$this->sites = $sites;
}
/**
* @inheritDoc
*/
public function get(): iterable
{
$sites = collect($this->sites->all());
$filters = $this->queryParameters->filter();
if ($filters && is_array($slugs = $filters->value('slugs'))) {
$sites = $sites->filter(
fn(Site $site) => in_array($site->getSlug(), $slugs)
);
}
return $sites->values();
}
}
Now that our capability is updated, our sites
resource now supports the following request:
GET /api/v1/sites?page[number]=1&page[size]=25
Accept: application/vnd.api+json
Under the hood, the PaginatesEnumerables
trait adds methods that use QuerySites::get()
to retrieve all of the sites, then the correct page is pulled out using Laravel's Enumerable::forPage()
method.
Custom Pagination
If you need to write your own pagination strategy for your non-Eloquent resource, you will need to write two classes:
- A paginator class, that implements the
LaravelJsonApi\Contracts\Pagination\Paginator
interface. This is the class you return from a schema'spagination()
method. - A page class, that extends the
LaravelJsonApi\Core\Pagination\AbstractPage
class.
We strongly recommend taking a look at our EnumerablePagination
and EnumerablePage
classes, to see how we have implemented the enumerable pagination capability. Those classes can be viewed here.
Once you have written those classes, you will need to return an instance of the pagination class from your schema's pagination()
method. (See the example SiteSchema::pagination()
method above, that returns an instance of the EnumerablePagination
class.)
Then you will need to update your query-all capability class, by:
- Adding the
LaravelJsonApi\Contracts\Store\HasPagination
interface; AND - Adding the
paginate()
andgetOrPaginate()
methods to your query-all capability.
For example:
namespace App\JsonApi\Sites\Capabilities;
use App\Entities\Site;
use App\Entities\SiteStorage;
use LaravelJsonApi\Contracts\Pagination\Page;
use LaravelJsonApi\Contracts\Store\HasPagination;
use LaravelJsonApi\NonEloquent\Capabilities\QueryAll;
class QuerySites extends QueryAll implements HasPagination
{
// ...
/**
* @inheritDoc
*/
public function get(): iterable
{
return $this->sites->all();
}
/**
* @inheritDoc
*/
public function paginate(array $page): Page
{
// an instance of your custom paginator...
$paginator = $this->schema()->pagination();
// ...return your custom page class using the settings from the paginator.
}
/**
* @inheritDoc
*/
public function getOrPaginate(?array $page): iterable
{
if (empty($page)) {
return $this->get();
}
return $this->paginate($page);
}
}
As shown in the above example, the paginate()
method needs to retrieve the paginator instance from the schema. It then uses the settings on that to return a page - the logic for doing that will be down to you to implement based on how your custom pagination works. Typically we implement a method on the paginator class to execute the pagination.
You also need to implement the getOrPaginate()
method. Typically you can copy and paste the example method above. This returns a page if the client has provided page parameters, otherwise it returns all the results using the query-all capability's get()
method.
Singular Filters
Typically a query-all capability will return zero-to-many resources. Singular filters are filters that change the result of the request to returning zero-to-one resource, i.e. the resource object or null
.
If you want to support singular filters on your non-Eloquent resource, you will need to:
- Add the
LaravelJsonApi\Contracts\Store\HasSingularFilters
to your query-all capability class; AND - Implement the
firstOrMany()
andfirstOrPaginate()
methods to your capability class.
In this example, we are going to add a slug
singular filter to our sites
resource. We will make these changes to the QuerySites
class as follows:
namespace App\JsonApi\Sites\Capabilities;
use App\Entities\Site;
use App\Entities\SiteStorage;
use LaravelJsonApi\Contracts\Store\HasPagination;
use LaravelJsonApi\Contracts\Store\HasSingularFilters;
use LaravelJsonApi\NonEloquent\Capabilities\QueryAll;
use LaravelJsonApi\NonEloquent\Concerns\PaginatesEnumerables;
class QuerySites extends QueryAll implements HasPagination, HasSingularFilters
{
use PaginatesEnumerables;
/**
* @var SiteStorage
*/
private SiteStorage $sites;
/**
* QuerySites constructor.
*
* @param SiteStorage $sites
*/
public function __construct(SiteStorage $sites)
{
parent::__construct();
$this->sites = $sites;
}
/**
* @inheritDoc
*/
public function get(): iterable
{
return $this->sites->all();
}
/**
* @inheritDoc
*/
public function firstOrMany()
{
$filters = $this->queryParameters->filter();
if ($filters && $filters->exists('slug')) {
return $this->sites->find(
$filters->value('slug')
);
}
return $this->get();
}
/**
* @inheritDoc
*/
public function firstOrPaginate(?array $page)
{
if (empty($page)) {
return $this->firstOrMany();
}
return $this->paginate($page);
}
}
As you can see from the example, we implement the singular filter in the firstOrMany()
method. This checks for the existence of the slug
filter, and if it exists, returns the matching Site
class via the SiteStorage::find()
method. Otherwise it returns the value from the get()
method - i.e. the list of resources.
You also need to implement the firstOrPaginate()
method. Typically you can copy and paste the example method above. This returns the firstOrMany()
method if the client has not provided page parameters; otherwise it returns the page from the paginate()
method. If your resource does not support pagination, the firstOrPaginate()
method should always return the result from the firstOrMany()
method.