Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[6.x] REST API support #356

Merged
merged 30 commits into from
Nov 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8dbfd65
Initial rest api support
ryanmitchell Nov 3, 2023
bc891ad
Ensure primary key is returned always
ryanmitchell Nov 3, 2023
3e334a3
:beer:
ryanmitchell Nov 3, 2023
1426ec4
Pluralize ie (api/runway/users versus api/runway/user)
ryanmitchell Nov 4, 2023
5bcdf24
Only define routes once as they are generic
ryanmitchell Nov 4, 2023
30835ca
Simplify route names
ryanmitchell Nov 4, 2023
e7d1b0e
Ensure filters work
ryanmitchell Nov 4, 2023
0a2c781
Merge branch '6.x' into pr/356
duncanmcclean Nov 6, 2023
3b75d51
Sync with v6 changes
ryanmitchell Nov 6, 2023
61c9301
:beer:
ryanmitchell Nov 6, 2023
4c58c25
Use correct key
ryanmitchell Nov 6, 2023
0bb3bf2
Change of approach
ryanmitchell Nov 6, 2023
5a00f76
Use toAugmentedArray
ryanmitchell Nov 6, 2023
340c7be
Work around wrapValue issues
ryanmitchell Nov 6, 2023
f2a34ec
:beer:
ryanmitchell Nov 6, 2023
c37259f
Add test coverage
ryanmitchell Nov 7, 2023
fde70b8
:beer:
ryanmitchell Nov 7, 2023
ce3ce00
nitpick
duncanmcclean Nov 11, 2023
e677354
rename controller
duncanmcclean Nov 11, 2023
6311d3d
Refactor tests to use JSON assertions
duncanmcclean Nov 11, 2023
42f2d55
Rename bootRestApi to bootApi
duncanmcclean Nov 11, 2023
4617c6f
Rename route parameters to stay consistent with elsewhere in the code…
duncanmcclean Nov 11, 2023
fdc4878
nitpick
duncanmcclean Nov 11, 2023
ce2f392
add test to cover nested fields
duncanmcclean Nov 11, 2023
5fef599
Add test to cover relationship field
duncanmcclean Nov 11, 2023
490f07b
Shallowly augment relationship fields
duncanmcclean Nov 11, 2023
35f978a
wip
duncanmcclean Nov 11, 2023
514581d
Make nested fields work in the API
duncanmcclean Nov 11, 2023
f5252b7
Pass the blueprint fields as a Collection & re-use them for relations…
duncanmcclean Nov 11, 2023
134206c
Add documentation
duncanmcclean Nov 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -161,6 +166,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
Loading