Skip to content

Commit

Permalink
feat(Http): Add support for setting container to collection of resources
Browse files Browse the repository at this point in the history
BREAKING CHANGE: If you pass container and do not use JsonResource exception will be thrown.
  • Loading branch information
pionl committed Jun 30, 2023
1 parent 780f59a commit e3bf3ec
Show file tree
Hide file tree
Showing 11 changed files with 419 additions and 23 deletions.
49 changes: 32 additions & 17 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
parameters:
ignoreErrors:
-
message: "#^Cannot cast array\\|string to string\\.$#"
count: 1
path: src/Testing/Actions/GetNamespaceForStubsAction.php
ignoreErrors:
-
message: "#^Access to an undefined property LaraStrict\\\\Http\\\\Resources\\\\JsonResource\\:\\:\\$preserveKeys\\.$#"
count: 1
path: src/Http/Resources/JsonResource.php

-
message: "#^Calling PHPStan\\\\PhpDoc\\\\PhpDocStringResolver\\:\\:resolve\\(\\) is not covered by backward compatibility promise\\. The method might change in a minor PHPStan version\\.$#"
count: 1
path: src/Testing/Actions/ParsePhpDocAction.php
-
message: "#^Unsafe usage of new static\\(\\)\\.$#"
count: 1
path: src/Http/Resources/JsonResource.php

-
message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertTrue\\(\\) with false and 'Hook should be…' will always evaluate to false\\.$#"
count: 1
path: tests/Feature/Testing/Commands/MakeExpectationCommandRealTest.php
-
message: "#^Method LaraStrict\\\\Http\\\\Resources\\\\JsonResourceCollection\\:\\:toArray\\(\\) return type with generic interface Illuminate\\\\Contracts\\\\Support\\\\Arrayable does not specify its types\\: TKey, TValue$#"
count: 1
path: src/Http/Resources/JsonResourceCollection.php

-
message: "#^Method Tests\\\\LaraStrict\\\\Feature\\\\Translations\\\\InvalidServiceProviderTranslations\\:\\:getProviderClass\\(\\) should return class\\-string\\<LaraStrict\\\\Providers\\\\AbstractServiceProvider\\> but returns string\\.$#"
count: 1
path: tests/Feature/Translations/InvalidServiceProviderTranslations.php
-
message: "#^Cannot cast array\\|string to string\\.$#"
count: 1
path: src/Testing/Actions/GetNamespaceForStubsAction.php

-
message: "#^Calling PHPStan\\\\PhpDoc\\\\PhpDocStringResolver\\:\\:resolve\\(\\) is not covered by backward compatibility promise\\. The method might change in a minor PHPStan version\\.$#"
count: 1
path: src/Testing/Actions/ParsePhpDocAction.php

-
message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertTrue\\(\\) with false and 'Hook should be…' will always evaluate to false\\.$#"
count: 1
path: tests/Feature/Testing/Commands/MakeExpectationCommandRealTest.php

-
message: "#^Method Tests\\\\LaraStrict\\\\Feature\\\\Translations\\\\InvalidServiceProviderTranslations\\:\\:getProviderClass\\(\\) should return class\\-string\\<LaraStrict\\\\Providers\\\\AbstractServiceProvider\\> but returns string\\.$#"
count: 1
path: tests/Feature/Translations/InvalidServiceProviderTranslations.php
14 changes: 14 additions & 0 deletions src/Http/Resources/JsonResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ public function setContainer(Container $container): self
return $this;
}

public static function collection($resource): JsonResourceCollection
{
return tap(
new JsonResourceCollection($resource, static::class),
static function (JsonResourceCollection $collection) {
// preserveKeys was added Laravel v9.45.0
if (property_exists($collection, 'preserveKeys')
&& property_exists(static::class, 'preserveKeys')) {
$collection->preserveKeys = (new static(null))->preserveKeys === true;
}
}
);
}

protected function getContainer(): Container
{
if ($this->container === null) {
Expand Down
31 changes: 31 additions & 0 deletions src/Http/Resources/JsonResourceCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace LaraStrict\Http\Resources;

use Illuminate\Contracts\Container\Container;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class JsonResourceCollection extends AnonymousResourceCollection
{
private ?Container $container = null;

public function setContainer(?Container $container): self
{
$this->container = $container;

return $this;
}

public function toArray($request)
{
return $this->collection->map(function (JsonResource $resource) use ($request) {
if ($this->container !== null) {
$resource->setContainer($this->container);
}

return $resource->toArray($request);
})->all();
}
}
41 changes: 35 additions & 6 deletions src/Testing/PHPUnit/ResourceTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
namespace LaraStrict\Testing\PHPUnit;

use Closure;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use LaraStrict\Http\Resources\JsonResource as LaraStrictJsonResource;
use LaraStrict\Http\Resources\JsonResourceCollection;
use LaraStrict\Testing\Assert\AssertExpectationTestCase;
use LaraStrict\Testing\Laravel\TestingContainer;
use LogicException;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use Mockery\Adapter\Phpunit\MockeryTestCaseSetUp;
use Throwable;

/**
* @template TEntity
Expand Down Expand Up @@ -44,25 +48,50 @@ protected function mockeryTestTearDown(): void
}

/**
* @param TEntity|callable():TEntity $object
* @param array<string|int, mixed> $expected
* @param TestingContainer|null $container Set container to the resource.
* @param TEntity|callable():TEntity $object
* @param array<string|int, mixed>|Exception $expected
* @param TestingContainer|null $container Set container to the resource.
*/
protected function assert(mixed $object, array $expected, ?TestingContainer $container = null): void
protected function assert(mixed $object, array|Exception $expected, ?TestingContainer $container = null): void
{
// Support catching LogicException below in our test cases.
if ($expected instanceof Throwable) {
$this->expectExceptionObject($expected);
}

$request = new Request();

$resource = $this->createResource(is_callable($object) ? $object() : $object);

if ($resource instanceof LaraStrictJsonResource && $container !== null) {
if ($container !== null) {
if ($resource instanceof LaraStrictJsonResource === false
&& $resource instanceof JsonResourceCollection === false) {
throw self::containerCannotBeSetException();
}

$resource->setContainer($container);
}

$this->assertEquals(expected: $expected, actual: $resource->resolve($request));
if ($expected instanceof Throwable) {
$this->expectExceptionObject($expected);
}

$result = $resource->resolve($request);

$this->assertEquals(expected: $expected, actual: $result);
}

/**
* @param TEntity $object
*/
abstract protected function createResource(mixed $object): JsonResource;

protected static function containerCannotBeSetException(): LogicException
{
return new LogicException(sprintf(
'Container can only be set on %s or %s.',
LaraStrictJsonResource::class,
JsonResourceCollection::class
));
}
}
130 changes: 130 additions & 0 deletions tests/Feature/Http/Resources/JsonResourceCollectionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

declare(strict_types=1);

namespace Tests\LaraStrict\Feature\Http\Resources;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use LaraStrict\Http\Resources\JsonResourceCollection;
use LaraStrict\Http\Resources\MessageResource;
use LaraStrict\Testing\Laravel\TestingContainer;
use PHPUnit\Framework\TestCase;

class JsonResourceCollectionTest extends TestCase
{
private Request $request;

protected function setUp(): void
{
parent::setUp();
$this->request = new Request();
}

/**
* @return array<string|int, array{0: Closure(static):void}>
*/
public function data(): array
{
// Preserve keys changes keys using mergeData, not input/output array
$input = [
1 => '1',
2 => '2',
];
$output = [
1 => [
'message' => '1',
],
2 => [
'message' => '2',
],
];

return [
'preserve keys false by default' => [
static fn (self $self) => $self->assert(
laravelCollection: MessageJsonResource::collection($input),
laraStrictCollection: MessageResource::collection($input),
expectedPreserveKeys: false,
expectedOutput: $output
),
],
'preserve keys true' => [
static fn (self $self) => $self->assert(
laravelCollection: PreserveKeysJsonResource::collection($input),
laraStrictCollection: PreserveKeysLaraStrictResource::collection($input),
expectedPreserveKeys: true,
expectedOutput: $output
),
],
'preserve keys false by default - no container' => [
static fn (self $self) => $self->assert(
laravelCollection: MessageJsonResource::collection($input),
laraStrictCollection: MessageResource::collection($input),
expectedPreserveKeys: false,
expectedOutput: $output,
setContainer: false
),
],
'preserve keys true - no container' => [
static fn (self $self) => $self->assert(
laravelCollection: PreserveKeysJsonResource::collection($input),
laraStrictCollection: PreserveKeysLaraStrictResource::collection($input),
expectedPreserveKeys: true,
expectedOutput: $output,
setContainer: false
),
],
];
}

/**
* @param Closure(static):void $assert
*
* @dataProvider data
*/
public function test(Closure $assert): void
{
$assert($this);
}

public function assert(
AnonymousResourceCollection $laravelCollection,
JsonResourceCollection $laraStrictCollection,
bool $expectedPreserveKeys,
array $expectedOutput,
bool $setContainer = true
): void {
// preserveKeys was added Laravel v9.45.0
if (property_exists($laravelCollection, 'preserveKeys')) {
$this->assertEquals(
expected: $expectedPreserveKeys,
actual: $laravelCollection->preserveKeys,
message: 'Laravel preserve keys'
);
}

if (property_exists($laraStrictCollection, 'preserveKeys')) {
$this->assertEquals(
expected: $expectedPreserveKeys,
actual: $laraStrictCollection->preserveKeys,
message: 'LaraStrict preserve keys'
);
}

$this->assertEquals(
expected: $expectedOutput,
actual: $laravelCollection->toArray($this->request),
message: 'Laravel toArray'
);

$this->assertEquals(
expected: $expectedOutput,
actual: $laraStrictCollection
->setContainer($setContainer ? new TestingContainer() : null)
->toArray($this->request),
message: 'LaraStrict toArray'
);
}
}
30 changes: 30 additions & 0 deletions tests/Feature/Http/Resources/MessageJsonResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Tests\LaraStrict\Feature\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

/**
* @property string $resource
*/
class MessageJsonResource extends JsonResource
{
public static $wrap = null;

public function __construct(string|null $resource)
{
parent::__construct($resource);
}

/**
* @return array
*/
public function toArray($request)
{
return [
'message' => $this->resource,
];
}
}
16 changes: 16 additions & 0 deletions tests/Feature/Http/Resources/PreserveKeysJsonResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Tests\LaraStrict\Feature\Http\Resources;

class PreserveKeysJsonResource extends MessageJsonResource
{
public bool $preserveKeys = true;

// TODO: Remove array when Laravel fixes its own implementation.
public function __construct(string|null|array $resource)
{
parent::__construct(is_array($resource) ? null : $resource);
}
}
12 changes: 12 additions & 0 deletions tests/Feature/Http/Resources/PreserveKeysLaraStrictResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Tests\LaraStrict\Feature\Http\Resources;

use LaraStrict\Http\Resources\MessageResource;

class PreserveKeysLaraStrictResource extends MessageResource
{
public bool $preserveKeys = true;
}

0 comments on commit e3bf3ec

Please sign in to comment.