Skip to content

Commit

Permalink
Improvements
Browse files Browse the repository at this point in the history
- removes the previous error handling logic in route() method (wip)
- moves the locale folder from src/ to project root
- adds translation strings to error messages
- adds error messages for en_US locale
- do not load Woops if ENV var is 'PRODUCTION'
- adds tests for DIModule
- improved tests for error handlers
- adds a test for route parameters
- updates dependencies in composer
- do not try to find the Config instance path
- check if .env is readable/exists for configuration
- uses `CurlyFormatter` for i18n messages (updates from [kodedphp/i18n](https://github.com/kodedphp/i18n/releases/tag/0.9.2))
- fixes the default translation.dir path (updates from [kodedphp/stdlib](kodedphp/stdlib@331dabc))
  • Loading branch information
kodeart authored Oct 18, 2023
1 parent 724c5e1 commit 51e4784
Show file tree
Hide file tree
Showing 20 changed files with 222 additions and 105 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*.md diff=markdown

.git* export-ignore
bench/ export-ignore
docs/ export-ignore
tests/ export-ignore
*.dist export-ignore
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ nbproject/
public/
site/
vendor/
.DS_Store/
tmp/

.idea/
.fleet/
.vscode/
.settings/
.project/
.buildpath/
.directory/

.DS_Store
Thumbs.db
Desktop.ini
ehthumbs.db
Expand Down
7 changes: 3 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@
"php": "^8.1",
"psr/http-server-middleware": "^1",
"koded/container": "^3",
"koded/cache-simple": "^3",
"koded/cache-simple": "^3.1",
"koded/http": "4.*",
"koded/i18n": "^0.9",
"koded/logging": "^3.1",
"koded/i18n": "^0.9.2",
"koded/logging": "^3.3",
"koded/session": "2.*",
"koded/stdlib": "6.2.1 as 5.1.1",
"filp/whoops": "^2.15",
"ext-json": "*",
"ext-mbstring": "*"
Expand Down
22 changes: 22 additions & 0 deletions locale/en_US.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

return [
'language' => 'English',
'messages' => [
'koded.handler.wrong.type' => '"type" must be an exception type',
'koded.handler.missing' => 'Error handler must either be specified explicitly, or defined as a static method named "handle" that is a member of the given exception type',
'koded.middleware.implements' => 'A middleware class {0} must implement {1}',

'koded.router.noSlash' => 'URI template must begin with \'/\'',
'koded.router.duplicateSlashes' => 'URI template has duplicate slashes',
'koded.router.pcre.compilation' => 'PCRE compilation error. {0}',
'koded.router.invalidRoute.title' => 'Invalid route. No regular expression provided',
'koded.router.invalidRoute.detail' => 'Provide a proper PCRE regular expression',
'koded.router.invalidParam.title' => "Invalid route parameter type '{0}'",
'koded.router.invalidParam.detail' => 'Use one of the supported parameter types',
'koded.router.duplicateRoute.title' => 'Duplicate route',
'koded.router.duplicateRoute.detail' => 'Detected a multiple route definitions. The URI template for "%s" conflicts with an already registered route "%s".',
'koded.router.multiPaths.title' => 'Invalid route. Multiple path parameters in the route template detected',
'koded.router.multiPaths.detail' => 'Only one "path" type is allowed as URI parameter',
]
];
44 changes: 19 additions & 25 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,18 @@
use function get_class;
use function get_debug_type;
use function get_parent_class;
use function getenv;
use function is_a;
use function is_callable;
use function method_exists;
use function rawurldecode;
use function sprintf;

(new WoopsRunner)
->prependHandler(new PrettyPageHandler)
->register();
if (false === getenv('PRODUCTION')) {
(new WoopsRunner)
->prependHandler(new PrettyPageHandler)
->register();
}

class App implements RequestHandlerInterface
{
Expand Down Expand Up @@ -125,22 +128,9 @@ public function route(
array $middleware = [],
bool $explicit = false): App
{
try {
$this->container->get(Router::class)->route($uriTemplate, $resource);
$this->explicit[$uriTemplate] = [$explicit, $middleware];
return $this;
} catch (Throwable $exception) {
$response = $this->container->get(ResponseInterface::class);
if ($this->handleException(
$request = $this->container->get(ServerRequestInterface::class),
$response,
$exception
)) {
($this->container)($this->renderer, [$request, $response]);
exit;
}
throw $exception;
}
$this->container->get(Router::class)->route($uriTemplate, $resource);
$this->explicit[$uriTemplate] = [$explicit, $middleware];
return $this;
}

/**
Expand All @@ -161,20 +151,22 @@ public function group(
/**[ template, resource, middleware, explicit ]**/
$route += ['', '', [], false];
[$uriTemplate, $resource, $mw, $explicit] = $route;
$this->route($prefix . $uriTemplate, $resource, array_merge($middleware, $mw), $explicit);
$this->route($prefix . $uriTemplate,
$resource,
array_merge($middleware, $mw),
$explicit
);
}
return $this;
}

public function withErrorHandler(string $type, callable|null $handler): App
{
if (false === is_a($type, Throwable::class, true)) {
throw new TypeError('"type" must be an exception type', HttpStatus::CONFLICT);
throw new TypeError(__('koded.handler.wrong.type'), HttpStatus::CONFLICT);
}
if (null === $handler && false === method_exists($type, 'handle')) {
throw new TypeError('Error handler must either be specified explicitly,' .
' or defined as a static method named "handle" that is a member of' .
' the given exception type', HttpStatus::NOT_IMPLEMENTED);
throw new TypeError(__('koded.handler.missing'), HttpStatus::NOT_IMPLEMENTED);
}
$this->handlers[$type] = $handler;
return $this;
Expand Down Expand Up @@ -216,7 +208,9 @@ private function initialize(?string $uriTemplate): void
is_a($middleware, MiddlewareInterface::class, true) => $this->container->new($middleware),
is_callable($middleware) => new CallableMiddleware($middleware),
default => throw new InvalidArgumentException(
sprintf('Middleware "%s" must implement %s', $class, MiddlewareInterface::class)
__('koded.middleware.implements', [
$class, MiddlewareInterface::class
])
)
};
}
Expand Down
12 changes: 4 additions & 8 deletions src/Middleware/AuthMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,10 @@

class AuthMiddleware implements MiddlewareInterface
{
private readonly AuthProcessor $processor;
private readonly AuthBackend $backend;

public function __construct(AuthProcessor $processor, AuthBackend $backend)
{
$this->processor = $processor;
$this->backend = $backend;
}
public function __construct(
private readonly AuthProcessor $processor,
private readonly AuthBackend $backend
) {}

public function process(
ServerRequestInterface $request,
Expand Down
12 changes: 6 additions & 6 deletions src/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
use Koded\Framework\Auth\{AuthBackend, AuthProcessor, BearerAuthProcessor, SessionAuthBackend};
use Koded\Http\{ServerRequest, ServerResponse};
use Koded\Http\Interfaces\{Request, Response};
use Koded\I18n\{I18n, I18nCatalog};
use Koded\I18n\{I18n, I18nCatalog, CurlyFormatter};
use Koded\Logging\Log;
use Koded\Logging\Processors\Cli;
use Koded\Stdlib\{Config, Configuration, Immutable};
use Psr\Http\Message\{ResponseInterface, ServerRequestInterface};
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use ReflectionClass;
use function dirname;
use function is_a;
use function is_readable;
Expand Down Expand Up @@ -55,14 +54,14 @@ private function configuration(): Configuration
}
if (is_a($this->configuration, Configuration::class, true)) {
$factory->fromObject($this->configuration);
$factory->rootPath = dirname((new ReflectionClass($this->configuration))->getFileName());
//$factory->root = dirname((new ReflectionClass($this->configuration))->getFileName());
} elseif (is_readable($this->configuration)) {
putenv(self::ENV_KEY . '=' . $this->configuration);
$factory->fromEnvVariable(self::ENV_KEY);
$factory->rootPath = dirname($this->configuration);
$factory->root = dirname($this->configuration);
}
load:
@$factory->fromEnvFile('.env');
is_readable("$factory->root/.env") and $factory->fromEnvFile("$factory->root/.env");
foreach ($factory->get('autoloaders', []) as $autoloader) {
include_once $autoloader;
}
Expand All @@ -74,7 +73,8 @@ private function defaultConfiguration(): Immutable
return new Immutable(
[
// I18n directives
'translation.dir' => __DIR__ . '/locale',
'translation.dir' => __DIR__ . '/../locale',
'translation.formatter' => CurlyFormatter::class,

// CORS overrides (all values are scalar)
'cors.disable' => false,
Expand Down
30 changes: 11 additions & 19 deletions src/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
use function Koded\Stdlib\json_unserialize;
use function preg_match;
use function preg_match_all;
use function sprintf;
use function str_contains;
use function str_replace;

Expand Down Expand Up @@ -45,8 +44,8 @@ public function __destruct()

public function route(string $template, object|string $resource): void
{
assert('/' === $template[0], 'URI template must begin with "/"');
assert(false === str_contains($template, '//'), 'URI template has duplicate slashes');
assert('/' === ($template[0] ?? ''), __('koded.router.noSlash'));
assert(false === str_contains($template, '//'), __('koded.router.duplicateSlashes'));

$id = $this->id($template);
if ($this->index[$id] ?? false) {
Expand Down Expand Up @@ -74,10 +73,6 @@ public function match(string $path): array

private function normalizeParams(array $route, array $params): array
{
if (empty($params)) {
$route['params'] = [];
return $route;
}
$route['params'] = json_unserialize(json_serialize(
array_filter($params, 'is_string', ARRAY_FILTER_USE_KEY),
JSON_NUMERIC_CHECK)
Expand Down Expand Up @@ -162,7 +157,7 @@ private function compileTemplate(string $template): array
];
} catch (Throwable $ex) {
throw new HTTPConflict(
title: 'PCRE compilation error. ' . $ex->getMessage(),
title: __('koded.router.pcre.compilation', [$ex->getMessage()]),
detail: $ex->getMessage(),
instance: $template,
);
Expand All @@ -176,13 +171,13 @@ private function assertSupportedType(
string $filter): void
{
('regex' === $type and empty($filter)) and throw new HTTPConflict(
title: 'Invalid route. No regular expression provided',
detail: 'Provide a proper PCRE regular expression',
title: __('koded.router.invalidRoute.title'),
detail: __('koded.router.invalidRoute.detail'),
instance: $template,
);
isset($types[$type]) or throw (new HTTPConflict(
title: "Invalid route parameter type '$type'",
detail: 'Use one of the supported parameter types',
title: __('koded.router.invalidParam.title', [$type]),
detail: __('koded.router.invalidParam.detail'),
instance: $template,
))->setMember('supported-types', array_keys($types));
}
Expand All @@ -194,16 +189,13 @@ private function assertIdentityAndPaths(
{
isset($this->identity[$identity]) and throw (new HTTPConflict(
instance: $template,
title: 'Duplicate route',
detail: sprintf(
'Detected a multiple route definitions. The URI template ' .
'for "%s" conflicts with an already registered route "%s".',
$template, $this->identity[$identity])
title: __('koded.router.duplicateRoute.title'),
detail: __('koded.router.duplicateRoute.detail', [$template, $this->identity[$identity]])
))->setMember('conflict-route', [$template => $this->identity[$identity]]);

$paths > 1 and throw new HTTPConflict(
title: 'Invalid route. Multiple path parameters in the route template detected',
detail: 'Only one "path" type is allowed as URI parameter',
title: __('koded.router.multiPaths.title'),
detail: __('koded.router.multiPaths.detail'),
instance: $template,
);
}
Expand Down
7 changes: 0 additions & 7 deletions src/locale/en_US.php

This file was deleted.

21 changes: 21 additions & 0 deletions tests/AppTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Tests\Koded\Framework;

use Koded\Framework\App;
use PHPUnit\Framework\TestCase;
use function date_default_timezone_get;
use function date_default_timezone_set;

class AppTest extends TestCase
{
public function test_construct_sets_timezone_to_utc()
{
date_default_timezone_set('Europe/Berlin');

new App;

$this->assertEquals('UTC', date_default_timezone_get(),
'App instance always sets the default timezone to UTC');
}
}
47 changes: 47 additions & 0 deletions tests/DIModuleConfigurationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace Tests\Koded\Framework;

use Koded\Framework\App;
use Koded\Stdlib\Config;
use PHPUnit\Framework\TestCase;
use function Koded\Stdlib\env;

class DIModuleConfigurationTest extends TestCase
{
use ObjectPropertyTrait;

/**
* @dataProvider configs
*/
public function test_loading_configuration($config)
{
$app = (new App(
config: $config,
));

/** @var Config $config */
$config = $this
->objectProperty($app, 'container')
->get(Config::class);

$this->assertSame('bar', $config->get('test.foo'));
$this->assertSame('BAR', env('TEST_FOO'),
'ENV variables are loaded from .env in the configured root directory');

$this->assertSame([
__DIR__ . '/Fixtures/test-autoloader.php',
], $config->get('autoloaders'));

$this->assertTrue(class_exists(\TestAutoloadedClass::class),
'Classes are loaded from "autoloaders" directive');
}

public function configs()
{
return [
[__DIR__ . '/Fixtures/test-conf.php'],
[(new Config(__DIR__ . '/Fixtures'))->fromPhpFile(__DIR__ . '/Fixtures/test-conf.php')]
];
}
}
Loading

0 comments on commit 51e4784

Please sign in to comment.