This commit is contained in:
40
.gitea/workflows/ci.yml
Normal file
40
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
name: PHP Composer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ '*' ]
|
||||
pull_request:
|
||||
branches: [ '*' ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
coverage: pcov
|
||||
|
||||
- name: Validate composer.json
|
||||
run: composer validate
|
||||
|
||||
- name: Cache Composer packages
|
||||
id: composer-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor
|
||||
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-php-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.composer-cache.outputs.cache-hit != 'true'
|
||||
run: composer install --prefer-dist --no-progress --no-suggest
|
||||
|
||||
- name: Run test suite
|
||||
run: composer run-script test
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/vendor/
|
||||
.php-cs-fixer.cache
|
||||
coverage.xml
|
||||
.env
|
||||
resources/docker/db-data
|
||||
29
.php-cs-fixer.dist.php
Normal file
29
.php-cs-fixer.dist.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return (new PhpCsFixer\Config())
|
||||
->setRules([
|
||||
'@Symfony' => true,
|
||||
'@Symfony:risky' => true,
|
||||
'@PSR12' => true,
|
||||
'@PHP81Migration' => true,
|
||||
'@PHP80Migration:risky' => true,
|
||||
'@PHPUnit84Migration:risky' => true,
|
||||
'ordered_imports' => true,
|
||||
'phpdoc_line_span' => [
|
||||
'const' => 'single',
|
||||
'method' => 'single',
|
||||
'property' => 'single',
|
||||
],
|
||||
'php_unit_test_case_static_method_calls' => true,
|
||||
'phpdoc_to_comment' => false,
|
||||
])
|
||||
->setRiskyAllowed(true)
|
||||
->setFinder(
|
||||
PhpCsFixer\Finder::create()
|
||||
->in([
|
||||
__DIR__,
|
||||
])
|
||||
->ignoreUnreadableDirs()
|
||||
);
|
||||
15
Makefile
Normal file
15
Makefile
Normal file
@@ -0,0 +1,15 @@
|
||||
EXEC="docker compose exec php"
|
||||
|
||||
up:
|
||||
docker compose up -d
|
||||
|
||||
start: up composer-install
|
||||
|
||||
stop:
|
||||
docker compose stop
|
||||
cmd:
|
||||
"$(EXEC)" sh
|
||||
phpstan:
|
||||
"$(EXEC)" vendor/bin/phpstan analyse src --level 8
|
||||
cs-fix:
|
||||
"$(EXEC)" vendor/bin/php-cs-fixer fix --allow-risky=yes
|
||||
53
composer.json
Normal file
53
composer.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "armelnl/armelnet",
|
||||
"description": "Armel.nl website",
|
||||
"license": "WTFPL",
|
||||
"type": "project",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/Utility/Helpers.php"
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"laminas/laminas-diactoros": "^3.3",
|
||||
"laminas/laminas-httphandlerrunner": "^2.10",
|
||||
"php-di/php-di": "^7.0",
|
||||
"league/plates": "^3.5",
|
||||
"filp/whoops": "^2.15",
|
||||
"vlucas/phpdotenv": "^5.6",
|
||||
"league/route": "^5.1",
|
||||
"symfony/var-dumper": "^7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.63",
|
||||
"phpstan/phpstan": "^1.12",
|
||||
"rregeer/phpunit-coverage-check": "^0.3.1",
|
||||
"pestphp/pest": "^2.35"
|
||||
},
|
||||
"scripts": {
|
||||
"php-cs-fixer": [
|
||||
"vendor/bin/php-cs-fixer fix"
|
||||
],
|
||||
"phpstan": [
|
||||
"@php vendor/bin/phpstan analyse --memory-limit=-1"
|
||||
],
|
||||
"check-coverage": [
|
||||
"@php vendor/bin/coverage-check coverage.xml 100"
|
||||
],
|
||||
"test": [
|
||||
"composer phpstan",
|
||||
"composer php-cs-fixer",
|
||||
"@php vendor/bin/pest --coverage-clover=coverage.xml",
|
||||
"composer check-coverage"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
}
|
||||
}
|
||||
6443
composer.lock
generated
Normal file
6443
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
61
config/di.php
Normal file
61
config/di.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Core\EnvironmentLoader;
|
||||
use App\Enums\Environment;
|
||||
use App\Http\Middleware\CspMiddleware;
|
||||
use App\Http\Middleware\HstsMiddleware;
|
||||
use App\Http\Middleware\StartSessionMiddleware;
|
||||
use App\Http\Routes;
|
||||
use App\Security\Csp\CspPolicy;
|
||||
use App\Security\Csp\Directives\DefaultSrc;
|
||||
use App\Security\Csp\Directives\ImgSrc;
|
||||
use App\Security\Csp\Directives\ScriptSrc;
|
||||
use App\Security\Csp\Directives\StyleSrc;
|
||||
use App\Security\Hst\Config as HstConfig;
|
||||
use Laminas\Diactoros\ServerRequest;
|
||||
use Laminas\Diactoros\ServerRequestFactory;
|
||||
use League\Plates\Engine;
|
||||
use League\Route\Router;
|
||||
use League\Route\Strategy\ApplicationStrategy;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
return [
|
||||
Environment::class => static fn (): Environment => Environment::from($_ENV['ENVIRONMENT']),
|
||||
ServerRequest::class => static fn (): ServerRequest => ServerRequestFactory::fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES),
|
||||
Engine::class => static fn (): Engine => new Engine(dirname(__DIR__).'/resources/views'),
|
||||
EnvironmentLoader::class => static fn () => new EnvironmentLoader(dirname(__DIR__)),
|
||||
'middlewares' => static fn () => [
|
||||
StartSessionMiddleware::class,
|
||||
CspMiddleware::class,
|
||||
HstsMiddleware::class,
|
||||
],
|
||||
Router::class => static function (ContainerInterface $container): Router {
|
||||
$strategy = new ApplicationStrategy();
|
||||
$strategy->setContainer($container);
|
||||
|
||||
$router = new Router();
|
||||
$router->setStrategy($strategy);
|
||||
|
||||
$router->lazyMiddlewares($container->get('middlewares'));
|
||||
|
||||
Routes::routes($router);
|
||||
|
||||
return $router;
|
||||
},
|
||||
CspPolicy::class => static fn (ContainerInterface $container): CspPolicy => (new CspPolicy())
|
||||
->addDirective($container->get(DefaultSrc::class))
|
||||
->addDirective($container->get(ImgSrc::class))
|
||||
->addDirective($container->get(ScriptSrc::class))
|
||||
->addDirective($container->get(StyleSrc::class)),
|
||||
DefaultSrc::class => static fn (): DefaultSrc => (new DefaultSrc())->addSource("'self'"),
|
||||
ImgSrc::class => static fn (): ImgSrc => (new ImgSrc())->addSource("'self'"),
|
||||
ScriptSrc::class => static fn (): ScriptSrc => (new ScriptSrc())->addSource("'self'"),
|
||||
StyleSrc::class => static fn (): StyleSrc => (new StyleSrc())->addSource("'self'"),
|
||||
HstConfig::class => static fn (): HstConfig => new HstConfig(
|
||||
maxAge: 31536000,
|
||||
includeSubdomains: true,
|
||||
preload: true
|
||||
),
|
||||
];
|
||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
services:
|
||||
php:
|
||||
build:
|
||||
context: resources/docker/php
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- ./:/app
|
||||
pgsql:
|
||||
image: postgres:13-alpine
|
||||
volumes:
|
||||
- ./resources/docker/db-data:/var/lib/postgresql/data:delegated
|
||||
environment:
|
||||
- POSTGRES_DB=app
|
||||
- POSTGRES_USER=app
|
||||
- POSTGRES_PASSWORD=secret
|
||||
4
phpstan.neon
Normal file
4
phpstan.neon
Normal file
@@ -0,0 +1,4 @@
|
||||
parameters:
|
||||
paths:
|
||||
- src
|
||||
level: 8
|
||||
17
phpunit.xml
Normal file
17
phpunit.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Test Suite">
|
||||
<directory suffix="Test.php">./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">./src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
9
public/index.php
Normal file
9
public/index.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Http\Kernel;
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
app(Kernel::class)->run();
|
||||
13
resources/docker/php/Dockerfile
Normal file
13
resources/docker/php/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM composer:latest as composer
|
||||
FROM dunglas/frankenphp:php8.3.10-alpine
|
||||
|
||||
ARG PUID=1000
|
||||
ARG PGID=1000
|
||||
|
||||
COPY --from=composer /usr/bin/composer /usr/bin/composer
|
||||
|
||||
RUN install-php-extensions \
|
||||
opcache \
|
||||
intl \
|
||||
pdo_pgsql \
|
||||
pcov
|
||||
1
resources/views/error.php
Normal file
1
resources/views/error.php
Normal file
@@ -0,0 +1 @@
|
||||
HTTP <?php echo $status; ?> <?php echo $message; ?>
|
||||
1
resources/views/home.php
Normal file
1
resources/views/home.php
Normal file
@@ -0,0 +1 @@
|
||||
Welkom
|
||||
30
src/Core/Container.php
Normal file
30
src/Core/Container.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use DI\Container as DIContainer;
|
||||
use DI\ContainerBuilder;
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
final class Container
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static ?DIContainer $container = null;
|
||||
|
||||
public static function getInstance(): DIContainer
|
||||
{
|
||||
if (null === self::$container) {
|
||||
$container = (new ContainerBuilder())
|
||||
->addDefinitions(__DIR__.'/../../config/di.php');
|
||||
|
||||
self::$container = $container->build();
|
||||
}
|
||||
|
||||
return self::$container;
|
||||
}
|
||||
}
|
||||
41
src/Core/EnvironmentLoader.php
Normal file
41
src/Core/EnvironmentLoader.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use App\Enums\Environment;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
final class EnvironmentLoader
|
||||
{
|
||||
public function __construct(private readonly string $rootDir)
|
||||
{
|
||||
}
|
||||
|
||||
public function load(?Environment $environment = null): void
|
||||
{
|
||||
$filename = $this->determineFileName($environment);
|
||||
|
||||
if (!file_exists($this->rootDir.'/'.$filename)) {
|
||||
throw new \RuntimeException(\sprintf('A %s file is necessary to run this application', $filename));
|
||||
}
|
||||
|
||||
$dotenv = Dotenv::createImmutable($this->rootDir, $filename);
|
||||
|
||||
$dotenv->load();
|
||||
|
||||
$dotenv
|
||||
->required('ENVIRONMENT')
|
||||
->allowedValues(array_map(static fn (Environment $environment): string => $environment->value, Environment::cases()));
|
||||
}
|
||||
|
||||
private function determineFileName(?Environment $environment = null): string
|
||||
{
|
||||
return match ($environment) {
|
||||
Environment::Testing => '.env.testing',
|
||||
default => '.env',
|
||||
};
|
||||
}
|
||||
}
|
||||
23
src/Enums/Environment.php
Normal file
23
src/Enums/Environment.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
enum Environment: string
|
||||
{
|
||||
case Development = 'development';
|
||||
case Production = 'production';
|
||||
case Testing = 'testing';
|
||||
|
||||
public function isCurrent(): bool
|
||||
{
|
||||
return $this->value === $_ENV['ENVIRONMENT'];
|
||||
}
|
||||
|
||||
public static function current(): self
|
||||
{
|
||||
return self::from($_ENV['ENVIRONMENT']);
|
||||
}
|
||||
}
|
||||
17
src/Http/Controllers/HomeController.php
Normal file
17
src/Http/Controllers/HomeController.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Response\View;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
final class HomeController
|
||||
{
|
||||
/** @param array<string, mixed> $args */
|
||||
public function __invoke(ServerRequestInterface $request, array $args = []): View
|
||||
{
|
||||
return view('home');
|
||||
}
|
||||
}
|
||||
33
src/Http/ExceptionHandler.php
Normal file
33
src/Http/ExceptionHandler.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||
use League\Route\Http\Exception;
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
final class ExceptionHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SapiEmitter $emitter
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(\Throwable $error): void
|
||||
{
|
||||
$this
|
||||
->emitter
|
||||
->emit(
|
||||
view(
|
||||
'error',
|
||||
[
|
||||
'status' => $statusCode = $error instanceof Exception ? $error->getStatusCode() : 500,
|
||||
'message' => $error instanceof Exception ? $error->getMessage() : 'Internal Server Error',
|
||||
],
|
||||
)
|
||||
->withStatus($statusCode),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
src/Http/Kernel.php
Normal file
48
src/Http/Kernel.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use App\Core\EnvironmentLoader;
|
||||
use App\Enums\Environment;
|
||||
use Laminas\Diactoros\ServerRequest;
|
||||
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||
use League\Route\Router;
|
||||
use Whoops\Handler\PrettyPageHandler;
|
||||
use Whoops\Run;
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
final readonly class Kernel
|
||||
{
|
||||
public function __construct(
|
||||
private EnvironmentLoader $environmentLoader,
|
||||
private ExceptionHandler $exceptionHandler,
|
||||
private ServerRequest $serverRequest,
|
||||
private Router $router,
|
||||
private SapiEmitter $sapiEmitter,
|
||||
) {
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->environmentLoader->load();
|
||||
$this->setExceptionHandler();
|
||||
|
||||
$response = $this->router->dispatch($this->serverRequest);
|
||||
$this->sapiEmitter->emit($response);
|
||||
}
|
||||
|
||||
private function setExceptionHandler(): void
|
||||
{
|
||||
if (Environment::Production->isCurrent()) {
|
||||
set_exception_handler($this->exceptionHandler->handle(...));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$whoops = new Run();
|
||||
$whoops->pushHandler(new PrettyPageHandler());
|
||||
$whoops->register();
|
||||
}
|
||||
}
|
||||
26
src/Http/Middleware/CspMiddleware.php
Normal file
26
src/Http/Middleware/CspMiddleware.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Security\Csp\CspPolicy;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
final readonly class CspMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CspPolicy $cspPolicy
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
return $handler
|
||||
->handle($request)
|
||||
->withHeader('Content-Security-Policy', (string) $this->cspPolicy);
|
||||
}
|
||||
}
|
||||
26
src/Http/Middleware/HstsMiddleware.php
Normal file
26
src/Http/Middleware/HstsMiddleware.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Security\Hst\Config;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
final class HstsMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Config $hstsConfig
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
return $handler
|
||||
->handle($request)
|
||||
->withHeader('Strict-Transport-Security', (string) $this->hstsConfig);
|
||||
}
|
||||
}
|
||||
22
src/Http/Middleware/StartSessionMiddleware.php
Normal file
22
src/Http/Middleware/StartSessionMiddleware.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
final class StartSessionMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
if (\PHP_SESSION_NONE === session_status()) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
11
src/Http/Response/View.php
Normal file
11
src/Http/Response/View.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Response;
|
||||
|
||||
use Laminas\Diactoros\Response;
|
||||
|
||||
final class View extends Response
|
||||
{
|
||||
}
|
||||
16
src/Http/Routes.php
Normal file
16
src/Http/Routes.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use App\Http\Controllers\HomeController;
|
||||
use League\Route\Router;
|
||||
|
||||
final class Routes
|
||||
{
|
||||
public static function routes(Router $route): void
|
||||
{
|
||||
$route->get('/', HomeController::class);
|
||||
}
|
||||
}
|
||||
23
src/Security/Csp/CspPolicy.php
Normal file
23
src/Security/Csp/CspPolicy.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Csp;
|
||||
|
||||
final class CspPolicy
|
||||
{
|
||||
/** @var array<string, Directive> */
|
||||
private array $directives = [];
|
||||
|
||||
public function addDirective(Directive $directive): self
|
||||
{
|
||||
$this->directives[$directive->getName()] = $directive;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return implode('; ', $this->directives).';';
|
||||
}
|
||||
}
|
||||
25
src/Security/Csp/Directive.php
Normal file
25
src/Security/Csp/Directive.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Csp;
|
||||
|
||||
abstract class Directive
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $sources = [];
|
||||
|
||||
abstract public function getName(): string;
|
||||
|
||||
public function addSource(string $source): self
|
||||
{
|
||||
$this->sources[] = $source;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getName().' '.implode(' ', $this->sources);
|
||||
}
|
||||
}
|
||||
15
src/Security/Csp/Directives/DefaultSrc.php
Normal file
15
src/Security/Csp/Directives/DefaultSrc.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Csp\Directives;
|
||||
|
||||
use App\Security\Csp\Directive;
|
||||
|
||||
final class DefaultSrc extends Directive
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'default-src';
|
||||
}
|
||||
}
|
||||
15
src/Security/Csp/Directives/ImgSrc.php
Normal file
15
src/Security/Csp/Directives/ImgSrc.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Csp\Directives;
|
||||
|
||||
use App\Security\Csp\Directive;
|
||||
|
||||
final class ImgSrc extends Directive
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'img-src';
|
||||
}
|
||||
}
|
||||
15
src/Security/Csp/Directives/ScriptSrc.php
Normal file
15
src/Security/Csp/Directives/ScriptSrc.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Csp\Directives;
|
||||
|
||||
use App\Security\Csp\Directive;
|
||||
|
||||
final class ScriptSrc extends Directive
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'script-src';
|
||||
}
|
||||
}
|
||||
15
src/Security/Csp/Directives/StyleSrc.php
Normal file
15
src/Security/Csp/Directives/StyleSrc.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Csp\Directives;
|
||||
|
||||
use App\Security\Csp\Directive;
|
||||
|
||||
final class StyleSrc extends Directive
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'style-src';
|
||||
}
|
||||
}
|
||||
25
src/Security/Hst/Config.php
Normal file
25
src/Security/Hst/Config.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Hst;
|
||||
|
||||
final class Config
|
||||
{
|
||||
public function __construct(
|
||||
public readonly int $maxAge,
|
||||
public readonly bool $includeSubdomains,
|
||||
public readonly bool $preload,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return \sprintf(
|
||||
'max-age=%s%s%s',
|
||||
$this->maxAge,
|
||||
$this->includeSubdomains ? '; includeSubdomains' : '',
|
||||
$this->preload ? '; preload' : '',
|
||||
);
|
||||
}
|
||||
}
|
||||
40
src/Utility/Helpers.php
Normal file
40
src/Utility/Helpers.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Core\Container;
|
||||
use App\Http\Response\View;
|
||||
use League\Plates\Engine;
|
||||
|
||||
if (!function_exists('app')) { // @codeCoverageIgnore
|
||||
function app(?string $className = null): mixed
|
||||
{
|
||||
return null === $className ? Container::getInstance() : Container::getInstance()->get($className);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('view')) { // @codeCoverageIgnore
|
||||
/**
|
||||
* @param mixed[] $args
|
||||
*/
|
||||
function view(string $template = '', array $args = []): View
|
||||
{
|
||||
/** @var View $response */
|
||||
$response = app(View::class);
|
||||
|
||||
if ('' === $template) {
|
||||
return $response; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
/** @var Engine $engine */
|
||||
$engine = app(Engine::class);
|
||||
|
||||
$response
|
||||
->getBody()
|
||||
->write(
|
||||
$engine->render($template, $args),
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
19
tests/Feature/HomeControllerTest.php
Normal file
19
tests/Feature/HomeControllerTest.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
it('returns status 200', function (): void {
|
||||
$response = get('/');
|
||||
$body = $response->getBody();
|
||||
$body->rewind();
|
||||
|
||||
expect($response->getStatusCode())->toBe(200);
|
||||
});
|
||||
|
||||
it('view is rendered', function (): void {
|
||||
$response = get('/');
|
||||
$body = $response->getBody();
|
||||
$body->rewind();
|
||||
|
||||
expect($body->getContents())->toBe('Welkom');
|
||||
});
|
||||
15
tests/Pest.php
Normal file
15
tests/Pest.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Laminas\Diactoros\ServerRequest;
|
||||
use League\Route\Router;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
function get(string $uri, array $headers = [], array $cookies = []): ResponseInterface
|
||||
{
|
||||
$router = app(Router::class);
|
||||
$request = new ServerRequest(uri: $uri, method: 'GET', headers: $headers, cookieParams: $cookies);
|
||||
|
||||
return $router->dispatch($request);
|
||||
}
|
||||
11
tests/TestCase.php
Normal file
11
tests/TestCase.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
}
|
||||
7
tests/Unit/ExampleTest.php
Normal file
7
tests/Unit/ExampleTest.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
test('example', function (): void {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
Reference in New Issue
Block a user