Skip to content

Commit

Permalink
Merge branch 'develop' into 4.x
Browse files Browse the repository at this point in the history
  • Loading branch information
lindyhopchris committed Nov 8, 2023
2 parents 0e9f746 + 84c1c70 commit 5ddbcf2
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 41 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file. This project adheres to
[Semantic Versioning](http://semver.org/) and [this changelog format](http://keepachangelog.com/).

## Unreleased

## [3.3.0] - 2023-11-08

### Added

- Can now create a JSON:API server once, without it being thread-cached. This is required for registering routes, as we
do not expect that server instance to be used again after routes are registered. This matches production,
where `route:cache` should have been used, so it would be inaccurate to leave the server thread-cached after
registering routes in a non-production environment e.g. test.

## [3.2.0] - 2023-07-20

### Added
Expand Down
55 changes: 36 additions & 19 deletions src/Core/Server/ServerRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,22 @@
class ServerRepository implements RepositoryContract
{
/**
* @var AppResolver
* @var array<string, ServerContract>
*/
private AppResolver $app;
private array $cache = [];

/**
* @var array
* @var array<string, class-string<ServerContract>>
*/
private array $cache;
private array $classes = [];

/**
* ServerRepository constructor.
*
* @param AppResolver $app
*/
public function __construct(AppResolver $app)
public function __construct(private readonly AppResolver $app)
{
$this->app = $app;
$this->cache = [];
}

Expand All @@ -59,31 +58,49 @@ public function server(string $name): ServerContract
throw new InvalidArgumentException('Expecting a non-empty JSON:API server name.');
}

if (isset($this->cache[$name])) {
return $this->cache[$name];
}
return $this->cache[$name] = $this->cache[$name] ?? $this->make($name);
}

/**
* Use a server once, without thread-caching it.
*
* @param string $name
* @return ServerContract
* TODO add to interface
*/
public function once(string $name): ServerContract
{
return $this->make($name);
}

$class = $this->config()->get("jsonapi.servers.{$name}");
/**
* @param string $name
* @return ServerContract
*/
private function make(string $name): ServerContract
{
$class = $this->classes[$name] ?? $this->config()->get("jsonapi.servers.{$name}");

if (empty($class) || !class_exists($class)) {
throw new RuntimeException("Server {$name} does not exist in config or is not a valid class.");
}
assert(
!empty($class) && class_exists($class) && is_a($class, ServerContract::class, true),
"JSON:API server '{$name}' does not exist in config or is not a valid class.",
);

$this->classes[$name] = $class;

try {
$server = new $class($this->app, $name);
} catch (Throwable $ex) {
throw new RuntimeException(
"Unable to construct server {$name} using class {$class}.",
"Unable to construct JSON:API server {$name} using class {$class}.",
0,
$ex
$ex,
);
}

if ($server instanceof ServerContract) {
return $this->cache[$name] = $server;
}
assert($server instanceof ServerContract, "Class {$class} is not a server instance.");

throw new RuntimeException("Class for server {$name} is not a server instance.");
return $server;
}

/**
Expand Down
120 changes: 100 additions & 20 deletions tests/Unit/Server/ServerRepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,40 +23,120 @@
use Illuminate\Contracts\Foundation\Application;
use LaravelJsonApi\Core\Server\ServerRepository;
use LaravelJsonApi\Core\Support\AppResolver;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class ServerRepositoryTest extends TestCase
{
/**
* @var MockObject&Application
*/
private Application&MockObject $app;

/**
* @var MockObject&ConfigRepository
*/
private ConfigRepository&MockObject $config;

/**
* @var AppResolver
*/
private AppResolver $resolver;

/**
* @var ServerRepository
*/
private ServerRepository $repository;

/**
* @return void
*/
protected function setUp(): void
{
parent::setUp();

$this->app = $this->createMock(Application::class);
$this->config = $this->createMock(ConfigRepository::class);
$this->resolver = new AppResolver(fn() => $this->app);
$this->repository = new ServerRepository($this->resolver);
}

/**
* @return void
*/
public function test(): void
{
$name = 'v1';
$klass = TestServer::class;
$expected = $this->willMakeServer($name = 'v1');
$actual = $this->repository->server($name);

$app = $this->createMock(Application::class);
$config = $this->createMock(ConfigRepository::class);
$resolver = new AppResolver(static fn() => $app);
$this->assertInstanceOf(TestServer::class, $actual);
$this->assertEquals($expected, $actual);
$this->assertSame($actual, $this->repository->server($name)); // server should be thread cached.
}

$app->method('make')
->with(ConfigRepository::class)
->willReturn($config);
/**
* @return void
*/
public function testItCanUseServerOnce1(): void
{
$this->willMakeServer($name = 'v2');

$config
->expects($this->once())
->method('get')
->with("jsonapi.servers.{$name}")
->willReturn($klass);
$server1 = $this->repository->once($name);
$server2 = $this->repository->server($name);
$server3 = $this->repository->server($name);

$this->assertNotSame($server1, $server2);
$this->assertNotSame($server1, $server3);
$this->assertSame($server2, $server3);
}

$expected = new TestServer($resolver, $name);
/**
* @return void
*/
public function testItCanUseServerOnce2(): void
{
$this->willMakeServer($name = 'v2');

$repository = new ServerRepository($resolver);
$server1 = $this->repository->server($name);
$server2 = $this->repository->once($name);
$server3 = $this->repository->server($name);

$actual = $repository->server($name);
$this->assertNotSame($server1, $server2);
$this->assertNotSame($server2, $server3);
$this->assertSame($server1, $server3);
}

$this->assertInstanceOf($klass, $actual);
$this->assertEquals($expected, $actual);
/**
* @return void
*/
public function testItHasInvalidClassNameForServer(): void
{
$this->willMakeServer($name = 'invalid', \DateTimeImmutable::class);

$this->expectException(\AssertionError::class);
$this->expectExceptionMessage("JSON:API server 'invalid' does not exist in config or is not a valid class.");

$this->repository->server($name);
}

/**
* @param string $name
* @param string $class
* @return TestServer
*/
private function willMakeServer(string $name, string $class = TestServer::class): TestServer
{
$this->app
->method('make')
->with(ConfigRepository::class)
->willReturn($this->config);

$this->config
->expects($this->once())
->method('get')
->with("jsonapi.servers.{$name}")
->willReturn($class);

/** We expect the server to only be constructed once. */
$this->assertSame($actual, $repository->server($name));
return new TestServer($this->resolver, $name);
}
}
3 changes: 1 addition & 2 deletions tests/Unit/Server/TestServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@

class TestServer extends BaseServer
{

/**
* @inheritDoc
* @return void
*/
public function serving(): void
{
Expand Down

0 comments on commit 5ddbcf2

Please sign in to comment.