Skip to content

Commit

Permalink
[6.x] REST API support (#356)
Browse files Browse the repository at this point in the history
* Initial rest api support

* Ensure primary key is returned always

* 🍺

* Pluralize ie (api/runway/users versus api/runway/user)

* Only define routes once as they are generic

* Simplify route names

* Ensure filters work

* Sync with v6 changes

* 🍺

* Use correct key

* Change of approach

* Use toAugmentedArray

* Work around wrapValue issues

* 🍺

* Add test coverage

* 🍺

* nitpick

* rename controller

* Refactor tests to use JSON assertions

* Rename bootRestApi to bootApi

* Rename route parameters to stay consistent with elsewhere in the codebase

* nitpick

* add test to cover nested fields

* Add test to cover relationship field

* Shallowly augment relationship fields

This commit reverts the changes made in 5a00f76 to make nested fields work.

* wip

* Make nested fields work in the API

* Pass the blueprint fields as a Collection & re-use them for relationship fields check

* Add documentation

---------

Co-authored-by: Duncan McClean <duncan@duncanmcclean.com>
  • Loading branch information
ryanmitchell and duncanmcclean authored Nov 11, 2023
1 parent 1815487 commit aff1fd4
Show file tree
Hide file tree
Showing 8 changed files with 385 additions and 0 deletions.
58 changes: 58 additions & 0 deletions docs/rest-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: REST API
---

Statamic comes with a read-only API that allows you to deliver content from Statamic to your frontend, external apps, SPA, or any other desired location. Content is delivered RESTfully as JSON data.

Runway includes support for Statamic's Content API, which enables you to access your Eloquent models.

If you prefer, Runway also supports [GraphQL](/graphql).

## Enabling for resources

If you haven't already done so, you'll need to enable Statamic's REST API. You can do this by adding the following line to your `.env` file:

```
STATAMIC_API_ENABLED=true
```

Alternatively, you can enable it for all environments in `config/statamic/api.php`:

```php
'enabled' => true,
```

Next, you'll need to enable the resources you want to make available. To do this, simply add a `runway` key to your `resources` array in `config/statamic/api.php`, and provide the handles of the resources for which you wish to enable the API:

```php
'resources' => [
'collections' => true,
// ...
'runway' => [
'product' => true,
],
],
```

## Endpoints

Each resource will have two endpoints:

* `/api/runway/{resourceHandle}` for retrieving models associated with a resource.
* `/api/runway/{resourceHandle}/{id}` for retrieving a specific model.

## Filtering

To enable filtering for your resources, you'll need to opt in by defining a list of `allowed_filters` for each resource in your `config/statamic/api.php` configuration file:

```php
'runway' => [
'product' => [
'allowed_filters' => ['name', 'slug'],
],
],
```

## More Information

For more information on Statamic's REST API functionality, please refer to the [Statamic Documentation](https://statamic.dev/rest-api#entries).
10 changes: 10 additions & 0 deletions src/Data/AugmentedModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Statamic\Data\AbstractAugmented;
use Statamic\Fields\Field;
use Statamic\Fields\Value;
use Statamic\Statamic;

class AugmentedModel extends AbstractAugmented
{
Expand Down Expand Up @@ -60,6 +61,15 @@ public function url(): ?string
: null;
}

public function apiUrl()
{
if (! $id = $this->data->{$this->resource->primaryKey()}) {
return null;
}

return Statamic::apiRoute('runway.show', [$this->resource->handle(), $id]);
}

protected function modelAttributes(): Collection
{
return collect($this->data->getAttributes());
Expand Down
75 changes: 75 additions & 0 deletions src/Http/Controllers/ApiController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace DoubleThreeDigital\Runway\Http\Controllers;

use DoubleThreeDigital\Runway\Http\Resources\ApiResource;
use DoubleThreeDigital\Runway\Resource;
use DoubleThreeDigital\Runway\Runway;
use Facades\Statamic\API\FilterAuthorizer;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Statamic\Exceptions\NotFoundHttpException;
use Statamic\Http\Controllers\API\ApiController as StatamicApiController;

class ApiController extends StatamicApiController
{
private $config;

protected $resourceConfigKey = 'runway';

protected $routeResourceKey = 'resourceHandle';

protected $resourceHandle;

public function index($resourceHandle)
{
$this->abortIfDisabled();

$this->resourceHandle = Str::singular($resourceHandle);

$resource = Runway::findResource($this->resourceHandle);

if (! $resource) {
throw new NotFoundHttpException;
}

$results = $this->filterSortAndPaginate($resource->model()->query());

$results = ApiResource::collection($results);

$results->setCollection(
$results->getCollection()->transform(fn ($result) => $result->withBlueprintFields($this->getFieldsFromBlueprint($resource)))
);

return $results;
}

public function show($resourceHandle, $record)
{
$this->abortIfDisabled();

$this->resourceHandle = Str::singular($resourceHandle);

$resource = Runway::findResource($this->resourceHandle);

if (! $resource) {
throw new NotFoundHttpException;
}

if (! $model = $resource->model()->find($record)) {
throw new NotFoundHttpException;
}

return ApiResource::make($model)->withBlueprintFields($this->getFieldsFromBlueprint($resource));
}

protected function allowedFilters()
{
return FilterAuthorizer::allowedForSubResources('api', $this->resourceConfigKey, Str::plural($this->resourceHandle));
}

private function getFieldsFromBlueprint(Resource $resource): Collection
{
return $resource->blueprint()->fields()->all();
}
}
50 changes: 50 additions & 0 deletions src/Http/Resources/ApiResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace DoubleThreeDigital\Runway\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

class ApiResource extends JsonResource
{
public $blueprintFields;

/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request
* @return array
*/
public function toArray($request)
{
$augmentedArray = $this->resource
->toAugmentedCollection($this->blueprintFields->map->handle()->all() ?? [])
->withRelations($this->blueprintFields->filter->isRelationship()->keys()->all())
->withShallowNesting()
->toArray();

collect($augmentedArray)
->filter(fn ($value, $key) => Str::contains($key, '->'))
->each(function ($value, $key) use (&$augmentedArray) {
$augmentedArray[Str::before($key, '->')][Str::after($key, '->')] = $value;
unset($augmentedArray[$key]);
});

return array_merge($augmentedArray, [
$this->resource->getKeyName() => $this->resource->getKey(),
]);
}

/**
* Set the fields that should be returned by this resource
*
* @return self
*/
public function withBlueprintFields(Collection $fields)
{
$this->blueprintFields = $fields;

return $this;
}
}
24 changes: 24 additions & 0 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@

namespace DoubleThreeDigital\Runway;

use DoubleThreeDigital\Runway\Http\Controllers\ApiController;
use DoubleThreeDigital\Runway\Policies\ResourcePolicy;
use DoubleThreeDigital\Runway\Search\Provider as SearchProvider;
use DoubleThreeDigital\Runway\Search\Searchable;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Traits\Conditionable;
use Statamic\API\Middleware\Cache;
use Statamic\Facades\CP\Nav;
use Statamic\Facades\GraphQL;
use Statamic\Facades\Permission;
use Statamic\Facades\Search;
use Statamic\Http\Middleware\API\SwapExceptionHandler as SwapAPIExceptionHandler;
use Statamic\Http\Middleware\RequireStatamicPro;
use Statamic\Providers\AddonServiceProvider;
use Statamic\Statamic;

Expand Down Expand Up @@ -82,6 +86,7 @@ public function boot()
$this->registerPolicies();
$this->registerNavigation();
$this->bootGraphQl();
$this->bootApi();

SearchProvider::register();
$this->bootModelEventListeners();
Expand Down Expand Up @@ -165,6 +170,25 @@ protected function bootGraphQl()
});
}

protected function bootApi()
{
if (config('statamic.api.enabled')) {
Route::middleware([
SwapApiExceptionHandler::class,
RequireStatamicPro::class,
Cache::class,
])->group(function () {
Route::middleware(config('statamic.api.middleware'))
->name('statamic.api.')
->prefix(config('statamic.api.route'))
->group(function () {
Route::name('runway.index')->get('runway/{resourceHandle}', [ApiController::class, 'index']);
Route::name('runway.show')->get('runway/{resourceHandle}/{record}', [ApiController::class, 'show']);
});
});
}
}

protected function bootModelEventListeners()
{
Runway::allResources()
Expand Down
5 changes: 5 additions & 0 deletions src/Traits/HasRunwayResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public function newAugmentedInstance(): Augmented
return new AugmentedModel($this);
}

public function shallowAugmentedArrayKeys()
{
return [$this->runwayResource()->primaryKey(), $this->runwayResource()->titleField(), 'api_url'];
}

public function runwayResource(): Resource
{
return Runway::findResourceByModel($this);
Expand Down
Loading

0 comments on commit aff1fd4

Please sign in to comment.