You've already forked Php-Template
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
1ac5075b37
|
|||
|
ba2beca107
|
|||
|
b5779afde9
|
|||
|
c91f35c0b1
|
|||
|
88098837a3
|
|||
|
cd49507140
|
|||
|
7792cac8b8
|
|||
|
eaff49b6a4
|
|||
|
721008bdfc
|
|||
|
a9a5cb6216
|
|||
|
0504956d9a
|
@@ -26,6 +26,12 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Write Version File
|
||||
run: |
|
||||
echo $GITEA_REF_NAME > VERSION
|
||||
sed -i "s/dev-version/${GITEA_REF_NAME}/g" src/Helpers/Version.php
|
||||
|
||||
|
||||
- name: 🏗️ 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ RUN composer install --optimize-autoloader --ignore-platform-reqs --no-dev
|
||||
|
||||
|
||||
# Use the official PHP CLI image with Alpine Linux for the second stage
|
||||
FROM php:8.4.14-alpine AS php
|
||||
FROM siteworxpro/php:8.5.0-cli-alpine AS php
|
||||
|
||||
ARG KAFKA_ENABLED=0
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ echo "Installing xDebug"
|
||||
|
||||
apk add make gcc linux-headers autoconf alpine-sdk
|
||||
|
||||
curl -sL https://github.com/xdebug/xdebug/archive/3.4.0.tar.gz -o 3.4.0.tar.gz
|
||||
tar -xvf 3.4.0.tar.gz
|
||||
cd xdebug-3.4.0 || exit
|
||||
curl -sL https://github.com/xdebug/xdebug/archive/3.5.0alpha3.tar.gz -o 3.5.0alpha3.tar.gz
|
||||
tar -xvf 3.5.0alpha3.tar.gz
|
||||
cd xdebug-3.5.0alpha3 || exit
|
||||
phpize
|
||||
./configure --enable-xdebug
|
||||
make
|
||||
@@ -20,5 +20,5 @@ xdebug.client_host = host.docker.internal
|
||||
" > /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||
|
||||
cd ..
|
||||
rm -rf xdebug-3.4.0
|
||||
rm -rf 3.4.0.tar.gz
|
||||
rm -rf xdebug-3.5.0alpha3
|
||||
rm -rf 3.5.0alpha3.tar.gz
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.4",
|
||||
"php": "^8.5",
|
||||
"league/route": "^6.2.0",
|
||||
"illuminate/database": "^v12.34.0",
|
||||
"spiral/roadrunner-http": "^v3.6.0",
|
||||
@@ -24,7 +24,8 @@
|
||||
"monolog/monolog": "^3.9",
|
||||
"react/promise": "^3",
|
||||
"react/async": "^4",
|
||||
"guzzlehttp/guzzle": "^7.10"
|
||||
"guzzlehttp/guzzle": "^7.10",
|
||||
"zircote/swagger-php": "^5.7"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^12.4",
|
||||
|
||||
565
composer.lock
generated
565
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "8c2444c3a25a3469cf369de1c085ad01",
|
||||
"content-hash": "f12aaf0dae6930c226e719a5705e3f91",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/cli",
|
||||
@@ -1559,6 +1559,64 @@
|
||||
},
|
||||
"time": "2018-02-13T20:26:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nikic/php-parser",
|
||||
"version": "v5.6.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nikic/PHP-Parser.git",
|
||||
"reference": "3a454ca033b9e06b63282ce19562e892747449bb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb",
|
||||
"reference": "3a454ca033b9e06b63282ce19562e892747449bb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-ctype": "*",
|
||||
"ext-json": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"php": ">=7.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"ircmaxell/php-yacc": "^0.0.7",
|
||||
"phpunit/phpunit": "^9.0"
|
||||
},
|
||||
"bin": [
|
||||
"bin/php-parse"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpParser\\": "lib/PhpParser"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nikita Popov"
|
||||
}
|
||||
],
|
||||
"description": "A PHP parser written in PHP",
|
||||
"keywords": [
|
||||
"parser",
|
||||
"php"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/nikic/PHP-Parser/issues",
|
||||
"source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2"
|
||||
},
|
||||
"time": "2025-10-21T19:32:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nyholm/psr7",
|
||||
"version": "1.8.2",
|
||||
@@ -1637,6 +1695,53 @@
|
||||
],
|
||||
"time": "2024-09-09T07:06:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpdoc-parser",
|
||||
"version": "2.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpdoc-parser.git",
|
||||
"reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495",
|
||||
"reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/annotations": "^2.0",
|
||||
"nikic/php-parser": "^5.3.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpstan/extension-installer": "^1.0",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"symfony/process": "^5.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PHPStan\\PhpDocParser\\": [
|
||||
"src/"
|
||||
]
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "PHPDoc parser with support for nullable, intersection and generic types",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
|
||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0"
|
||||
},
|
||||
"time": "2025-08-30T15:50:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "predis/predis",
|
||||
"version": "v3.2.0",
|
||||
@@ -3154,6 +3259,157 @@
|
||||
],
|
||||
"time": "2024-09-25T14:21:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v8.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/7598dd5770580fa3517ec83e8da0c9b9e01f4291",
|
||||
"reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/filesystem": "^7.4|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Finder\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Finds files and directories via an intuitive fluent interface",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/v8.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-05T14:36:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-ctype": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-ctype": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Ctype\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gert de Pagter",
|
||||
"email": "BackEndTea@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for ctype functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"ctype",
|
||||
"polyfill",
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.33.0",
|
||||
@@ -3661,6 +3917,82 @@
|
||||
],
|
||||
"time": "2025-07-15T13:41:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v7.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/yaml.git",
|
||||
"reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/6c84a4b55aee4cd02034d1c528e83f69ddf63810",
|
||||
"reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-ctype": "^1.8"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/console": "<6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/console": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"bin": [
|
||||
"Resources/bin/yaml-lint"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Yaml\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Loads and dumps YAML files",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/yaml/tree/v7.4.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-16T10:14:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "voku/portable-ascii",
|
||||
"version": "2.0.3",
|
||||
@@ -3734,6 +4066,94 @@
|
||||
}
|
||||
],
|
||||
"time": "2024-11-21T01:49:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "zircote/swagger-php",
|
||||
"version": "5.7.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zircote/swagger-php.git",
|
||||
"reference": "9a37739401485b42d779495e70548309820d11d6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/9a37739401485b42d779495e70548309820d11d6",
|
||||
"reference": "9a37739401485b42d779495e70548309820d11d6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"nikic/php-parser": "^4.19 || ^5.0",
|
||||
"php": ">=7.4",
|
||||
"phpstan/phpdoc-parser": "^2.0",
|
||||
"psr/log": "^1.1 || ^2.0 || ^3.0",
|
||||
"symfony/deprecation-contracts": "^2 || ^3",
|
||||
"symfony/finder": "^5.0 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/process": ">=6, <6.4.14"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/package-versions-deprecated": "^1.11",
|
||||
"doctrine/annotations": "^2.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.62.0",
|
||||
"phpstan/phpstan": "^1.6 || ^2.0",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"rector/rector": "^1.0 || ^2.0",
|
||||
"vimeo/psalm": "^4.30 || ^5.0"
|
||||
},
|
||||
"suggest": {
|
||||
"doctrine/annotations": "^2.0",
|
||||
"radebatz/type-info-extras": "^1.0.2"
|
||||
},
|
||||
"bin": [
|
||||
"bin/openapi"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"OpenApi\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Robert Allen",
|
||||
"email": "zircote@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Bob Fanger",
|
||||
"email": "bfanger@gmail.com",
|
||||
"homepage": "https://bfanger.nl"
|
||||
},
|
||||
{
|
||||
"name": "Martin Rademacher",
|
||||
"email": "mano@radebatz.net",
|
||||
"homepage": "https://radebatz.net"
|
||||
}
|
||||
],
|
||||
"description": "Generate interactive documentation for your RESTful API using PHP attributes (preferred) or PHPDoc annotations",
|
||||
"homepage": "https://github.com/zircote/swagger-php",
|
||||
"keywords": [
|
||||
"api",
|
||||
"json",
|
||||
"rest",
|
||||
"service discovery"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/zircote/swagger-php/issues",
|
||||
"source": "https://github.com/zircote/swagger-php/tree/5.7.5"
|
||||
},
|
||||
"time": "2025-11-28T23:22:21+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
@@ -4027,64 +4447,6 @@
|
||||
],
|
||||
"time": "2025-08-01T08:46:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nikic/php-parser",
|
||||
"version": "v5.6.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nikic/PHP-Parser.git",
|
||||
"reference": "3a454ca033b9e06b63282ce19562e892747449bb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb",
|
||||
"reference": "3a454ca033b9e06b63282ce19562e892747449bb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-ctype": "*",
|
||||
"ext-json": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"php": ">=7.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"ircmaxell/php-yacc": "^0.0.7",
|
||||
"phpunit/phpunit": "^9.0"
|
||||
},
|
||||
"bin": [
|
||||
"bin/php-parse"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpParser\\": "lib/PhpParser"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nikita Popov"
|
||||
}
|
||||
],
|
||||
"description": "A PHP parser written in PHP",
|
||||
"keywords": [
|
||||
"parser",
|
||||
"php"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/nikic/PHP-Parser/issues",
|
||||
"source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2"
|
||||
},
|
||||
"time": "2025-10-21T19:32:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phar-io/manifest",
|
||||
"version": "2.0.4",
|
||||
@@ -5821,89 +6183,6 @@
|
||||
],
|
||||
"time": "2025-11-04T01:21:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-ctype": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-ctype": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Ctype\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gert de Pagter",
|
||||
"email": "BackEndTea@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for ctype functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"ctype",
|
||||
"polyfill",
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.33.0",
|
||||
@@ -6370,7 +6649,7 @@
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^8.4"
|
||||
"php": "^8.5"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
|
||||
@@ -6,6 +6,7 @@ return [
|
||||
|
||||
'app' => [
|
||||
'log_level' => Env::get('LOG_LEVEL', 'debug'),
|
||||
'dev_mode' => Env::get('DEV_MODE', false, 'bool'),
|
||||
],
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,20 @@ services:
|
||||
environment:
|
||||
PHP_IDE_CONFIG: serverName=localhost
|
||||
|
||||
swagger-ui:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.swagger-ui.entrypoints=web-secure"
|
||||
- "traefik.http.routers.swagger-ui.rule=Host(`localhost`) && PathPrefix(`/docs`)"
|
||||
- "traefik.http.routers.swagger-ui.tls=true"
|
||||
- "traefik.http.routers.swagger-ui.service=swagger-ui"
|
||||
- "traefik.http.services.swagger-ui.loadbalancer.server.port=8080"
|
||||
image: swaggerapi/swagger-ui:latest
|
||||
container_name: swagger-ui
|
||||
environment:
|
||||
BASE_URL: /docs
|
||||
URL: /.well-known/swagger.yaml
|
||||
|
||||
migration-container:
|
||||
volumes:
|
||||
- ./db/migrations:/app/db/migrations
|
||||
@@ -92,6 +106,7 @@ services:
|
||||
DEBUG: 1
|
||||
REDIS_HOST: redis
|
||||
DB_HOST: postgres
|
||||
DEV_MODE: 1
|
||||
|
||||
## Kafka and Zookeeper for local development
|
||||
kafka-ui:
|
||||
|
||||
33
src/Api.php
33
src/Api.php
@@ -6,17 +6,20 @@ namespace Siteworxpro\App;
|
||||
|
||||
use League\Route\Http\Exception\MethodNotAllowedException;
|
||||
use League\Route\Http\Exception\NotFoundException;
|
||||
use League\Route\RouteGroup;
|
||||
use League\Route\Router;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Siteworxpro\App\Controllers\HealthcheckController;
|
||||
use Siteworxpro\App\Controllers\IndexController;
|
||||
use Siteworxpro\App\Controllers\OpenApiController;
|
||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||
use Siteworxpro\App\Http\Middleware\CorsMiddleware;
|
||||
use Siteworxpro\App\Http\Middleware\JwtMiddleware;
|
||||
use Siteworxpro\App\Http\Middleware\ScopeMiddleware;
|
||||
use Siteworxpro\App\Http\Responses\NotFoundResponse;
|
||||
use Siteworxpro\App\Http\Responses\ServerErrorResponse;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
use Siteworxpro\App\Services\Facades\Logger;
|
||||
use Siteworxpro\HttpStatus\CodesEnum;
|
||||
use Spiral\RoadRunner\Http\PSR7Worker;
|
||||
use Spiral\RoadRunner\Worker;
|
||||
|
||||
@@ -69,8 +72,14 @@ class Api
|
||||
|
||||
$this->router = new Router();
|
||||
$this->router->get('/', IndexController::class . '::get');
|
||||
$this->router->post('/', IndexController::class . '::post');
|
||||
$this->router->get('/healthz', HealthcheckController::class . '::get');
|
||||
|
||||
$this->router->group('/.well-known', function (RouteGroup $router) {
|
||||
$router->get('/swagger.yaml', OpenApiController::class . '::get');
|
||||
$router->get('/swagger.json', OpenApiController::class . '::get');
|
||||
});
|
||||
|
||||
$this->router->middleware(new CorsMiddleware());
|
||||
$this->router->middleware(new JwtMiddleware());
|
||||
$this->router->middleware(new ScopeMiddleware());
|
||||
@@ -104,28 +113,20 @@ class Api
|
||||
$response = $this->router->handle($request);
|
||||
$this->worker->respond($response);
|
||||
} catch (MethodNotAllowedException | NotFoundException) {
|
||||
$uri = '';
|
||||
if (isset($request)) {
|
||||
$uri = $request->getUri()->getPath();
|
||||
}
|
||||
|
||||
$this->worker->respond(
|
||||
JsonResponseFactory::createJsonResponse(
|
||||
['status_code' => 404, 'reason_phrase' => 'Not Found'],
|
||||
CodesEnum::NOT_FOUND
|
||||
)
|
||||
JsonResponseFactory::createJsonResponse(new NotFoundResponse($uri))
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error($e->getMessage());
|
||||
Logger::error($e->getTraceAsString());
|
||||
|
||||
$json = ['status_code' => 500, 'reason_phrase' => 'Server Error'];
|
||||
if (Config::get("server.dev_mode")) {
|
||||
$json = [
|
||||
'status_code' => 500,
|
||||
'reason_phrase' => 'Server Error',
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
];
|
||||
}
|
||||
|
||||
$this->worker->respond(
|
||||
JsonResponseFactory::createJsonResponse($json, CodesEnum::INTERNAL_SERVER_ERROR)
|
||||
JsonResponseFactory::createJsonResponse(new ServerErrorResponse($e))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use Ahc\Cli\Application;
|
||||
use Siteworxpro\App\Cli\Commands\DemoCommand;
|
||||
use Siteworxpro\App\Cli\Commands\Queue\Start;
|
||||
use Siteworxpro\App\Cli\Commands\Queue\TestJob;
|
||||
use Siteworxpro\App\Helpers\Version;
|
||||
use Siteworxpro\App\Kernel;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
|
||||
@@ -21,7 +22,7 @@ class App
|
||||
public function __construct()
|
||||
{
|
||||
Kernel::boot();
|
||||
$this->app = new Application('Php-Template', Config::get('app.version') ?? 'dev-master');
|
||||
$this->app = new Application('Php-Template', Version::VERSION);
|
||||
|
||||
$this->app->add(new DemoCommand());
|
||||
$this->app->add(new Start());
|
||||
|
||||
@@ -6,7 +6,9 @@ namespace Siteworxpro\App\Controllers;
|
||||
|
||||
use League\Route\Http\Exception\NotFoundException;
|
||||
use Nyholm\Psr7\ServerRequest;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Siteworxpro\App\Helpers\Version;
|
||||
|
||||
/**
|
||||
* Class Controller
|
||||
@@ -15,6 +17,18 @@ use Psr\Http\Message\ResponseInterface;
|
||||
*
|
||||
* @package Siteworxpro\App\Controllers
|
||||
*/
|
||||
#[OA\Info(
|
||||
version: Version::VERSION,
|
||||
description: "This is a template API built using Siteworxpro framework.",
|
||||
title: "Siteworxpro Template API",
|
||||
contact: new OA\Contact(
|
||||
name: "Siteworxpro",
|
||||
url: "https://www.siteworxpro.com",
|
||||
email: "support@siteworxpro.com"
|
||||
),
|
||||
license: new OA\License('MIT', 'https://opensource.org/licenses/MIT')
|
||||
)]
|
||||
#[OA\Server(url: "https://localhost", description: "Local Server")]
|
||||
abstract class Controller implements ControllerInterface
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -8,9 +8,11 @@ use Illuminate\Database\PostgresConnection;
|
||||
use Nyholm\Psr7\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||
use Siteworxpro\App\Http\Responses\GenericResponse;
|
||||
use Siteworxpro\App\Models\Model;
|
||||
use Siteworxpro\App\Services\Facades\Redis;
|
||||
use Siteworxpro\HttpStatus\CodesEnum;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
/**
|
||||
* Class HealthcheckController
|
||||
@@ -22,8 +24,13 @@ use Siteworxpro\HttpStatus\CodesEnum;
|
||||
class HealthcheckController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handles the GET request for health check.
|
||||
*
|
||||
* @throws \JsonException
|
||||
*/
|
||||
#[OA\Get(path: '/healthz', tags: ['Healthcheck'])]
|
||||
#[OA\Response(response: '200', description: 'Healthcheck OK')]
|
||||
#[OA\Response(response: '503', description: 'Healthcheck Failed')]
|
||||
public function get(ServerRequest $request): ResponseInterface
|
||||
{
|
||||
try {
|
||||
@@ -47,7 +54,7 @@ class HealthcheckController extends Controller
|
||||
}
|
||||
|
||||
return JsonResponseFactory::createJsonResponse(
|
||||
['status_code' => 200, 'message' => 'Healthcheck OK']
|
||||
new GenericResponse('Healthcheck OK', CodesEnum::OK->value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,11 @@ namespace Siteworxpro\App\Controllers;
|
||||
use Nyholm\Psr7\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Siteworxpro\App\Attributes\Guards;
|
||||
use Siteworxpro\App\Docs\TokenSecurity;
|
||||
use Siteworxpro\App\Docs\UnauthorizedResponse;
|
||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Siteworxpro\App\Http\Responses\GenericResponse;
|
||||
|
||||
/**
|
||||
* Class IndexController
|
||||
@@ -24,18 +28,34 @@ class IndexController extends Controller
|
||||
#[Guards\Jwt]
|
||||
#[Guards\Scope(['get.index', 'status.check'])]
|
||||
#[Guards\RequireAllScopes]
|
||||
#[OA\Get(path: '/', security: [new TokenSecurity()], tags: ['Examples'])]
|
||||
#[OA\Response(
|
||||
response: '200',
|
||||
description: 'An Example Response',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/GenericResponse')
|
||||
)]
|
||||
#[UnauthorizedResponse]
|
||||
public function get(ServerRequest $request): ResponseInterface
|
||||
{
|
||||
return JsonResponseFactory::createJsonResponse(['status_code' => 200, 'message' => 'Server is running']);
|
||||
return JsonResponseFactory::createJsonResponse(new GenericResponse('Server is running'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the POST request for the index route.
|
||||
*
|
||||
* @throws \JsonException
|
||||
*/
|
||||
#[Guards\Jwt]
|
||||
#[Guards\Scope(['post.index'])]
|
||||
#[OA\Post(path: '/', security: [new TokenSecurity()], tags: ['Examples'])]
|
||||
#[OA\Response(
|
||||
response: '200',
|
||||
description: 'An Example Response',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/GenericResponse')
|
||||
)]
|
||||
#[UnauthorizedResponse]
|
||||
public function post(ServerRequest $request): ResponseInterface
|
||||
{
|
||||
return JsonResponseFactory::createJsonResponse(['status_code' => 200, 'message' => 'Server is running']);
|
||||
return JsonResponseFactory::createJsonResponse(new GenericResponse('POST request received'));
|
||||
}
|
||||
}
|
||||
|
||||
41
src/Controllers/OpenApiController.php
Normal file
41
src/Controllers/OpenApiController.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Controllers;
|
||||
|
||||
use Nyholm\Psr7\Response;
|
||||
use Nyholm\Psr7\ServerRequest;
|
||||
use OpenApi\Generator;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class OpenApiController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handles the GET request to generate and return the OpenAPI specification.
|
||||
*
|
||||
* @param ServerRequest $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function get(ServerRequest $request): ResponseInterface
|
||||
{
|
||||
$openapi = new Generator()->generate([
|
||||
__DIR__ . '/../Controllers',
|
||||
__DIR__ . '/../Models',
|
||||
__DIR__ . '/../Http/Responses',
|
||||
]);
|
||||
|
||||
$response = new Response();
|
||||
|
||||
if (
|
||||
$request->getHeaderLine('Accept') === 'application/json' ||
|
||||
str_contains($request->getUri()->getPath(), '.json')
|
||||
) {
|
||||
$response->getBody()->write($openapi->toJson());
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
$response->getBody()->write($openapi->toYaml());
|
||||
return $response->withHeader('Content-Type', 'application/x-yaml');
|
||||
}
|
||||
}
|
||||
19
src/Docs/TokenSecurity.php
Normal file
19
src/Docs/TokenSecurity.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Siteworxpro\App\Docs;
|
||||
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class TokenSecurity extends OA\SecurityScheme
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
securityScheme: 'bearerAuth',
|
||||
type: 'http',
|
||||
description: 'JWT based authentication using Bearer tokens.',
|
||||
bearerFormat: 'JWT',
|
||||
scheme: 'bearer'
|
||||
);
|
||||
}
|
||||
}
|
||||
26
src/Docs/UnauthorizedResponse.php
Normal file
26
src/Docs/UnauthorizedResponse.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Siteworxpro\App\Docs;
|
||||
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[\Attribute]
|
||||
class UnauthorizedResponse extends OA\Response
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
response: '401',
|
||||
description: 'Unauthorized - Authentication is required and has failed or has not yet been provided.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
properties: [
|
||||
new OA\Property(property: 'status_code', type: 'integer', example: 401),
|
||||
new OA\Property(property: 'message', type: 'string', example: 'Unauthorized'),
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,16 @@ class Dispatcher implements DispatcherContract, Arrayable
|
||||
$this->registerListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
foreach ($this->pushed as $event => $payload) {
|
||||
$this->dispatch($event, $payload);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register event listeners based on the ListensFor attribute.
|
||||
*
|
||||
|
||||
15
src/Events/Subscribers/Subscriber.php
Normal file
15
src/Events/Subscribers/Subscriber.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Events\Subscribers;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
|
||||
abstract class Subscriber implements SubscriberInterface, Arrayable
|
||||
{
|
||||
public function toArray(): array
|
||||
{
|
||||
return get_object_vars($this);
|
||||
}
|
||||
}
|
||||
10
src/Events/Subscribers/SubscriberInterface.php
Normal file
10
src/Events/Subscribers/SubscriberInterface.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Events\Subscribers;
|
||||
|
||||
interface SubscriberInterface
|
||||
{
|
||||
public function handle(string $eventName, mixed $payload): mixed;
|
||||
}
|
||||
10
src/Helpers/Version.php
Normal file
10
src/Helpers/Version.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Helpers;
|
||||
|
||||
class Version
|
||||
{
|
||||
public const string VERSION = 'dev-master';
|
||||
}
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Http;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Nyholm\Psr7\Response;
|
||||
use Siteworxpro\HttpStatus\CodesEnum;
|
||||
|
||||
@@ -17,13 +18,19 @@ class JsonResponseFactory
|
||||
/**
|
||||
* Create a JSON response with the given data and status code.
|
||||
*
|
||||
* @param array $data The data to include in the response.
|
||||
* @param array|Arrayable $data The data to include in the response.
|
||||
* @param CodesEnum $statusCode The HTTP status code for the response.
|
||||
* @return Response The JSON response.
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public static function createJsonResponse(array $data, CodesEnum $statusCode = CodesEnum::OK): Response
|
||||
{
|
||||
public static function createJsonResponse(
|
||||
array|Arrayable $data,
|
||||
CodesEnum $statusCode = CodesEnum::OK
|
||||
): Response {
|
||||
if ($data instanceof Arrayable) {
|
||||
$data = $data->toArray();
|
||||
}
|
||||
|
||||
return new Response(
|
||||
status: $statusCode->value,
|
||||
headers: [
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace Siteworxpro\App\Http\Middleware;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\WrapperClock;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Lcobucci\JWT\JwtFacade;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256 as Hmac256;
|
||||
@@ -28,6 +27,7 @@ use Siteworxpro\App\Attributes\Guards\Jwt;
|
||||
use Siteworxpro\App\Controllers\Controller;
|
||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
use Siteworxpro\App\Services\Facades\Guzzle;
|
||||
use Siteworxpro\App\Services\Facades\Redis;
|
||||
use Siteworxpro\HttpStatus\CodesEnum;
|
||||
|
||||
@@ -133,21 +133,21 @@ class JwtMiddleware extends Middleware
|
||||
}
|
||||
|
||||
return JsonResponseFactory::createJsonResponse([
|
||||
'status_code' => 401,
|
||||
'status_code' => CodesEnum::UNAUTHORIZED->value,
|
||||
'message' => 'Unauthorized: Invalid token',
|
||||
'errors' => $violations
|
||||
], CodesEnum::UNAUTHORIZED);
|
||||
} catch (InvalidTokenStructure) {
|
||||
// Token could not be parsed due to malformed structure.
|
||||
return JsonResponseFactory::createJsonResponse([
|
||||
'status_code' => 401,
|
||||
'status_code' => CodesEnum::UNAUTHORIZED->value,
|
||||
'message' => 'Unauthorized: Invalid token',
|
||||
], CodesEnum::UNAUTHORIZED);
|
||||
} catch (GuzzleException) {
|
||||
} catch (GuzzleException | \RuntimeException) {
|
||||
return JsonResponseFactory::createJsonResponse([
|
||||
'status_code' => 501,
|
||||
'message' => 'Token validation service unavailable',
|
||||
], CodesEnum::UNAUTHORIZED);
|
||||
'status_code' => CodesEnum::INTERNAL_SERVER_ERROR->value,
|
||||
'message' => 'Token validation service unavailable or unknown error',
|
||||
], CodesEnum::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
// Expose all token claims as request attributes for downstream consumers.
|
||||
@@ -170,7 +170,6 @@ class JwtMiddleware extends Middleware
|
||||
* @return SignedWith Signature constraint used during JWT parsing.
|
||||
*
|
||||
* @throws \RuntimeException When no signing key is configured.
|
||||
* @throws GuzzleException On JWKS key retrieval issues.
|
||||
* @throws \JsonException
|
||||
*/
|
||||
private function getSignedWith(string $token): SignedWith
|
||||
@@ -188,7 +187,7 @@ class JwtMiddleware extends Middleware
|
||||
} elseif (str_contains($keyConfig, '.well-known/')) {
|
||||
$jwt = explode('.', $token);
|
||||
if (count($jwt) !== 3) {
|
||||
throw new \RuntimeException('Invalid JWT structure for JWKS key retrieval.');
|
||||
throw new InvalidTokenStructure('Invalid JWT structure for JWKS key retrieval.');
|
||||
}
|
||||
$header = json_decode(base64_decode($jwt[0]), true, 512, JSON_THROW_ON_ERROR);
|
||||
$keyId = $header['kid'] ?? '0'; // Default to '0' if no kid present
|
||||
@@ -205,9 +204,6 @@ class JwtMiddleware extends Middleware
|
||||
return new SignedWith(new Hmac256(), $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
private function getJwksKey(string $url, string $keyId): Key
|
||||
{
|
||||
$cached = Redis::get('jwks_key_' . $keyId);
|
||||
@@ -215,15 +211,14 @@ class JwtMiddleware extends Middleware
|
||||
return InMemory::plainText($cached);
|
||||
}
|
||||
|
||||
$client = new Client();
|
||||
$openIdConfig = $client->get($url);
|
||||
$openIdConfig = Guzzle::get($url);
|
||||
$body = json_decode($openIdConfig->getBody()->getContents(), true, JSON_THROW_ON_ERROR);
|
||||
$jwksUri = $body['jwks_uri'] ?? '';
|
||||
if (empty($jwksUri)) {
|
||||
throw new \RuntimeException('JWKS URI not found in OpenID configuration.');
|
||||
}
|
||||
|
||||
$jwksResponse = $client->get($jwksUri);
|
||||
$jwksResponse = Guzzle::get($jwksUri);
|
||||
$jwksBody = json_decode(
|
||||
$jwksResponse->getBody()->getContents(),
|
||||
true,
|
||||
@@ -234,7 +229,7 @@ class JwtMiddleware extends Middleware
|
||||
$firstKey = array_filter(
|
||||
$jwksBody['keys'],
|
||||
fn($key) => $key['kid'] === $keyId
|
||||
)[0] ?? null;
|
||||
)[0] ?? $jwksBody['keys'][0] ?? null;
|
||||
|
||||
if (empty($firstKey)) {
|
||||
throw new \RuntimeException('No matching key found in JWKS for key ID: ' . $keyId);
|
||||
|
||||
32
src/Http/Responses/GenericResponse.php
Normal file
32
src/Http/Responses/GenericResponse.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Http\Responses;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Schema(
|
||||
schema: 'GenericResponse',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'Operation completed successfully.'),
|
||||
new OA\Property(property: 'status_code', type: 'integer', example: 200),
|
||||
]
|
||||
)]
|
||||
readonly class GenericResponse implements Arrayable
|
||||
{
|
||||
public function __construct(
|
||||
private string $message = '',
|
||||
private int $statusCode = 200
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'message' => $this->message,
|
||||
'status_code' => $this->statusCode,
|
||||
];
|
||||
}
|
||||
}
|
||||
40
src/Http/Responses/NotFoundResponse.php
Normal file
40
src/Http/Responses/NotFoundResponse.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Siteworxpro\App\Http\Responses;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Siteworxpro\HttpStatus\CodesEnum;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Schema(
|
||||
schema: 'NotFoundResponse',
|
||||
properties: [
|
||||
new OA\Property(
|
||||
property: 'message',
|
||||
type: 'string',
|
||||
example: 'The requested resource /api/resource was not found.'
|
||||
),
|
||||
new OA\Property(property: 'status_code', type: 'integer', example: 404),
|
||||
new OA\Property(
|
||||
property: 'context',
|
||||
description: 'Additional context about the not found error.',
|
||||
type: 'object',
|
||||
example: '{}'
|
||||
),
|
||||
]
|
||||
)]
|
||||
readonly class NotFoundResponse implements Arrayable
|
||||
{
|
||||
public function __construct(private string $uri, private array $context = [])
|
||||
{
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'status_code' => CodesEnum::NOT_FOUND->value,
|
||||
'message' => 'The requested resource ' . $this->uri . ' was not found.',
|
||||
'context' => $this->context,
|
||||
];
|
||||
}
|
||||
}
|
||||
56
src/Http/Responses/ServerErrorResponse.php
Normal file
56
src/Http/Responses/ServerErrorResponse.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Siteworxpro\App\Http\Responses;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
use Siteworxpro\HttpStatus\CodesEnum;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Schema(
|
||||
schema: 'ServerErrorResponse',
|
||||
properties: array(
|
||||
new OA\Property(property: 'message', type: 'string', example: 'An internal server error occurred.'),
|
||||
new OA\Property(property: 'status_code', type: 'integer', example: 500),
|
||||
new OA\Property(
|
||||
property: 'file',
|
||||
type: 'string',
|
||||
example: '/var/www/html/app/Http/Controllers/ExampleController.php'
|
||||
),
|
||||
new OA\Property(property: 'line', type: 'integer', example: 42),
|
||||
new OA\Property(
|
||||
property: 'trace',
|
||||
type: 'array',
|
||||
items: new OA\Items(type: 'string'),
|
||||
)
|
||||
)
|
||||
)]
|
||||
readonly class ServerErrorResponse implements Arrayable
|
||||
{
|
||||
public function __construct(private \Throwable $e, private array $context = [])
|
||||
{
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
if (Config::get('app.dev_mode')) {
|
||||
return [
|
||||
'status_code' => $this->e->getCode() != 0 ?
|
||||
$this->e->getCode() :
|
||||
CodesEnum::INTERNAL_SERVER_ERROR->value,
|
||||
'message' => $this->e->getMessage(),
|
||||
'file' => $this->e->getFile(),
|
||||
'line' => $this->e->getLine(),
|
||||
'trace' => $this->e->getTrace(),
|
||||
'context' => $this->context,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status_code' => $this->e->getCode() != 0 ?
|
||||
$this->e->getCode() :
|
||||
CodesEnum::INTERNAL_SERVER_ERROR->value,
|
||||
'message' => 'An internal server error occurred.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,13 @@ declare(strict_types=1);
|
||||
namespace Siteworxpro\App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Siteworxpro\App\Helpers\Ulid;
|
||||
|
||||
/**
|
||||
* Class User
|
||||
*
|
||||
* @property string $id
|
||||
* @property-read string $id
|
||||
* @property string $first_name
|
||||
* @property string $last_name
|
||||
* @property string $email
|
||||
@@ -19,6 +21,23 @@ use Carbon\Carbon;
|
||||
* @property-read string $full_name
|
||||
* @property-read string $formatted_email
|
||||
*/
|
||||
#[OA\Schema(
|
||||
schema: "User",
|
||||
properties: [
|
||||
new OA\Property(
|
||||
property: "id",
|
||||
description: "Unique identifier for the user",
|
||||
type: "string",
|
||||
format: "ulid",
|
||||
readOnly: true,
|
||||
example: '01KBD5WPZKYD77BYM2QD9NKG99'
|
||||
),
|
||||
new OA\Property(property: "first_name", type: "string"),
|
||||
new OA\Property(property: "last_name", type: "string"),
|
||||
new OA\Property(property: "email", type: "string", format: "email"),
|
||||
new OA\Property(property: "created_at", type: "string", format: "date-time"),
|
||||
]
|
||||
)]
|
||||
class User extends Model
|
||||
{
|
||||
protected $casts = [
|
||||
@@ -36,6 +55,12 @@ class User extends Model
|
||||
'password',
|
||||
];
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
parent::__construct($attributes);
|
||||
$this->attributes['id'] = $this->attributes['id'] ?? Ulid::generate();
|
||||
}
|
||||
|
||||
public function getFullNameAttribute(): string
|
||||
{
|
||||
return "$this->first_name $this->last_name";
|
||||
|
||||
28
src/Services/Facades/Guzzle.php
Normal file
28
src/Services/Facades/Guzzle.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Services\Facades;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Siteworxpro\App\Services\Facade;
|
||||
|
||||
/**
|
||||
* @method static Response get(string $uri, array $options = [])
|
||||
* @method static Response post(string $uri, array $options = [])
|
||||
* @method static Response put(string $uri, array $options = [])
|
||||
* @method static Response delete(string $uri, array $options = [])
|
||||
* @method static Response patch(string $uri, array $options = [])
|
||||
* @method static Response head(string $uri, array $options = [])
|
||||
* @method static PromiseInterface sendAsync(\Psr\Http\Message\RequestInterface $request, array $options = [])
|
||||
* @method static PromiseInterface requestAsync(string $method, string $uri, array $options = [])
|
||||
*/
|
||||
class Guzzle extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return Client::class;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class IndexControllerTest extends AbstractController
|
||||
$response = $controller->get($this->getMockRequest());
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('{"status_code":200,"message":"Server is running"}', (string)$response->getBody());
|
||||
$this->assertEquals('{"message":"Server is running","status_code":200}', (string)$response->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,6 +35,6 @@ class IndexControllerTest extends AbstractController
|
||||
$response = $controller->post($this->getMockRequest());
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('{"status_code":200,"message":"Server is running"}', (string)$response->getBody());
|
||||
$this->assertEquals('{"message":"POST request received","status_code":200}', (string)$response->getBody());
|
||||
}
|
||||
}
|
||||
|
||||
33
tests/Controllers/OpenApiControllerTest.php
Normal file
33
tests/Controllers/OpenApiControllerTest.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\Tests\Controllers;
|
||||
|
||||
use Siteworxpro\App\Controllers\OpenApiController;
|
||||
|
||||
class OpenApiControllerTest extends ControllerTest
|
||||
{
|
||||
public function testBuildsYaml(): void
|
||||
{
|
||||
$request = $this->getMockRequest('/.well-known/openapi.yaml');
|
||||
$controller = new OpenApiController();
|
||||
|
||||
$response = $controller->get($request);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertStringContainsString('openapi: 3.0.0', (string)$response->getBody());
|
||||
}
|
||||
|
||||
public function testBuildsJson(): void
|
||||
{
|
||||
$request = $this->getMockRequest(uri: '/.well-known/openapi.json');
|
||||
$controller = new OpenApiController();
|
||||
|
||||
$response = $controller->get($request);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('application/json', $response->getHeaderLine('Content-Type'));
|
||||
$this->assertNotFalse(json_decode($response->getBody()->getContents()));
|
||||
}
|
||||
}
|
||||
177
tests/Events/DispatcherTest.php
Normal file
177
tests/Events/DispatcherTest.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\Tests\Events;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Siteworxpro\Tests\Unit;
|
||||
|
||||
class DispatcherTest extends Unit
|
||||
{
|
||||
/**
|
||||
* @throws \Throwable
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function testRegistersListeners(): void
|
||||
{
|
||||
$dispatcher = $this->getContainer()->make('Siteworxpro\App\Events\Dispatcher');
|
||||
|
||||
$eventFired = false;
|
||||
$dispatcher->listen('TestEvent', function ($event) use (&$eventFired) {
|
||||
$this->assertEquals('TestEvent', $event);
|
||||
$eventFired = true;
|
||||
});
|
||||
|
||||
$dispatcher->dispatch('TestEvent');
|
||||
$this->assertTrue($eventFired, 'The TestEvent listener was not fired.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function testPushesEvents()
|
||||
{
|
||||
$dispatcher = $this->getContainer()->make('Siteworxpro\App\Events\Dispatcher');
|
||||
|
||||
$eventsFired = 0;
|
||||
$dispatcher->listen('PushedEvent1', function ($event) use (&$eventsFired) {
|
||||
$eventsFired++;
|
||||
$this->assertEquals('PushedEvent1', $event);
|
||||
});
|
||||
|
||||
$dispatcher->listen('PushedEvent2', function ($event) use (&$eventsFired) {
|
||||
$eventsFired++;
|
||||
$this->assertEquals('PushedEvent2', $event);
|
||||
});
|
||||
|
||||
$dispatcher->push('PushedEvent1');
|
||||
$dispatcher->push('PushedEvent2');
|
||||
|
||||
unset($dispatcher); // Trigger destructor
|
||||
$this->assertEquals(2, $eventsFired);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function testFlushEvent(): void
|
||||
{
|
||||
$dispatcher = $this->getContainer()->make('Siteworxpro\App\Events\Dispatcher');
|
||||
|
||||
$eventFired = false;
|
||||
$dispatcher->listen('FlushEvent', function ($event) use (&$eventFired) {
|
||||
$this->assertEquals('FlushEvent', $event);
|
||||
$eventFired = true;
|
||||
});
|
||||
|
||||
$dispatcher->push('FlushEvent');
|
||||
$dispatcher->flush('FlushEvent');
|
||||
|
||||
$this->assertTrue($eventFired, 'The FlushEvent listener was not fired.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function testHasListeners(): void
|
||||
{
|
||||
$dispatcher = $this->getContainer()->make('Siteworxpro\App\Events\Dispatcher');
|
||||
|
||||
$this->assertFalse(
|
||||
$dispatcher->hasListeners(
|
||||
'NonExistentEvent'
|
||||
),
|
||||
'Expected no listeners for NonExistentEvent.'
|
||||
);
|
||||
|
||||
$dispatcher->listen('ExistingEvent', function () {
|
||||
// Listener logic
|
||||
});
|
||||
|
||||
$this->assertTrue(
|
||||
$dispatcher->hasListeners(
|
||||
'ExistingEvent'
|
||||
),
|
||||
'Expected listeners for ExistingEvent.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function testForgetEvent(): void
|
||||
{
|
||||
$dispatcher = $this->getContainer()->make('Siteworxpro\App\Events\Dispatcher');
|
||||
|
||||
$eventFired = false;
|
||||
$dispatcher->listen('ForgetEvent', function ($event) use (&$eventFired) {
|
||||
$this->assertEquals('ForgetEvent', $event);
|
||||
$eventFired = true;
|
||||
});
|
||||
|
||||
$dispatcher->push('ForgetEvent');
|
||||
$dispatcher->forget('ForgetEvent');
|
||||
|
||||
unset($dispatcher); // Trigger destructor
|
||||
|
||||
$this->assertFalse($eventFired, 'The ForgetEvent listener was fired but should have been forgotten.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function testForgetPushed()
|
||||
{
|
||||
$this->expectNotToPerformAssertions();
|
||||
|
||||
$dispatcher = $this->getContainer()->make('Siteworxpro\App\Events\Dispatcher');
|
||||
|
||||
$dispatcher->listen('EventToForget', function () {
|
||||
$this->fail('The EventToForget listener was fired but should have been forgotten.');
|
||||
});
|
||||
|
||||
$dispatcher->push('EventToForget');
|
||||
$dispatcher->forgetPushed();
|
||||
|
||||
unset($dispatcher); // Trigger destructor
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function testToArray(): void
|
||||
{
|
||||
$dispatcher = $this->getContainer()->make('Siteworxpro\App\Events\Dispatcher');
|
||||
|
||||
$dispatcher->listen('ArrayEvent', function () {
|
||||
// Listener logic
|
||||
});
|
||||
|
||||
$arrayRepresentation = $dispatcher->toArray();
|
||||
$this->assertArrayHasKey('ArrayEvent', $arrayRepresentation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function testSubscriber()
|
||||
{
|
||||
$subscriber = $this->getMockBuilder('Siteworxpro\App\Events\Subscribers\Subscriber')
|
||||
->onlyMethods(['handle'])
|
||||
->getMock();
|
||||
|
||||
$subscriber->expects($this->once())
|
||||
->method('handle')
|
||||
->with('SubscribedEvent', [])
|
||||
->willReturn(null);
|
||||
|
||||
$dispatcher = $this->getContainer()->make('Siteworxpro\App\Events\Dispatcher');
|
||||
$dispatcher->subscribe($subscriber);
|
||||
|
||||
$dispatcher->dispatch('SubscribedEvent');
|
||||
}
|
||||
}
|
||||
21
tests/Facades/GuzzleTest.php
Normal file
21
tests/Facades/GuzzleTest.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\Tests\Facades;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Siteworxpro\App\Services\Facades\Guzzle;
|
||||
|
||||
class GuzzleTest extends AbstractFacade
|
||||
{
|
||||
protected function getFacadeClass(): string
|
||||
{
|
||||
return Guzzle::class;
|
||||
}
|
||||
|
||||
protected function getConcrete(): string
|
||||
{
|
||||
return Client::class;
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,63 @@ use Nyholm\Psr7\ServerRequest;
|
||||
use Siteworxpro\App\Attributes\Guards\Jwt;
|
||||
use Siteworxpro\App\Http\Middleware\JwtMiddleware;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
use Siteworxpro\App\Services\Facades\Guzzle;
|
||||
use Siteworxpro\App\Services\Facades\Redis;
|
||||
use Siteworxpro\HttpStatus\CodesEnum;
|
||||
|
||||
class JwtMiddlewareTest extends Middleware
|
||||
{
|
||||
private const string TEST_SIGNING_KEY = 'test_signing_key_123456444478901234';
|
||||
|
||||
private const string TEST_RSA_PRIVATE_KEY = <<<EOD
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEAqTheAdlelxJL0K15BqUEo0lBzY06P7J0PhMfPlg2fgIJH+ng
|
||||
ZmrpYFhBkj2L5Fnvxz0y58eu9WhhokwpS0GzgFIw+KfLV/WLX4PgionsQshrt0Pi
|
||||
XvthaSH1xuYtg2N13dVVTv3Au0BBFLUHMrQ+bO5hgvowHBNfFf0GaHLW2m0eZ2Um
|
||||
hWbtdv4HxrXBO5gI2N4UevyQ+inczN7RBZR6ZzyNoDO6Up6kS23/58zOruO+PGi7
|
||||
q9eb7hU+getpVgA29wEWMgT+N6c5n5AcENgM1sHxZK43GR5vhMGbVJqnrUsMGof7
|
||||
rT9Lxey3gjPS2r5nz2PNFcQ1i07QKDzvQHp2wwIDAQABAoIBAFMAC9QaWzP8TGWJ
|
||||
gNBKhnDU0MrSl5yAmlWMKYn52JiLxQ/7Ng7mJ5wTDe5986zIlDyEfwCCyAUk8qaZ
|
||||
drOsATBSoCSGoM1+6aKq26r4JYNILNVSHal64XegqZ2qbu6ADWMGbXZ2Ll9qD8Hp
|
||||
XSN4lxn0/q0wrAJJWh094zO+CDZP+zBbX9oHxb5JAVxjCaNW84sI6/6agXM5zzgK
|
||||
wcBt5Y0i8V8f7n9kg+CPNqY6BKg7o2ONFYTEVKuuEnVS/eupHQwBWExPCdxc85Tb
|
||||
YqFL0dmgehE0OTQ6FrEN7Xh6jE4GMJtWmTvBNpqhsMZ0i08tAZSPs+Us9rnppKkK
|
||||
T1SC2xECgYEA7yOv4C7dtHmFbn0YfnbBEfgvGAubv5jPDtZ5u6tUEhhU3rOcWexM
|
||||
Xhj7OFV4I8lbu2t7GY+2BR7Y2ikOLW9MrOGo6qWhsjTQuZs6QaRKObcPvl2s0LYY
|
||||
GxD1u84VjHPzID2pKVPqxaQ7KdcIaujAedWwAf4PV/uK2prKdGvzIksCgYEAtSau
|
||||
4Ml1UpXvKxiBcVKsHIoEO0g3NL1+wAbdStg8TFi+leCMJoPwZ01t64BTtHF+pgDP
|
||||
vn6VEgDSP3J4+W3dVhoajQeKBioT3MpDRP/qKDsImi2zJrg+hh9DMTlZd0Ab3EXv
|
||||
ycjw3FWRcpcU/1l261fA/m3QPwZikF2VlO/0cmkCgYEAvtefCuy718RHHObOPlZt
|
||||
O/bxNmJFOEEttOyql39iB1LNoDB8bTLruwh6q/lheEXAZDChO8P5gdqdOnUbMF0r
|
||||
Nqib0i6+fOYzUHw1oJ8I8UhLUyOUv7ciQ69kPC15+u2psCglMKscp/+pi3lk6VS4
|
||||
DkLfRKfI/PDsXgq72O8xSEMCgYEApukSnvngyQxvR1UYB7N19AHTLlA21bh4LjTk
|
||||
905QGMR4Lp6sY9yTyIsWabRe69bbK9d5kvsNHX52OpGeF6z8EJaSujklGtLwZDJV
|
||||
UyE9vn3OSkkrVdTTfz8U6Sj/XxpJ0Wb7LwCftVR+ZIgCh9kF8ohzwbqq8zdN39jq
|
||||
t0V1BWkCgYEA2Mk2gOdYAN8aZgydFYKhogY5UNK/CFpq7hhekEyt73uxzxguVpZn
|
||||
AJ9mq2L1CVJ5WqAUk2IzioeR7XAndntesbOafDuR4mhCUJhX+m/YQlKbTrs2dScR
|
||||
S88z05AnmQmr5eCbQmVULZGo9xeLDB+GDWvvjpQ+NWcha2uO0O0RTQY=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
EOD;
|
||||
|
||||
private const string TEST_JWKS_JSON = <<<EOD
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"alg": "RS256",
|
||||
"e": "AQAB",
|
||||
"ext": true,
|
||||
"key_ops": [
|
||||
"verify"
|
||||
],
|
||||
"kty": "RSA",
|
||||
"n": "qTheAdlelxJL0K15BqUEo0lBzY06P7J0PhMfPlg2fgIJH-ngZmrpYFhBkj2L5Fnvxz0y58eu9WhhokwpS0GzgFIw-KfLV_WLX4PgionsQshrt0PiXvthaSH1xuYtg2N13dVVTv3Au0BBFLUHMrQ-bO5hgvowHBNfFf0GaHLW2m0eZ2UmhWbtdv4HxrXBO5gI2N4UevyQ-inczN7RBZR6ZzyNoDO6Up6kS23_58zOruO-PGi7q9eb7hU-getpVgA29wEWMgT-N6c5n5AcENgM1sHxZK43GR5vhMGbVJqnrUsMGof7rT9Lxey3gjPS2r5nz2PNFcQ1i07QKDzvQHp2ww",
|
||||
"kid": "2o5IaHnjxYtkpNWEcdPlwnaRJnaCJ2k2LY2nR4z6cN4=",
|
||||
"use": "sig"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOD;
|
||||
|
||||
public function getClass(): object
|
||||
{
|
||||
return new class {
|
||||
@@ -51,7 +102,7 @@ class JwtMiddlewareTest extends Middleware
|
||||
$class = new class {
|
||||
public function getCallable(): array
|
||||
{
|
||||
return [ $this, 'index' ];
|
||||
return [$this, 'index'];
|
||||
}
|
||||
|
||||
public function index()
|
||||
@@ -208,6 +259,91 @@ class JwtMiddlewareTest extends Middleware
|
||||
$this->assertEquals(CodesEnum::OK->value, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function testJwtFromJwkEndpoint()
|
||||
{
|
||||
Config::set('jwt.audience', 'https://client-app.io');
|
||||
Config::set('jwt.issuer', 'https://api.my-awesome-app.io');
|
||||
|
||||
Redis::partialMock()->shouldReceive('get')->andReturn(null);
|
||||
Redis::shouldReceive('set')->andReturn('OK');
|
||||
Guzzle::partialMock()->shouldReceive('get')
|
||||
->with('https://test.com/.well-known/openid-configuration')
|
||||
->andReturn(new Response(200, [], json_encode([
|
||||
'jwks_uri' => 'https://test.com/keys'
|
||||
], JSON_THROW_ON_ERROR)));
|
||||
|
||||
Guzzle::shouldReceive('get')
|
||||
->with('https://test.com/keys')
|
||||
->andReturn(new Response(200, [], self::TEST_JWKS_JSON));
|
||||
|
||||
Config::set('jwt.signing_key', 'https://test.com/.well-known/openid-configuration');
|
||||
|
||||
$class = $this->getClass();
|
||||
|
||||
$handler = \Mockery::mock(Dispatcher::class);
|
||||
$handler->shouldReceive('getMiddlewareStack')
|
||||
->andReturn([$class]);
|
||||
$handler
|
||||
->shouldReceive('handle')
|
||||
->once()
|
||||
->andReturn(new Response(200));
|
||||
|
||||
$request = new ServerRequest('GET', '/');
|
||||
$request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwtRsa());
|
||||
$middleware = new JwtMiddleware();
|
||||
$response = $middleware->process($request, $handler);
|
||||
$this->assertEquals(CodesEnum::OK->value, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function testCatchesInvalidJwksUrl()
|
||||
{
|
||||
Config::set('jwt.signing_key', 'https://test.com/.well-known/openid-configuration');
|
||||
Redis::partialMock()->shouldReceive('get')->andReturn(null);
|
||||
Redis::shouldReceive('set')->andReturn('OK');
|
||||
Guzzle::partialMock()->shouldReceive('get')
|
||||
->with('https://test.com/.well-known/openid-configuration')
|
||||
->andReturn(new Response(200, [], json_encode([], JSON_THROW_ON_ERROR)));
|
||||
|
||||
|
||||
|
||||
$class = $this->getClass();
|
||||
|
||||
$handler = \Mockery::mock(Dispatcher::class);
|
||||
$handler->shouldReceive('getMiddlewareStack')
|
||||
->andReturn([$class]);
|
||||
|
||||
$request = new ServerRequest('GET', '/');
|
||||
$request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwtRsa());
|
||||
$middleware = new JwtMiddleware();
|
||||
$response = $middleware->process($request, $handler);
|
||||
$this->assertEquals(CodesEnum::INTERNAL_SERVER_ERROR->value, $response->getStatusCode());
|
||||
}
|
||||
|
||||
private function getJwtRsa(): string
|
||||
{
|
||||
$key = InMemory::plainText(self::TEST_RSA_PRIVATE_KEY);
|
||||
$signer = new \Lcobucci\JWT\Signer\Rsa\Sha256();
|
||||
$token = new JwtFacade()->issue(
|
||||
$signer,
|
||||
$key,
|
||||
static fn(
|
||||
Builder $builder,
|
||||
DateTimeImmutable $issuedAt
|
||||
): Builder => $builder
|
||||
->issuedBy('https://api.my-awesome-app.io')
|
||||
->permittedFor('https://client-app.io')
|
||||
->expiresAt($issuedAt->modify('+10 minutes'))
|
||||
);
|
||||
|
||||
return $token->toString();
|
||||
}
|
||||
|
||||
private function getJwt(): string
|
||||
{
|
||||
$key = InMemory::plainText(self::TEST_SIGNING_KEY);
|
||||
@@ -216,7 +352,7 @@ class JwtMiddlewareTest extends Middleware
|
||||
$token = new JwtFacade()->issue(
|
||||
$signer,
|
||||
$key,
|
||||
static fn (
|
||||
static fn(
|
||||
Builder $builder,
|
||||
DateTimeImmutable $issuedAt
|
||||
): Builder => $builder
|
||||
|
||||
22
tests/Http/Responses/NotFoundResponseTest.php
Normal file
22
tests/Http/Responses/NotFoundResponseTest.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Siteworxpro\Tests\Http\Responses;
|
||||
|
||||
use Siteworxpro\App\Http\Responses\NotFoundResponse;
|
||||
use Siteworxpro\Tests\Unit;
|
||||
|
||||
class NotFoundResponseTest extends Unit
|
||||
{
|
||||
public function testToArray(): void
|
||||
{
|
||||
$response = new NotFoundResponse('/api/resource', ['key' => 'value']);
|
||||
|
||||
$expected = [
|
||||
'status_code' => 404,
|
||||
'message' => 'The requested resource /api/resource was not found.',
|
||||
'context' => ['key' => 'value'],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $response->toArray());
|
||||
}
|
||||
}
|
||||
89
tests/Http/Responses/ServerErrorResponseTest.php
Normal file
89
tests/Http/Responses/ServerErrorResponseTest.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Siteworxpro\Tests\Http\Responses;
|
||||
|
||||
use Siteworxpro\App\Http\Responses\ServerErrorResponse;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
use Siteworxpro\Tests\Unit;
|
||||
|
||||
class ServerErrorResponseTest extends Unit
|
||||
{
|
||||
public function testToArrayInDevMode(): void
|
||||
{
|
||||
Config::set('app.dev_mode', true);
|
||||
|
||||
try {
|
||||
// Simulate an exception to generate a server error response
|
||||
throw new \Exception('A Test Error occurred.');
|
||||
} catch (\Exception $e) {
|
||||
$response = new ServerErrorResponse($e, ['operation' => 'data_processing']);
|
||||
|
||||
$expected = [
|
||||
'status_code' => 500,
|
||||
'message' => 'A Test Error occurred.',
|
||||
'context' => [
|
||||
'operation' => 'data_processing'
|
||||
],
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTrace(),
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $response->toArray());
|
||||
}
|
||||
}
|
||||
|
||||
public function testToArrayNotInDevMode(): void
|
||||
{
|
||||
try {
|
||||
throw new \Exception('A Test Error occurred.');
|
||||
} catch (\Exception $exception) {
|
||||
$response = new ServerErrorResponse($exception);
|
||||
|
||||
$expected = [
|
||||
'status_code' => 500,
|
||||
'message' => 'An internal server error occurred.',
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $response->toArray());
|
||||
}
|
||||
}
|
||||
|
||||
public function testToArrayIfCodeIsSet(): void
|
||||
{
|
||||
try {
|
||||
throw new \Exception('A Test Error occurred.', 1234);
|
||||
} catch (\Exception $exception) {
|
||||
$response = new ServerErrorResponse($exception);
|
||||
|
||||
$expected = [
|
||||
'status_code' => 1234,
|
||||
'message' => 'An internal server error occurred.',
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $response->toArray());
|
||||
}
|
||||
}
|
||||
|
||||
public function testToArrayIfCodeIsSetDevMode(): void
|
||||
{
|
||||
Config::set('app.dev_mode', true);
|
||||
|
||||
try {
|
||||
throw new \Exception('A Test Error occurred.', 1234);
|
||||
} catch (\Exception $exception) {
|
||||
$response = new ServerErrorResponse($exception);
|
||||
|
||||
$expected = [
|
||||
'status_code' => 1234,
|
||||
'message' => 'A Test Error occurred.',
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'trace' => $exception->getTrace(),
|
||||
'context' => [],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $response->toArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
30
tests/Models/UserTest.php
Normal file
30
tests/Models/UserTest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\Tests\Models;
|
||||
|
||||
use Siteworxpro\App\Models\User;
|
||||
use Siteworxpro\Tests\Unit;
|
||||
|
||||
class UserTest extends Unit
|
||||
{
|
||||
public function testFormatsName(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->first_name = 'John';
|
||||
$user->last_name = 'Doe';
|
||||
|
||||
$this->assertEquals('John Doe', $user->full_name);
|
||||
}
|
||||
|
||||
public function testFormatsEmail(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->first_name = 'Jane';
|
||||
$user->last_name = 'Smith';
|
||||
$user->email = 'jane.smith@email.com';
|
||||
|
||||
$this->assertEquals('Jane Smith <jane.smith@email.com>', $user->formatted_email);
|
||||
}
|
||||
}
|
||||
15
tests/ServiceProviders/DispatcherServiceProviderTest.php
Normal file
15
tests/ServiceProviders/DispatcherServiceProviderTest.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\Tests\ServiceProviders;
|
||||
|
||||
use Siteworxpro\App\Services\ServiceProviders\DispatcherServiceProvider;
|
||||
|
||||
class DispatcherServiceProviderTest extends AbstractServiceProvider
|
||||
{
|
||||
protected function getProviderClass(): string
|
||||
{
|
||||
return DispatcherServiceProvider::class;
|
||||
}
|
||||
}
|
||||
@@ -13,13 +13,25 @@ use Siteworxpro\App\Services\Facades\Config;
|
||||
|
||||
abstract class Unit extends TestCase
|
||||
{
|
||||
protected function getContainer(): Container
|
||||
{
|
||||
$container = Facade::getFacadeContainer();
|
||||
if ($container === null) {
|
||||
$container = new Container();
|
||||
Facade::setFacadeContainer($container);
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
$container = new Container();
|
||||
Facade::setFacadeContainer($container);
|
||||
$container = $this->getContainer();
|
||||
|
||||
$container->bind(SWConfig::class, function () {
|
||||
return SWConfig::load(__DIR__ . '/../config.php');
|
||||
|
||||
Reference in New Issue
Block a user