Skip to content

Commit e8bf26e

Browse files
committed
Initial commit
0 parents  commit e8bf26e

40 files changed

+3234
-0
lines changed

Diff for: .gitattributes

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.gitattributes export-ignore
2+
.gitignore export-ignore
3+
.travis.yml export-ignore
4+
5+
phpunit.xml export-ignore
6+
tests export-ignore

Diff for: .gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
composer.lock
2+
vendor

Diff for: .travis.yml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
language: php
2+
3+
php:
4+
- 7.1
5+
- 7.2
6+
7+
install:
8+
- composer install --no-interaction --prefer-source
9+
10+
script:
11+
- vendor/bin/phpunit

Diff for: README.md

+373
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
# tobscure/json-api-server
2+
3+
[![Build Status](https://img.shields.io/travis/tobscure/json-api-server/master.svg?style=flat)](https://travis-ci.org/tobscure/json-api-server)
4+
[![Pre Release](https://img.shields.io/packagist/vpre/tobscure/json-api-server.svg?style=flat)](https://github.com/tobscure/json-api-server/releases)
5+
[![License](https://img.shields.io/packagist/l/tobscure/json-api-server.svg?style=flat)](https://packagist.org/packages/tobscure/json-api-server)
6+
7+
**A fully automated framework-agnostic [JSON:API](http://jsonapi.org) server implementation in PHP.**
8+
Define your schema, plug in your models, and we'll take care of the rest. 🍻
9+
10+
```bash
11+
composer require tobscure/json-api-server
12+
```
13+
14+
```php
15+
use Tobscure\JsonApiServer\Api;
16+
use Tobscure\JsonApiServer\Adapter\EloquentAdapter;
17+
use Tobscure\JsonApiServer\Schema\Builder;
18+
19+
$api = new Api('http://example.com/api');
20+
21+
$api->resource('articles', new EloquentAdapter(new Article), function (Builder $schema) {
22+
$schema->attribute('title');
23+
$schema->hasOne('author', 'people');
24+
$schema->hasMany('comments');
25+
});
26+
27+
$api->resource('people', new EloquentAdapter(new User), function (Builder $schema) {
28+
$schema->attribute('firstName');
29+
$schema->attribute('lastName');
30+
$schema->attribute('twitter');
31+
});
32+
33+
$api->resource('comments', new EloquentAdapter(new Comment), function (Builder $schema) {
34+
$schema->attribute('body');
35+
$schema->hasOne('author', 'people');
36+
});
37+
38+
/** @var Psr\Http\Message\ServerRequestInterface $request */
39+
/** @var Psr\Http\Message\Response $response */
40+
try {
41+
$response = $api->handle($request);
42+
} catch (Exception $e) {
43+
$response = $api->error($e);
44+
}
45+
```
46+
47+
Assuming you have a few [Eloquent](https://laravel.com/docs/5.7/eloquent) models set up, the above code will serve a **complete JSON:API that conforms to the [spec](https://jsonapi.org/format/)**, including support for:
48+
49+
- **Showing** individual resources (`GET /api/articles/1`)
50+
- **Listing** resource collections (`GET /api/articles`)
51+
- **Sorting**, **filtering**, **pagination**, and **sparse fieldsets**
52+
- **Compound documents** with inclusion of related resources
53+
- **Creating** resources (`POST /api/articles`)
54+
- **Updating** resources (`PATCH /api/articles/1`)
55+
- **Deleting** resources (`DELETE /api/articles/1`)
56+
- **Error handling**
57+
58+
The schema definition is extremely powerful and lets you easily apply [permissions](#visibility), [getters](#getters), [setters](#setters-savers), [validation](#validation), and custom [filtering](#filtering) and [sorting](#sorting) logic to build a fully functional API in minutes.
59+
60+
### Handling Requests
61+
62+
```php
63+
use Tobscure\JsonApiServer\Api;
64+
65+
$api = new Api('http://example.com/api');
66+
67+
try {
68+
$response = $api->handle($request);
69+
} catch (Exception $e) {
70+
$response = $api->error($e);
71+
}
72+
```
73+
74+
`Tobscure\JsonApiServer\Api` is a [PSR-15 Request Handler](https://www.php-fig.org/psr/psr-15/). Instantiate it with your API's base URL. Convert your framework's request object into a [PSR-7 Request](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) implementation, then let the `Api` handler take it from there. Catch any exceptions and give them back to `Api` if you want a JSON:API error response.
75+
76+
### Defining Resources
77+
78+
Define your API's resources using the `resource` method. The first argument is the [resource type](https://jsonapi.org/format/#document-resource-object-identification). The second is an implementation of `Tobscure\JsonApiServer\Adapter\AdapterInterface` which will allow the handler to interact with your models. The third is a closure in which you'll build the schema for your resource.
79+
80+
```php
81+
use Tobscure\JsonApiServer\Schema\Builder;
82+
83+
$api->resource('comments', $adapter, function (Builder $schema) {
84+
// define your schema
85+
});
86+
```
87+
88+
We provide an `EloquentAdapter` to hook your resources up with Laravel [Eloquent](https://laravel.com/docs/5.7/eloquent) models. Set it up with an instance of the model that your resource represents. You can [implement your own adapter](https://github.com/tobscure/json-api-server/blob/master/src/Adapter/AdapterInterface.php) if you use a different ORM.
89+
90+
```php
91+
use Tobscure\JsonApiServer\Adapter\EloquentAdapter;
92+
93+
$adapter = new EloquentAdapter(new User);
94+
```
95+
96+
### Attributes
97+
98+
Define an [attribute field](https://jsonapi.org/format/#document-resource-object-attributes) on your resource using the `attribute` method:
99+
100+
```php
101+
$schema->attribute('firstName');
102+
```
103+
104+
By default the attribute will correspond to the property on your model with the same name. (`EloquentAdapter` will `snake_case` it automatically for you.) If you'd like it to correspond to a different property, provide it as a second argument:
105+
106+
```php
107+
$schema->attribute('firstName', 'fname');
108+
```
109+
110+
### Relationships
111+
112+
Define [relationship fields](https://jsonapi.org/format/#document-resource-object-relationships) on your resource using the `hasOne` and `hasMany` methods:
113+
114+
```php
115+
$schema->hasOne('user');
116+
$schema->hasMany('comments');
117+
```
118+
119+
By default the [resource type](https://jsonapi.org/format/#document-resource-object-identification) that the relationship corresponds to will be derived from the relationship name. In the example above, the `user` relationship would correspond to the `users` resource type, while `comments` would correspond to `comments`. If you'd like to use a different resource type, provide it as a second argument:
120+
121+
```php
122+
$schema->hasOne('author', 'people');
123+
```
124+
125+
Like attributes, the relationship will automatically read and write to the relation on your model with the same name. If you'd like it to correspond to a different relation, provide it as a third argument.
126+
127+
Has-one relationships are available for [inclusion](https://jsonapi.org/format/#fetching-includes) via the `include` query parameter. You can include them by default, if the `include` query parameter is empty, by calling the `included` method:
128+
129+
```php
130+
$schema->hasOne('user')
131+
->included();
132+
```
133+
134+
Has-many relationships must be explicitly made available for inclusion via the `includable` method. This is because pagination of included resources is not supported, so performance may suffer if there are large numbers of related resources.
135+
136+
```php
137+
$schema->hasMany('comments')
138+
->includable();
139+
```
140+
141+
### Getters
142+
143+
Use the `get` method to define custom retrieval logic for your field, instead of just reading the value straight from the model property. (Of course, if you're using Eloquent, you could also define [casts](https://laravel.com/docs/5.7/eloquent-mutators#attribute-casting) or [accessors](https://laravel.com/docs/5.7/eloquent-mutators#defining-an-accessor) on your model to achieve a similar thing.)
144+
145+
```php
146+
$schema->attribute('firstName')
147+
->get(function ($model, $request) {
148+
return ucfirst($model->first_name);
149+
});
150+
```
151+
152+
### Visibility
153+
154+
You can specify logic to restrict the visibility of a field using any one of the `visible`, `visibleIf`, `hidden`, and `hiddenIf` methods:
155+
156+
```php
157+
$schema->attribute('email')
158+
// Make a field always visible (default)
159+
->visible()
160+
161+
// Make a field visible only if certain logic is met
162+
->visibleIf(function ($model, $request) {
163+
return $model->id == $request->getAttribute('userId');
164+
})
165+
166+
// Always hide a field (useful for write-only fields like password)
167+
->hidden()
168+
169+
// Hide a field only if certain logic is met
170+
->hiddenIf(function ($model, $request) {
171+
return $request->getAttribute('userIsSuspended');
172+
});
173+
```
174+
175+
You can also restrict the visibility of the whole resource using the `scope` method. This will allow you to modify the query builder object provided by your adapter:
176+
177+
```php
178+
$schema->scope(function ($query, $request) {
179+
$query->where('user_id', $request->getAttribute('userId'));
180+
});
181+
```
182+
183+
### Making Fields Writable
184+
185+
By default, fields are read-only. You can allow a field to be written to using any one of the `writable`, `writableIf`, `readonly`, and `readonlyIf` methods:
186+
187+
```php
188+
$schema->attribute('email')
189+
// Make an attribute writable
190+
->writable()
191+
192+
// Make an attribute writable only if certain logic is met
193+
->writableIf(function ($model, $request) {
194+
return $model->id == $request->getAttribute('userId');
195+
})
196+
197+
// Make an attribute read-only (default)
198+
->readonly()
199+
200+
// Make an attribute writable *unless* certain logic is met
201+
->readonlyIf(function ($model, $request) {
202+
return $request->getAttribute('userIsSuspended');
203+
});
204+
```
205+
206+
### Default Values
207+
208+
You can provide a default value for a field to be used when creating a new resource if there is no value provided by the consumer. Pass a value or a closure to the `default` method:
209+
210+
211+
```php
212+
$schema->attribute('joinedAt')
213+
->default(new DateTime);
214+
215+
$schema->attribute('ipAddress')
216+
->default(function ($request) {
217+
return $request->getServerParams()['REMOTE_ADDR'] ?? null;
218+
});
219+
```
220+
221+
If you're using Eloquent, you could also define [default attribute values](https://laravel.com/docs/5.7/eloquent#default-attribute-values) to achieve a similar thing, although you wouldn't have access to the request object.
222+
223+
### Validation
224+
225+
You can ensure that data provided for a field is valid before it is saved. Provide a closure to the `validate` method, and call the first argument if validation fails:
226+
227+
```php
228+
$schema->attribute('email')
229+
->validate(function ($fail, $email) {
230+
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
231+
$fail('Invalid email');
232+
}
233+
});
234+
235+
$schema->hasMany('groups')
236+
->validate(function ($fail, $groups) {
237+
foreach ($groups as $group) {
238+
if ($group->id === 1) {
239+
$fail('You cannot assign this group');
240+
}
241+
}
242+
});
243+
```
244+
245+
See [Macros](#macros) below to learn how to use Laravel's [Validation](https://laravel.com/docs/5.7/validation) component in your schema.
246+
247+
### Setters & Savers
248+
249+
Use the `set` method to define custom mutation logic for your field, instead of just setting the value straight on the model property. (Of course, if you're using Eloquent, you could also define [casts](https://laravel.com/docs/5.7/eloquent-mutators#attribute-casting) or [mutators](https://laravel.com/docs/5.7/eloquent-mutators#defining-a-mutator) on your model to achieve a similar thing.)
250+
251+
```php
252+
$schema->attribute('firstName')
253+
->set(function ($model, $value, $request) {
254+
return $model->first_name = strtolower($value);
255+
});
256+
```
257+
258+
If your attribute corresponds to some other form of data storage rather than a simple property on your model, you can use the `save` method to provide a closure to be run _after_ your model is saved:
259+
260+
```php
261+
$schema->attribute('locale')
262+
->save(function ($model, $value, $request) {
263+
$model->preferences()->update(['value' => $value])->where('key', 'locale');
264+
});
265+
```
266+
267+
### Filtering
268+
269+
You can define a field as `filterable` to allow the resource index to be [filtered](https://jsonapi.org/recommendations/#filtering) by the field's value. This works for both attributes and relationships:
270+
271+
```php
272+
$schema->attribute('firstName')
273+
->filterable();
274+
275+
$schema->hasMany('groups')
276+
->filterable();
277+
278+
// e.g. GET /api/users?filter[firstName]=Toby&filter[groups]=1,2,3
279+
```
280+
281+
You can optionally pass a closure to customize how the filter is applied to the query builder object provided by your adapter:
282+
283+
```php
284+
$schema->attribute('minPosts')
285+
->hidden()
286+
->filterable(function ($query, $value, $request) {
287+
$query->where('postCount', '>=', $value);
288+
});
289+
```
290+
291+
### Sorting
292+
293+
You can define an attribute as `sortable` to allow the resource index to be [sorted](https://jsonapi.org/format/#fetching-sorting) by the attribute's value:
294+
295+
```php
296+
$schema->attribute('firstName')
297+
->sortable();
298+
299+
$schema->attribute('lastName')
300+
->sortable();
301+
302+
// e.g. GET /api/users?sort=lastName,firstName
303+
```
304+
305+
### Pagination
306+
307+
By default, resources are automatically [paginated](https://jsonapi.org/format/#fetching-pagination) with 20 records per page. You can change this limit using the `paginate` method on the schema builder:
308+
309+
```php
310+
$schema->paginate(50);
311+
```
312+
313+
### Creating Resources
314+
315+
By default, resources are not [creatable](https://jsonapi.org/format/#crud-creating) (i.e. `POST` requests will return `403 Forbidden`). You can allow them to be created using the `creatable`, `creatableIf`, `notCreatable`, and `notCreatableIf` methods on the schema builder:
316+
317+
```php
318+
$schema->creatableIf(function ($request) {
319+
return $request->getAttribute('isAdmin');
320+
});
321+
```
322+
323+
### Deleting Resources
324+
325+
By default, resources are not [deletable](https://jsonapi.org/format/#crud-deleting) (i.e. `DELETE` requests will return `403 Forbidden`). You can allow them to be deleted using the `deletable`, `deletableIf`, `notDeletable`, and `notDeletableIf` methods on the schema builder:
326+
327+
```php
328+
$schema->deletableIf(function ($request) {
329+
return $request->getAttribute('isAdmin');
330+
});
331+
```
332+
333+
### Macros
334+
335+
You can define macros on the `Tobscure\JsonApiServer\Schema\Attribute` class to aid construction of your API schema. Below is an example that sets up a `rules` macro which will add a validator to validate the attribute value using Laravel's [Validation](https://laravel.com/docs/5.7/validation) component:
336+
337+
```php
338+
use Tobscure\JsonApiServer\Schema\Attribute;
339+
340+
Attribute::macro('rules', function ($rules) use ($validator) {
341+
$this->validate(function ($fail, $value) use ($validator, $rules) {
342+
$key = $this->name;
343+
$validation = Validator::make([$key => $value], [$key => $rules]);
344+
345+
if ($validation->fails()) {
346+
$fail((string) $validation->messages());
347+
}
348+
});
349+
});
350+
```
351+
352+
```php
353+
$schema->attribute('username')
354+
->rules(['required', 'min:3', 'max:30']);
355+
```
356+
357+
## Examples
358+
359+
- [Flarum](https://github.com/flarum/core/tree/master/src/Api) is forum software that uses tobscure/json-api-server to power its API.
360+
361+
## Contributing
362+
363+
Feel free to send pull requests or create issues if you come across problems or have great ideas. See the [Contributing Guide](https://github.com/tobscure/json-api-server/blob/master/CONTRIBUTING.md) for more information.
364+
365+
### Running Tests
366+
367+
```bash
368+
$ vendor/bin/phpunit
369+
```
370+
371+
## License
372+
373+
This code is published under the [The MIT License](LICENSE). This means you can do almost anything with it, as long as the copyright notice and the accompanying license file is left intact.

0 commit comments

Comments
 (0)