Skip to content

Commit

Permalink
Merge pull request #7 from timacdonald/relationship-helper
Browse files Browse the repository at this point in the history
Just a bunch of mooooare stuff
  • Loading branch information
timacdonald authored Feb 15, 2022
2 parents c3b872c + 0e4fad5 commit b9c212c
Show file tree
Hide file tree
Showing 26 changed files with 950 additions and 199 deletions.
30 changes: 20 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

# JSON:API Resource for Laravel

A lightweight JSON Resource for Laravel that helps you adhere to the JSON:API standards and also implements features such as sparse fieldsets and compound documents, whilst allowing you to extend the spec as needed for your project.
A lightweight JSON Resource for Laravel that helps you adhere to the JSON:API standard and also implements features such as sparse fieldsets and compound documents.

These docs are not designed to introduce you to the JSON:API spec and the associated concepts, instead you should [head over and read the spec](https:/jsonapi.org) if you are not familiar with it.
These docs are not designed to introduce you to the JSON:API spec and the associated concepts, instead you should [head over and read the spec](https:/jsonapi.org) if you are not familiar with it. The documentation that follows only contains information on _how_ to implement the specification via the package.

# Version support

Expand Down Expand Up @@ -96,6 +96,9 @@ class UserResource extends JsonApiResource
'posts' => fn () => PostResource::collection($this->posts),
'subscription' => fn () => SubscriptionResource::make($this->subscription),
'profileImage' => fn () => optional($this->profileImage, fn (ProfileImage $profileImage) => ProfileImageResource::make($profileImage)),
// if the relationship has been loaded and is null, can we not just return the resource still and have a nice default? That way you never have to handle any of this
// optional noise?
// also is there a usecase for returning a resource linkage right from here and not a full resource?
];
}
}
Expand Down Expand Up @@ -131,12 +134,15 @@ To provide links for a resource, you can implement the `toLinks(Request $request
```php
<?php

use TiMacDonald\JsonApi\Link;

class UserResource extends JsonApiResource
{
protected function toLinks(Request $request): array
{
return [
'self' => route('users.show', $this->resource),
Link::self(route('users.show', $this->resource)),
'related' => 'https://example.com/related'
];
}
}
Expand Down Expand Up @@ -330,21 +336,16 @@ Relationships can be resolved deeply and also multiple relationship paths can be
## Credits

- [Tim MacDonald](https://github.com/timacdonald)
- [Jess Archer](https://github.com/jessarcher) for co-creating our initial in-house version and the brainstorming
- [All Contributors](../../contributors)

And a special (vegi) thanks to [Caneco](https://twitter.com/caneco) for the logo ✨

# Coming soon...

- [ ] Top level links, jsonapi, etc.
- [ ] Test assertions?
- [ ] decide how to handle top level keys for single and collections (static? should collections have to be extended to specify the values? or can there be static methods on the single resource for the collection?)
- [ ] Handle loading relations on a already in memory object with Spatie Query builder (PR)
- [ ] Resource identifier links and meta as a new concept different to normal resource links and relationships.
- [ ] Ability to send the resource identifier "id" and "type" for a belongsTo relationship, even if not included?
- [ ] Helper to define links
- [ ] Investigate collection count support
- [ ] Transducers for all the looping?
- [ ] a contract that other classes can implement to support the JSON:API spec as relationships? Can we have it work at a top level as well? Would that even make sense? Maybe be providing a toResponse implementation?

# To document

Expand All @@ -356,3 +357,12 @@ And a special (vegi) thanks to [Caneco](https://twitter.com/caneco) for the logo
- [ ] caching id and type
- [ ] caching includes and fields
- [ ] how it clears itself on toResponse
- [ ] that the goal is to have a consistent output at all levels, hence the maximal dataset for empty values
- [ ] Link object and meta

# Not yet supported
- [ ] Top level links & meta - how would you modify this for a collection? Top level links need to merge with pagination links
- [ ] decide how to handle top level keys for single and collections (static? should collections have to be extended to specify the values? or can there be static methods on the single resource for the collection?)
- [ ] returning a resource as `null` as the Laravel resource does not support this. Is possible to support locally, but it might be unexpected. Perhaps a PR to Laravel is best?
- [ ] Responses that contain only resource identifiers (related)
- [ ] `400` when requesting relationships that are not present.
4 changes: 2 additions & 2 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
parameters:
level: 8
level: max
paths:
- src
checkInternalClassCaseSensitivity: true
checkTooWideReturnTypesInProtectedAndPublicMethods: true
checkUninitializedProperties: true
checkMissingIterableValueType: false
checkMissingIterableValueType: true
includes:
- vendor-bin/linting/vendor/nunomaduro/larastan/extension.neon
22 changes: 12 additions & 10 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor-bin/testing/vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>

<coverage>
<include><directory suffix=".php">src</directory></include>
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>

<php>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="DB_CONNECTION" value="sqlite" />
<env name="DB_DATABASE" value=":memory:" />
</php>
</phpunit>
18 changes: 1 addition & 17 deletions src/Concerns/Attributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static function maximalAttributes(): void
private function requestedAttributes(Request $request): Collection
{
return Collection::make($this->toAttributes($request))
->only($this->fields($request))
->only(Fields::getInstance()->parse($request, $this->toType($request), self::$minimalAttributes))
->map(
/**
* @param mixed $value
Expand All @@ -49,20 +49,4 @@ private function requestedAttributes(Request $request): Collection
fn ($value): bool => $value instanceof PotentiallyMissing && $value->isMissing()
);
}

/**
* @internal
*/
private function fields(Request $request): ?array
{
$fields = Fields::getInstance()->parse($request, $this->toType($request));

if ($fields !== null) {
return $fields;
}

return self::$minimalAttributes
? []
: null;
}
}
87 changes: 87 additions & 0 deletions src/Concerns/Caching.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace TiMacDonald\JsonApi\Concerns;

use Closure;
use Illuminate\Support\Collection;
use TiMacDonald\JsonApi\JsonApiResource;
use TiMacDonald\JsonApi\JsonApiResourceCollection;
use TiMacDonald\JsonApi\Support\UnknownRelationship;

trait Caching
{
/**
* @internal
*/
private ?string $idCache = null;

/**
* @internal
*/
private ?string $typeCache = null;

/**
* @internal
*/
private ?Collection $requestedRelationshipsCache = null;

/**
* @internal
* @infection-ignore-all
*/
public function flush(): void
{
$this->idCache = null;

$this->typeCache = null;

if ($this->requestedRelationshipsCache !== null) {
$this->requestedRelationshipsCache->each(
/**
* @param JsonApiResource|JsonApiResourceCollection|UnknownRelationship $resource
*/
fn ($resource) => $resource->flush()
);
}

$this->requestedRelationshipsCache = null;
}

/**
* @internal
* @infection-ignore-all
*/
private function rememberId(Closure $closure): string
{
return $this->idCache ??= $closure();
}


/**
* @internal
* @infection-ignore-all
*/
private function rememberType(Closure $closure): string
{
return $this->typeCache ??= $closure();
}

/**
* @internal
* @infection-ignore-all
*/
private function rememberRequestRelationships(Closure $closure): Collection
{
return $this->requestedRelationshipsCache ??= $closure();
}

/**
* @internal
*/
public function requestedRelationshipsCache(): ?Collection
{
return $this->requestedRelationshipsCache;
}
}
47 changes: 5 additions & 42 deletions src/Concerns/Identification.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,6 @@ trait Identification
*/
private static ?Closure $typeResolver;

/**
* @internal
*/
private ?string $idCache = null;

/**
* @internal
*/
private ?string $typeCache = null;

/**
* @internal
*/
Expand All @@ -51,25 +41,12 @@ public static function resolveTypeNormally(): void
self::$typeResolver = null;
}

/**
* @internal
*/
public function toResourceIdentifier(Request $request): array
{
return [
'data' => [
'id' => $this->resolveId($request),
'type' => $this->resolveType($request),
],
];
}

/**
* @internal
*/
public function toUniqueResourceIdentifier(Request $request): string
{
return "type:{$this->resolveType($request)} id:{$this->resolveId($request)}";
return "type:{$this->resolveType($request)};id:{$this->resolveId($request)};";
}

/**
Expand All @@ -88,24 +65,6 @@ private function resolveType(Request $request): string
return $this->rememberType(fn (): string => $this->toType($request));
}

/**
* @internal
* @infection-ignore-all
*/
private function rememberType(Closure $closure): string
{
return $this->typeCache ??= $closure();
}

/**
* @internal
* @infection-ignore-all
*/
private function rememberId(Closure $closure): string
{
return $this->idCache ??= $closure();
}

/**
* @internal
*/
Expand All @@ -116,6 +75,10 @@ private static function idResolver(): Closure
throw ResourceIdentificationException::attemptingToDetermineIdFor($resource);
}

/**
* @see https://github.com/timacdonald/json-api#customising-the-resource-id
* @phpstan-ignore-next-line
*/
return (string) $resource->getKey();
};
}
Expand Down
35 changes: 35 additions & 0 deletions src/Concerns/Implementation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace TiMacDonald\JsonApi\Concerns;

use Closure;
use TiMacDonald\JsonApi\JsonApiServerImplementation as ServerImplementation;

/**
* @internal
*/
trait Implementation
{
/**
* @internal
*/
private static ?Closure $serverImplementationResolver = null;

/**
* @internal
*/
public static function resolveServerImplementationNormally(): void
{
self::$serverImplementationResolver = null;
}

/**
* @internal
*/
public static function serverImplementationResolver(): Closure
{
return self::$serverImplementationResolver ?? fn (): ServerImplementation => new ServerImplementation('1.0');
}
}
34 changes: 34 additions & 0 deletions src/Concerns/Links.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace TiMacDonald\JsonApi\Concerns;

use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use TiMacDonald\JsonApi\Link;

/**
* @internal
*/
trait Links
{
/**
* @internal
* @return array<string, Link>
*/
private function resolveLinks(Request $request): array
{
return Collection::make($this->toLinks($request))
->mapWithKeys(
/**
* @param string|Link $value
* @param string|int $key
*/
fn ($value, $key): array => $value instanceof Link
? [$value->key() => $value]
: [$key => new Link($value)]
)
->all();
}
}
Loading

0 comments on commit b9c212c

Please sign in to comment.