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

feat: sitekit resource loader #2

Merged
merged 13 commits into from
Nov 21, 2023
Merged
2 changes: 1 addition & 1 deletion .phplint.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
path: ./src
jobs: 10
cache: .cache/phplint.cache
cache: var/cache/phplint.cache
extensions:
- php
warning: false
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
parameters:
level: 9
tmpDir: .cache/phpstan
tmpDir: var/cache/phpstan
paths:
- src
2 changes: 1 addition & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
</report>
</coverage>

<testsuite name="atoolo-resource-loader">
<testsuite name="atoolo-resource">
<directory>test/</directory>
</testsuite>

Expand Down
33 changes: 33 additions & 0 deletions src/Exception/InvalidResourceException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Atoolo\Resource\Exception;

/**
* This exception is used when a resource is invalid. This can have the
* following reasons:
*
* - If the resource is syntactically incorrect.
* - If the resource does not contain necessary data.
*/
class InvalidResourceException extends \RuntimeException
{
public function __construct(
private readonly string $location,
string $message = "",
int $code = 0,
?\Throwable $previous = null
) {
parent::__construct(
$location . ': ' . $message,
$code,
$previous
);
}

public function getLocation(): string
{
return $this->location;
}
}
30 changes: 30 additions & 0 deletions src/Exception/ResourceNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Atoolo\Resource\Exception;

/**
* This exception is used if no resource could be found via the specified
* location.
*/
class ResourceNotFoundException extends \RuntimeException
{
public function __construct(
private readonly string $location,
string $message = "",
int $code = 0,
?\Throwable $previous = null
) {
parent::__construct(
$location . ': ' . $message,
$code,
$previous
);
}

public function getLocation(): string
{
return $this->location;
}
}
19 changes: 19 additions & 0 deletions src/Loader/SiteKit/ContextStub.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Atoolo\Resource\Loader\SiteKit;

/**
* Provides the behavior of a context needed to load a SiteKit resource.
* @codeCoverageIgnore
*/
class ContextStub
{
public function redirectToTranslation(
LifecylceStub $lifecylce,
string $path
): mixed {
return null;
}
}
40 changes: 40 additions & 0 deletions src/Loader/SiteKit/LifecylceStub.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Atoolo\Resource\Loader\SiteKit;

/**
* Provides the behavior of a lifecylce needed to load a SiteKit resource.
* @codeCoverageIgnore
*/
class LifecylceStub
{
/**
* @param array<string, mixed> $data
*/
public function init(array $data): ResourceStub
{
$resource = new ResourceStub();
$resource->init($data);
return $resource;
}

public function finish(ResourceStub $resource): bool
{
return false;
}

public function process(string $name, ResourceStub $resource): bool
{
return true;
}

/**
* @return array<string, mixed>
*/
public function service(ResourceStub $resource): array
{
return $resource->getData();
}
}
41 changes: 41 additions & 0 deletions src/Loader/SiteKit/ResourceStub.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Atoolo\Resource\Loader\SiteKit;

/**
* Provides the behavior of a resource needed to load a SiteKit resource.
* @codeCoverageIgnore
*/
class ResourceStub
{
/**
* @var array<string, mixed>
*/
private array $data = [];

/**
* @param array<string, mixed> $data
*/
public function init(array $data): void
{
$this->data['init'] = $data;
}

/**
* @param array<string, mixed> $data
*/
public function process(string $name, array $data): void
{
$this->data[$name] = $data;
}

/**
* @return array<string, mixed>
*/
public function getData(): array
{
return $this->data;
}
}
151 changes: 151 additions & 0 deletions src/Loader/SiteKitLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

declare(strict_types=1);

namespace Atoolo\Resource\Loader;

use Atoolo\Resource\Exception\InvalidResourceException;
use Atoolo\Resource\Exception\ResourceNotFoundException;
use Atoolo\Resource\Loader\SiteKit\ContextStub;
use Atoolo\Resource\Loader\SiteKit\LifecylceStub;
use Atoolo\Resource\Resource;
use Atoolo\Resource\ResourceLoader;

/**
* ResourceLoader that loads resources created with SiteKit aggregators.
* @phpstan-type InitData array{id: int, name: string, objectType: string}
* @phpstan-type ResourceData array{init: InitData}
*/
class SiteKitLoader implements ResourceLoader
{
public function __construct(
private readonly string $basePath
) {
}

/**
* @throws InvalidResourceException
* @throws ResourceNotFoundException
*/
public function load(string $location): Resource
sitepark-schaeper marked this conversation as resolved.
Show resolved Hide resolved
{
$data = $this->loadRaw($location);

$this->validateData($location, $data);

$init = $data['init'];

return new Resource(
$location,
(string)$init['id'],
$init['name'],
$init['objectType'],
$data
);
}

/**
* @return array<string, mixed> $data
*/
private function loadRaw(string $location): array
{
$file = $this->basePath . '/' . $location;

/**
* $context and $lifecycle must be defined here, because for the SiteKit
* resource PHP files these variables must be provided for the require
* call.
*/
$context = new ContextStub();
$lifecycle = new LifecylceStub();
sitepark-schaeper marked this conversation as resolved.
Show resolved Hide resolved

$saveErrorReporting = error_reporting();

try {
error_reporting(E_ERROR | E_PARSE);
return require $file;
} catch (\ParseError $e) {
throw new InvalidResourceException(
$location,
$e->getMessage(),
0,
$e
);
} catch (\Error $e) {
if (!file_exists($file)) {
throw new ResourceNotFoundException(
$location,
$e->getMessage(),
0,
$e
);
}
throw new InvalidResourceException(
$location,
$e->getMessage(),
0,
$e
);
} finally {
error_reporting($saveErrorReporting);
}
}

/**
* @param array<string, mixed> $data
* @return ($data is ResourceData ? void : never)
*/
private function validateData(string $location, array $data): void
{

/*
* Cannot be passed because this case cannot occur here. This would
* already lead to an error in ResourceStub. But is still included,
* that so no phpstan errors arise.
*/
// @codeCoverageIgnoreStart
if (!isset($data['init']) || !is_array($data['init'])) {
throw new InvalidResourceException($location, 'init field missing');
}
// @codeCoverageIgnoreEnd

$init = $data['init'];

if (!isset($init['id'])) {
throw new InvalidResourceException(
$location,
'id field missing'
);
}
if (!is_int($init['id'])) {
throw new InvalidResourceException(
$location,
'id field not an int'
);
}
if (!isset($init['name'])) {
throw new InvalidResourceException(
$location,
'name field missing'
);
}
if (!is_string($init['name'])) {
throw new InvalidResourceException(
$location,
'name field not a string'
);
}
if (!isset($init['objectType'])) {
throw new InvalidResourceException(
$location,
'objectType field missing'
);
}
if (!is_string($init['objectType'])) {
throw new InvalidResourceException(
$location,
'objectType field not a string'
);
}
}
}
Loading
Loading