You've already forked Traefik-Redis-Api
done. going to bed now.
This commit is contained in:
14
.allowed-licenses.php
Normal file
14
.allowed-licenses.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Lendable\ComposerLicenseChecker\LicenseConfigurationBuilder;
|
||||||
|
|
||||||
|
return new LicenseConfigurationBuilder()
|
||||||
|
->addLicenses(
|
||||||
|
'MIT',
|
||||||
|
'BSD-2-Clause',
|
||||||
|
'BSD-3-Clause',
|
||||||
|
'Apache-2.0',
|
||||||
|
)
|
||||||
|
->build();
|
||||||
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.idea/
|
||||||
|
vendor/
|
||||||
|
.phpunit.cache/
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.idea/
|
||||||
|
vendor/
|
||||||
|
.phpunit.cache/
|
||||||
5
.gitlab-ci.yml
Normal file
5
.gitlab-ci.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
include:
|
||||||
|
- local: .gitlab/ci/stages.yml
|
||||||
|
- local: .gitlab/ci/tests.yml
|
||||||
|
- local: .gitlab/ci/libraries.yml
|
||||||
|
|
||||||
15
.gitlab/ci/libraries.yml
Normal file
15
.gitlab/ci/libraries.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
Install Composer Libraries:
|
||||||
|
stage: libraries
|
||||||
|
image: siteworxpro/composer:latest
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_TAG'
|
||||||
|
when: never
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push"'
|
||||||
|
when: always
|
||||||
|
- when: never
|
||||||
|
script:
|
||||||
|
- composer install --ignore-platform-reqs
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- vendor/
|
||||||
|
expire_in: 1 hour
|
||||||
3
.gitlab/ci/stages.yml
Normal file
3
.gitlab/ci/stages.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
stages:
|
||||||
|
- libraries
|
||||||
|
- tests
|
||||||
62
.gitlab/ci/tests.yml
Normal file
62
.gitlab/ci/tests.yml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
Unit Tests:
|
||||||
|
stage: tests
|
||||||
|
needs:
|
||||||
|
- Install Composer Libraries
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_TAG'
|
||||||
|
when: never
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push"'
|
||||||
|
when: always
|
||||||
|
- when: never
|
||||||
|
image: siteworxpro/composer
|
||||||
|
before_script: |
|
||||||
|
bin/pcov.sh
|
||||||
|
script: |
|
||||||
|
echo "Running unit tests..."
|
||||||
|
composer run tests:unit:coverage
|
||||||
|
artifacts:
|
||||||
|
expire_in: 1 day
|
||||||
|
paths:
|
||||||
|
- tests/reports/
|
||||||
|
|
||||||
|
Run License Check:
|
||||||
|
stage: tests
|
||||||
|
needs:
|
||||||
|
- Install Composer Libraries
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_TAG'
|
||||||
|
when: never
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push"'
|
||||||
|
when: on_success
|
||||||
|
- when: never
|
||||||
|
image: siteworxpro/composer
|
||||||
|
script:
|
||||||
|
- composer run tests:license
|
||||||
|
|
||||||
|
Run Code Lint:
|
||||||
|
stage: tests
|
||||||
|
needs:
|
||||||
|
- Install Composer Libraries
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_TAG'
|
||||||
|
when: never
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push"'
|
||||||
|
when: on_success
|
||||||
|
- when: never
|
||||||
|
image: siteworxpro/composer
|
||||||
|
script:
|
||||||
|
- composer run tests:lint
|
||||||
|
|
||||||
|
Run Code Sniffer:
|
||||||
|
stage: tests
|
||||||
|
needs:
|
||||||
|
- Install Composer Libraries
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_TAG'
|
||||||
|
when: never
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push"'
|
||||||
|
when: on_success
|
||||||
|
- when: never
|
||||||
|
image: siteworxpro/composer
|
||||||
|
script:
|
||||||
|
- composer run tests:phpstan
|
||||||
24
.rr.yaml
Normal file
24
.rr.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
server:
|
||||||
|
command: "php server.php"
|
||||||
|
|
||||||
|
rpc:
|
||||||
|
listen: tcp://127.0.0.1:6001
|
||||||
|
|
||||||
|
http:
|
||||||
|
pool:
|
||||||
|
allocate_timeout: 5s
|
||||||
|
reset_timeout: 5s
|
||||||
|
destroy_timeout: 5s
|
||||||
|
stream_timeout: 5s
|
||||||
|
num_workers: ${WORKERS:-4}
|
||||||
|
debug: ${DEBUG:-false}
|
||||||
|
|
||||||
|
address: 0.0.0.0:${HTTP_PORT:-9501}
|
||||||
|
access_logs: ${ACCESS_LOGS:-true}
|
||||||
|
|
||||||
|
logs:
|
||||||
|
encoding: json
|
||||||
|
level: ${LOG_LEVEL:-info}
|
||||||
|
mode: production
|
||||||
41
Dockerfile
Normal file
41
Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Use the RoadRunner image as a base for the first stage
|
||||||
|
FROM ghcr.io/roadrunner-server/roadrunner:2024.3.5 AS roadrunner
|
||||||
|
|
||||||
|
# Use the official Composer image as the base for the library stage
|
||||||
|
FROM siteworxpro/composer AS library
|
||||||
|
|
||||||
|
# Add Composer configuration files to the working directory
|
||||||
|
ADD composer.json composer.lock ./
|
||||||
|
|
||||||
|
# Install PHP dependencies, ignoring platform requirements and excluding development dependencies
|
||||||
|
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.6-alpine AS php
|
||||||
|
|
||||||
|
# Move the production PHP configuration file to the default location
|
||||||
|
RUN mv /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini \
|
||||||
|
&& apk add libpq-dev linux-headers --no-cache \
|
||||||
|
&& docker-php-ext-install pdo_pgsql sockets \
|
||||||
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
# Set the working directory to /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the RoadRunner binary from the first stage to the second stage
|
||||||
|
COPY --from=roadrunner /usr/bin/rr /usr/local/bin/rr
|
||||||
|
|
||||||
|
# Copy the installed PHP dependencies from the library stage
|
||||||
|
COPY --from=library /app/vendor /app/vendor
|
||||||
|
|
||||||
|
# Copy the RoadRunner configuration file and source
|
||||||
|
ADD src src/
|
||||||
|
ADD server.php .
|
||||||
|
ADD .rr.yaml .
|
||||||
|
ADD config.php .
|
||||||
|
|
||||||
|
EXPOSE 9501
|
||||||
|
|
||||||
|
# Entrypoint command to run the RoadRunner server with the specified configuration
|
||||||
|
ENTRYPOINT ["rr", "serve", "-c", ".rr.yaml", "-s"]
|
||||||
28
README.md
Normal file
28
README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Template
|
||||||
|
|
||||||
|
## Dev Environment
|
||||||
|
|
||||||
|
```shell
|
||||||
|
export PHP_IDE_CONFIG=serverName=localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --rm -v $(PWD):/app siteworxpro/composer install --ignore-platform-reqs
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --rm -v $(PWD):/app siteworxpro/composer run tests:all
|
||||||
|
```
|
||||||
|
### migrations
|
||||||
|
create a new migration
|
||||||
|
```shell
|
||||||
|
docker run --rm -v $(PWD):/app siteworxpro/migrate:v4.18.3 create -ext sql -dir /app/db/migrations -seq create_users_table
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
postgres://siteworxpro:password@localhost:5432/siteworxpro?sslmode=disable
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --rm -v $(PWD):/app siteworxpro/migrate:v4.18.3 -database "postgres://siteworxpro:password@localhost:5432/siteworxpro?sslmode=disable" -path /app/db/migrations up
|
||||||
|
```
|
||||||
19
bin/pcov.sh
Executable file
19
bin/pcov.sh
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
apk --no-cache add pcre-dev ${PHPIZE_DEPS}
|
||||||
|
|
||||||
|
git clone https://github.com/krakjoe/pcov.git
|
||||||
|
cd pcov || exec
|
||||||
|
phpize
|
||||||
|
./configure --enable-pcov
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
|
||||||
|
echo "extension=pcov.so" > /usr/local/etc/php/conf.d/docker-php-ext-pcov.ini
|
||||||
|
echo "pcov.enabled=1" >> /usr/local/etc/php/conf.d/docker-php-ext-pcov.ini
|
||||||
|
echo "pcov.directory=." >> /usr/local/etc/php/conf.d/docker-php-ext-pcov.ini
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
cd ..
|
||||||
|
rm -rf pcov
|
||||||
24
bin/xdebug.sh
Executable file
24
bin/xdebug.sh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
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
|
||||||
|
phpize
|
||||||
|
./configure --enable-xdebug
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
|
||||||
|
echo "
|
||||||
|
zend_extension=xdebug.so
|
||||||
|
xdebug.mode=debug
|
||||||
|
xdebug.start_with_request = yes
|
||||||
|
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
|
||||||
68
composer.json
Normal file
68
composer.json
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name": "siteworxpro/app",
|
||||||
|
"type": "project",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Siteworxpro\\App\\": "src/",
|
||||||
|
"Siteworxpro\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^8.4",
|
||||||
|
"league/route": "^6.2",
|
||||||
|
"illuminate/database": "^12.10",
|
||||||
|
"spiral/roadrunner-http": "^3.5",
|
||||||
|
"nyholm/psr7": "^1.8",
|
||||||
|
"illuminate/support": "^v12.10.2",
|
||||||
|
"roadrunner-php/app-logger": "^1.2",
|
||||||
|
"siteworxpro/config": "^1.1",
|
||||||
|
"predis/predis": "^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^12.1",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"squizlabs/php_codesniffer": "^3.12",
|
||||||
|
"lendable/composer-license-checker": "^1.2",
|
||||||
|
"phpstan/phpstan": "^2.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"tests:all": [
|
||||||
|
"composer run-script tests:unit",
|
||||||
|
"composer run-script tests:lint",
|
||||||
|
"composer run-script tests:license",
|
||||||
|
"composer run-script tests:phpstan"
|
||||||
|
],
|
||||||
|
"tests:unit": [
|
||||||
|
"phpunit --colors=always --display-deprecations tests "
|
||||||
|
],
|
||||||
|
"tests:unit:coverage": [
|
||||||
|
"phpunit --colors=always --display-deprecations --coverage-html tests/reports/html tests "
|
||||||
|
],
|
||||||
|
"tests:lint": [
|
||||||
|
"phpcs ./src --standard=PSR12 --colors -v",
|
||||||
|
"phpcs ./tests --standard=PSR12 --colors -v"
|
||||||
|
],
|
||||||
|
"tests:lint:fix": [
|
||||||
|
"phpcbf ./src --standard=PSR12 --colors -v",
|
||||||
|
"phpcbf ./tests --standard=PSR12 --colors -v"
|
||||||
|
],
|
||||||
|
"tests:license": [
|
||||||
|
"composer-license-checker"
|
||||||
|
],
|
||||||
|
"tests:phpstan": [
|
||||||
|
"phpstan analyse --level 4 ./src/ -c phpstan.neon"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"repositories": {
|
||||||
|
"git.siteworxpro.com/24": {
|
||||||
|
"type": "composer",
|
||||||
|
"url": "https://git.siteworxpro.com/api/v4/group/24/-/packages/composer/packages.json",
|
||||||
|
"options": {
|
||||||
|
"ssl": {
|
||||||
|
"verify_peer": false,
|
||||||
|
"allow_self_signed": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5072
composer.lock
generated
Normal file
5072
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
config.php
Normal file
39
config.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Siteworxpro\App\Helpers\Env;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server configuration.
|
||||||
|
*/
|
||||||
|
'server' => [
|
||||||
|
'port' => Env::get('HTTP_PORT', 9501, 'int'),
|
||||||
|
'dev_mode' => Env::get('DEV_MODE', false, 'bool'),
|
||||||
|
'debug' => Env::get('DEBUG', false, 'bool'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The database configuration.
|
||||||
|
*/
|
||||||
|
'db' => [
|
||||||
|
'driver' => Env::get('DB_DRIVER', 'pgsql'),
|
||||||
|
'host' => Env::get('DB_HOST', 'localhost'),
|
||||||
|
'database' => Env::get('DB_DATABASE', 'siteworxpro'),
|
||||||
|
'username' => Env::get('DB_USERNAME', 'siteworxpro'),
|
||||||
|
'password' => Env::get('DB_PASSWORD', 'password'),
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
'cors' => [
|
||||||
|
'allowed_origins' => Env::get('CORS_ALLOWED_ORIGINS', 'localhost:3000'),
|
||||||
|
'allow_credentials' => Env::get('CORS_ALLOW_CREDENTIALS', true, 'bool'),
|
||||||
|
'max_age' => Env::get('CORS_MAX_AGE', ''),
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'host' => Env::get('REDIS_HOST', 'localhost'),
|
||||||
|
'port' => Env::get('REDIS_PORT', 6379, 'int'),
|
||||||
|
'database' => Env::get('REDIS_DATABASE', 0, 'int'),
|
||||||
|
]
|
||||||
|
];
|
||||||
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#volumes:
|
||||||
|
# pgdata: {}
|
||||||
|
|
||||||
|
services:
|
||||||
|
dev-runtime:
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
entrypoint: "/bin/sh -c 'while true; do sleep 30; done;'"
|
||||||
|
environment:
|
||||||
|
PHP_IDE_CONFIG: serverName=localhost
|
||||||
|
WORKERS: 1
|
||||||
|
HTTP_PORT: 8080
|
||||||
|
DEBUG: 1
|
||||||
|
REDIS_HOST: 192.168.1.30
|
||||||
|
|
||||||
|
# migrations:
|
||||||
|
# image: siteworxpro/migrate:v4.18.3
|
||||||
|
# restart: no
|
||||||
|
# volumes:
|
||||||
|
# - .:/app
|
||||||
|
# command: "-database 'postgres://${DB_DATABASE-siteworxpro}:${DB_PASSWORD-password}@${DB_HOST-postgres}:5432/siteworxpro?sslmode=disable' -path /app/db/migrations up"
|
||||||
|
#
|
||||||
|
# postgres:
|
||||||
|
# image: postgres:latest
|
||||||
|
# environment:
|
||||||
|
# POSTGRES_USER: ${DB_USERNAME:-siteworxpro}
|
||||||
|
# POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
|
||||||
|
# POSTGRES_DB: ${DB_DATABASE:-siteworxpro}
|
||||||
|
# ports:
|
||||||
|
# - "5432:5432"
|
||||||
|
# volumes:
|
||||||
|
# - pgdata:/var/lib/postgresql/data
|
||||||
3
phpstan.neon
Normal file
3
phpstan.neon
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
parameters:
|
||||||
|
ignoreErrors:
|
||||||
|
- '#Static call to instance method#'
|
||||||
24
phpunit.xml
Normal file
24
phpunit.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
cacheDirectory=".phpunit.cache"
|
||||||
|
executionOrder="depends,defects"
|
||||||
|
beStrictAboutCoverageMetadata="true"
|
||||||
|
beStrictAboutOutputDuringTests="true"
|
||||||
|
displayDetailsOnPhpunitDeprecations="true"
|
||||||
|
failOnPhpunitDeprecation="true"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnWarning="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="default">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
|
||||||
|
<include>
|
||||||
|
<directory>src</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
||||||
17
server.php
Normal file
17
server.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Siteworxpro\App\Server;
|
||||||
|
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Instantiate the ExternalServer class
|
||||||
|
$server = new Server();
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
try {
|
||||||
|
$server->startServer();
|
||||||
|
} catch (JsonException $e) {
|
||||||
|
echo $e->getMessage();
|
||||||
|
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
47
src/Controllers/Controller.php
Normal file
47
src/Controllers/Controller.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Controllers;
|
||||||
|
|
||||||
|
use League\Route\Http\Exception\NotFoundException;
|
||||||
|
use Nyholm\Psr7\ServerRequest;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
abstract class Controller implements ControllerInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ServerRequest $request
|
||||||
|
* @return ResponseInterface
|
||||||
|
* @throws NotFoundException
|
||||||
|
*/
|
||||||
|
public function get(ServerRequest $request): ResponseInterface
|
||||||
|
{
|
||||||
|
throw new NotFoundException("not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotFoundException
|
||||||
|
*/
|
||||||
|
public function post(ServerRequest $request): ResponseInterface
|
||||||
|
{
|
||||||
|
throw new NotFoundException("not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotFoundException
|
||||||
|
*/
|
||||||
|
public function delete(ServerRequest $request): ResponseInterface
|
||||||
|
{
|
||||||
|
throw new NotFoundException("not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotFoundException
|
||||||
|
*/
|
||||||
|
public function patch(ServerRequest $request): ResponseInterface
|
||||||
|
{
|
||||||
|
throw new NotFoundException("not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/Controllers/ControllerInterface.php
Normal file
43
src/Controllers/ControllerInterface.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Controllers;
|
||||||
|
|
||||||
|
use Nyholm\Psr7\ServerRequest;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
interface ControllerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the request and return a response.
|
||||||
|
*
|
||||||
|
* @param ServerRequest $request The request data.
|
||||||
|
* @return mixed The response data.
|
||||||
|
*/
|
||||||
|
public function get(ServerRequest $request): ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the request and return a response.
|
||||||
|
*
|
||||||
|
* @param ServerRequest $request The request data.
|
||||||
|
* @return mixed The response data.
|
||||||
|
*/
|
||||||
|
public function post(ServerRequest $request): ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the request and return a response.
|
||||||
|
*
|
||||||
|
* @param ServerRequest $request The request data.
|
||||||
|
* @return mixed The response data.
|
||||||
|
*/
|
||||||
|
public function delete(ServerRequest $request): ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the request and return a response.
|
||||||
|
*
|
||||||
|
* @param ServerRequest $request The request data.
|
||||||
|
* @return mixed The response data.
|
||||||
|
*/
|
||||||
|
public function patch(ServerRequest $request): ResponseInterface;
|
||||||
|
}
|
||||||
63
src/Controllers/MiddlewaresController.php
Normal file
63
src/Controllers/MiddlewaresController.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Controllers;
|
||||||
|
|
||||||
|
use Nyholm\Psr7\ServerRequest;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||||
|
use Siteworxpro\App\Traefik\EntityEnum;
|
||||||
|
use Siteworxpro\App\Traefik\RedisClient;
|
||||||
|
|
||||||
|
class MiddlewaresController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function get(ServerRequest $request): ResponseInterface
|
||||||
|
{
|
||||||
|
if ($request->getAttribute('id') !== null) {
|
||||||
|
$name = $request->getAttribute('id');
|
||||||
|
|
||||||
|
return JsonResponseFactory::createJsonResponse(RedisClient::getMiddleware($name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonResponseFactory::createJsonResponse(RedisClient::getAllMiddlewares());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function post(ServerRequest $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$name = $request->getAttribute('id');
|
||||||
|
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
|
||||||
|
if (empty($data)) {
|
||||||
|
return JsonResponseFactory::createJsonResponse(['error' => 'Middleware is invalid'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
RedisClient::createOrReplace($name, $data, EntityEnum::MIDDLEWARE);
|
||||||
|
|
||||||
|
return JsonResponseFactory::createJsonResponse(['message' => 'Middleware added successfully']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function delete(ServerRequest $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$name = $request->getAttribute('id');
|
||||||
|
|
||||||
|
if (empty($name)) {
|
||||||
|
return JsonResponseFactory::createJsonResponse(['error' => 'Middleware is invalid'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
RedisClient::deleteAllKeys($name, EntityEnum::MIDDLEWARE);
|
||||||
|
|
||||||
|
return JsonResponseFactory::createJsonResponse(['message' => 'Middleware deleted successfully']);
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/Controllers/RoutesController.php
Normal file
81
src/Controllers/RoutesController.php
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Controllers;
|
||||||
|
|
||||||
|
use Nyholm\Psr7\ServerRequest;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||||
|
use Siteworxpro\App\Traefik\EntityEnum;
|
||||||
|
use Siteworxpro\App\Traefik\RedisClient;
|
||||||
|
|
||||||
|
class RoutesController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function get(ServerRequest $request): ResponseInterface
|
||||||
|
{
|
||||||
|
if ($request->getAttribute('id') !== null) {
|
||||||
|
$name = $request->getAttribute('id');
|
||||||
|
|
||||||
|
return JsonResponseFactory::createJsonResponse(RedisClient::getRouter($name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonResponseFactory::createJsonResponse(RedisClient::getAllRouters());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function post(ServerRequest $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
|
||||||
|
$name = $request->getAttribute('id');
|
||||||
|
|
||||||
|
if (empty($data['service'])) {
|
||||||
|
return JsonResponseFactory::createJsonResponse(['error' => 'Service is required'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($data['rule'])) {
|
||||||
|
return JsonResponseFactory::createJsonResponse(['error' => 'Rule is required'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
RedisClient::createOrReplace($name, $data, EntityEnum::ROUTER);
|
||||||
|
|
||||||
|
return JsonResponseFactory::createJsonResponse(['message' => 'Router created successfully']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function delete(ServerRequest $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$name = $request->getAttribute('id');
|
||||||
|
|
||||||
|
RedisClient::deleteAllKeys($name, EntityEnum::ROUTER);
|
||||||
|
|
||||||
|
return JsonResponseFactory::createJsonResponse(['message' => 'Router deleted successfully']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function patch(ServerRequest $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$name = $request->getAttribute('id');
|
||||||
|
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
|
||||||
|
try {
|
||||||
|
RedisClient::patchEntity($name, $data, EntityEnum::ROUTER);
|
||||||
|
} catch (\InvalidArgumentException) {
|
||||||
|
return JsonResponseFactory::createJsonResponse(['error' => 'Router not found'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonResponseFactory::createJsonResponse(['message' => 'Router updated successfully']);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/Controllers/ServicesController.php
Normal file
63
src/Controllers/ServicesController.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Controllers;
|
||||||
|
|
||||||
|
use Nyholm\Psr7\ServerRequest;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||||
|
use Siteworxpro\App\Traefik\EntityEnum;
|
||||||
|
use Siteworxpro\App\Traefik\RedisClient;
|
||||||
|
|
||||||
|
class ServicesController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function get(ServerRequest $request): ResponseInterface
|
||||||
|
{
|
||||||
|
if ($request->getAttribute('id') !== null) {
|
||||||
|
$name = $request->getAttribute('id');
|
||||||
|
|
||||||
|
return JsonResponseFactory::createJsonResponse(RedisClient::getService($name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonResponseFactory::createJsonResponse(RedisClient::getAllServices());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function post(ServerRequest $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
|
||||||
|
$name = $request->getAttribute('id');
|
||||||
|
|
||||||
|
if (empty($data['loadbalancer'])) {
|
||||||
|
return JsonResponseFactory::createJsonResponse(['error' => 'loadbalancer is required'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
RedisClient::createOrReplace($name, $data, EntityEnum::SERVICE);
|
||||||
|
|
||||||
|
return JsonResponseFactory::createJsonResponse(['message' => 'Service updated successfully']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function delete(ServerRequest $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$name = $request->getAttribute('id');
|
||||||
|
|
||||||
|
if (empty($name)) {
|
||||||
|
return JsonResponseFactory::createJsonResponse(['error' => 'Service name is required'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
RedisClient::deleteAllKeys($name, EntityEnum::SERVICE);
|
||||||
|
|
||||||
|
return JsonResponseFactory::createJsonResponse(['message' => 'Service deleted successfully']);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/Facades/Config.php
Normal file
57
src/Facades/Config.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Facades;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
use Siteworx\Config\Exception\EmptyDirectoryException;
|
||||||
|
use Siteworx\Config\Exception\FileNotFoundException;
|
||||||
|
use Siteworx\Config\Exception\UnsupportedFormatException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Config
|
||||||
|
*
|
||||||
|
* This class serves as a facade for the configuration settings of the application.
|
||||||
|
* It extends the Facade class from the Illuminate\Support\Facades namespace.
|
||||||
|
*
|
||||||
|
* @method static bool | string | int get(string $key) Retrieve the configuration value for the given key.
|
||||||
|
*
|
||||||
|
* @package Siteworx\App\Facades
|
||||||
|
*/
|
||||||
|
class Config extends Facade
|
||||||
|
{
|
||||||
|
protected static $cached = false;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws UnsupportedFormatException
|
||||||
|
* @throws FileNotFoundException
|
||||||
|
* @throws EmptyDirectoryException
|
||||||
|
*/
|
||||||
|
public static function getFacadeRoot(): \Siteworx\Config\Config
|
||||||
|
{
|
||||||
|
if (self::$resolvedInstance !== null) {
|
||||||
|
try {
|
||||||
|
$config = self::resolveFacadeInstance(self::getFacadeAccessor());
|
||||||
|
if ($config instanceof \Siteworx\Config\Config) {
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
} catch (BindingResolutionException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Siteworx\Config\Config::load(__DIR__ . '/../../config.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the registered name of the component.
|
||||||
|
*
|
||||||
|
* @return string The name of the component.
|
||||||
|
*/
|
||||||
|
protected static function getFacadeAccessor(): string
|
||||||
|
{
|
||||||
|
return \Siteworx\Config\Config::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/Facades/Logger.php
Normal file
51
src/Facades/Logger.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Facades;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
use RoadRunner\Logger\Logger as RRLogger;
|
||||||
|
use Spiral\Goridge\RPC\RPC;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Logger
|
||||||
|
*
|
||||||
|
* This class serves as a facade for the Monolog logger.
|
||||||
|
* It extends the Facade class from the Illuminate\Support\Facades namespace.
|
||||||
|
*
|
||||||
|
* @method static debug(string $message, array $context = []) Log an informational message.
|
||||||
|
* @method static info(string $message, array $context = []) Log an informational message.
|
||||||
|
* @method static error(string $message, array $context = []) Log an error message.
|
||||||
|
* @method static warning(string $message, array $context = []) Log a warning message.
|
||||||
|
*
|
||||||
|
* @package Siteworxpro\App\Facades
|
||||||
|
*/
|
||||||
|
class Logger extends Facade
|
||||||
|
{
|
||||||
|
public static function getFacadeRoot(): RRLogger
|
||||||
|
{
|
||||||
|
if (self::$resolvedInstance !== null) {
|
||||||
|
$logger = self::resolveFacadeInstance(self::getFacadeAccessor());
|
||||||
|
|
||||||
|
if ($logger instanceof RRLogger) {
|
||||||
|
return $logger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$rpc = RPC::create('tcp://127.0.0.1:6001');
|
||||||
|
|
||||||
|
return new RRLogger($rpc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the registered name of the component.
|
||||||
|
*
|
||||||
|
* @return string The name of the component.
|
||||||
|
*/
|
||||||
|
protected static function getFacadeAccessor(): string
|
||||||
|
{
|
||||||
|
return RRLogger::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/Facades/Redis.php
Normal file
53
src/Facades/Redis.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Facades;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
use Predis\Client;
|
||||||
|
use Predis\Response\Status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Facade for the Redis client.
|
||||||
|
*
|
||||||
|
* This class provides a static interface to interact with the Redis client.
|
||||||
|
*
|
||||||
|
* @method static array scan($cursor, ?array $options = null)
|
||||||
|
* @method static string|null get(string $key)
|
||||||
|
* @method static Status|null set(string $key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
|
||||||
|
* @method static array keys(string $pattern)
|
||||||
|
* @method static int del(string $key)
|
||||||
|
*/
|
||||||
|
class Redis extends Facade
|
||||||
|
{
|
||||||
|
|
||||||
|
public static function getFacadeRoot(): Client
|
||||||
|
{
|
||||||
|
if (self::$resolvedInstance !== null) {
|
||||||
|
$redis = self::resolveFacadeInstance(self::getFacadeAccessor());
|
||||||
|
|
||||||
|
if ($redis instanceof Client) {
|
||||||
|
return $redis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new Redis client instance if not already resolved
|
||||||
|
return new Client([
|
||||||
|
'scheme' => 'tcp',
|
||||||
|
'host' => Config::get('redis.host'),
|
||||||
|
'port' => Config::get('redis.port'),
|
||||||
|
'database' => Config::get('redis.database'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the registered name of the component.
|
||||||
|
*
|
||||||
|
* @return string The name of the component.
|
||||||
|
*/
|
||||||
|
protected static function getFacadeAccessor(): string
|
||||||
|
{
|
||||||
|
return Client::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/Helpers/Env.php
Normal file
26
src/Helpers/Env.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Helpers;
|
||||||
|
|
||||||
|
abstract class Env
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $key
|
||||||
|
* @param null $default
|
||||||
|
* @param string $castTo
|
||||||
|
* @return float|bool|int|string
|
||||||
|
*/
|
||||||
|
public static function get(string $key, $default = null, string $castTo = 'string'): float | bool | int | string
|
||||||
|
{
|
||||||
|
$env = getenv($key) !== false ? getenv($key) : $default;
|
||||||
|
|
||||||
|
return match ($castTo) {
|
||||||
|
'bool', 'boolean' => (bool) $env,
|
||||||
|
'int', 'integer' => (int) $env,
|
||||||
|
'float' => (float) $env,
|
||||||
|
default => (string) $env,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/Http/JsonResponseFactory.php
Normal file
34
src/Http/JsonResponseFactory.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Http;
|
||||||
|
|
||||||
|
use Nyholm\Psr7\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class JsonResponseFactory
|
||||||
|
*
|
||||||
|
* A factory class for creating JSON responses.
|
||||||
|
*/
|
||||||
|
class JsonResponseFactory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a JSON response with the given data and status code.
|
||||||
|
*
|
||||||
|
* @param array $data The data to include in the response.
|
||||||
|
* @param int $statusCode The HTTP status code for the response.
|
||||||
|
* @return Response The JSON response.
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public static function createJsonResponse(array $data, int $statusCode = 200): Response
|
||||||
|
{
|
||||||
|
return new Response(
|
||||||
|
status: $statusCode,
|
||||||
|
headers: [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
],
|
||||||
|
body: json_encode($data, JSON_THROW_ON_ERROR)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/Http/Middleware/CorsMiddleware.php
Normal file
72
src/Http/Middleware/CorsMiddleware.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Http\Middleware;
|
||||||
|
|
||||||
|
use Nyholm\Psr7\Response;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use Siteworxpro\App\Facades\Config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class CorsMiddleware
|
||||||
|
*
|
||||||
|
* Middleware to handle CORS (Cross-Origin Resource Sharing) requests.
|
||||||
|
* It checks the origin of the request and sets appropriate CORS headers
|
||||||
|
* in the response.
|
||||||
|
*/
|
||||||
|
class CorsMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Process the incoming request and add CORS headers to the response.
|
||||||
|
*
|
||||||
|
* @param ServerRequestInterface $request The incoming request.
|
||||||
|
* @param RequestHandlerInterface $handler The request handler.
|
||||||
|
* @return ResponseInterface The response with CORS headers.
|
||||||
|
*/
|
||||||
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
|
{
|
||||||
|
$origin = $request->getHeaderLine('Origin');
|
||||||
|
$allowedOrigins = array_map(
|
||||||
|
'trim',
|
||||||
|
explode(
|
||||||
|
',',
|
||||||
|
Config::get('cors.allowed_origins')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$allowOrigin = in_array($origin, $allowedOrigins, true)
|
||||||
|
? $origin
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if ($request->getMethod() === 'OPTIONS') {
|
||||||
|
$response = new Response(204);
|
||||||
|
} else {
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($allowOrigin === null) {
|
||||||
|
return $response; // Do not add CORS headers if origin is not allowed.
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $response
|
||||||
|
->withHeader('Access-Control-Allow-Origin', $allowOrigin)
|
||||||
|
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
|
||||||
|
->withHeader(
|
||||||
|
'Access-Control-Allow-Headers',
|
||||||
|
$request->getHeaderLine('Access-Control-Request-Headers')
|
||||||
|
?: 'Content-Type, Authorization'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Config::get('cors.allow_credentials') === true) {
|
||||||
|
$response = $response->withHeader('Access-Control-Allow-Credentials', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
$maxAge = Config::get('cors.max_age') ?: '86400'; // Use correct configuration key.
|
||||||
|
|
||||||
|
return $response->withHeader('Access-Control-Max-Age', $maxAge);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/Models/Model.php
Normal file
11
src/Models/Model.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model as ORM;
|
||||||
|
|
||||||
|
abstract class Model extends ORM
|
||||||
|
{
|
||||||
|
}
|
||||||
198
src/Server.php
Normal file
198
src/Server.php
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App;
|
||||||
|
|
||||||
|
use Illuminate\Container\Container;
|
||||||
|
use Illuminate\Database\Capsule\Manager;
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
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\MiddlewaresController;
|
||||||
|
use Siteworxpro\App\Controllers\RoutesController;
|
||||||
|
use Siteworxpro\App\Controllers\ServicesController;
|
||||||
|
use Siteworxpro\App\Facades\Config;
|
||||||
|
use Siteworxpro\App\Facades\Logger;
|
||||||
|
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||||
|
use Siteworxpro\App\Http\Middleware\CorsMiddleware;
|
||||||
|
use Spiral\RoadRunner\Http\PSR7Worker;
|
||||||
|
use Spiral\RoadRunner\Worker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Server
|
||||||
|
*
|
||||||
|
* This class represents the main server application.
|
||||||
|
* It handles incoming HTTP requests, routes them to the appropriate handlers,
|
||||||
|
* and manages the server lifecycle.
|
||||||
|
*
|
||||||
|
* @package Siteworxpro\App
|
||||||
|
*/
|
||||||
|
class Server
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Router The router instance for handling routes.
|
||||||
|
*/
|
||||||
|
protected Router $router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var PSR7Worker The PSR-7 worker instance for handling HTTP requests.
|
||||||
|
*/
|
||||||
|
protected PSR7Worker $worker;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server constructor.
|
||||||
|
*
|
||||||
|
* Initializes the server by booting the PSR-7 worker and router.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstraps the server by initializing the PSR-7 worker and router.
|
||||||
|
*
|
||||||
|
* This method sets up the PSR-7 worker and router instances, and registers
|
||||||
|
* the routes for the server. It should be called in the constructor of
|
||||||
|
* subclasses to ensure proper initialization.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function boot(): void
|
||||||
|
{
|
||||||
|
$container = new Container();
|
||||||
|
Facade::setFacadeApplication($container);
|
||||||
|
|
||||||
|
$this->worker = new PSR7Worker(
|
||||||
|
Worker::create(),
|
||||||
|
new Psr17Factory(),
|
||||||
|
new Psr17Factory(),
|
||||||
|
new Psr17Factory()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->router = new Router();
|
||||||
|
|
||||||
|
$this->registerRoutes();
|
||||||
|
// $this->bootModelCapsule(); // no db
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstraps the model capsule for database connections.
|
||||||
|
*
|
||||||
|
* This method sets up the database connection using the Eloquent ORM.
|
||||||
|
* It retrieves the database configuration from the Config facade and
|
||||||
|
* initializes the Eloquent capsule manager.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function bootModelCapsule(): void
|
||||||
|
{
|
||||||
|
$capsule = new Manager();
|
||||||
|
$capsule->addConnection([
|
||||||
|
'driver' => Config::get('db.driver'),
|
||||||
|
'host' => Config::get('db.host'),
|
||||||
|
'database' => Config::get('db.database'),
|
||||||
|
'username' => Config::get('db.username'),
|
||||||
|
'password' => Config::get('db.password'),
|
||||||
|
'charset' => 'utf8',
|
||||||
|
'collation' => 'utf8_unicode_ci',
|
||||||
|
'prefix' => '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$capsule->setAsGlobal();
|
||||||
|
$capsule->bootEloquent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the routes for the server.
|
||||||
|
*
|
||||||
|
* This method is responsible for defining the routes that the server will handle.
|
||||||
|
* It should be implemented in subclasses to provide specific route definitions.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function registerRoutes(): void
|
||||||
|
{
|
||||||
|
$this->router->group('/http/routes', function (RouteGroup $router) {
|
||||||
|
$router->get('/', RoutesController::class . '::get');
|
||||||
|
$router->get('/{id}', RoutesController::class . '::get');
|
||||||
|
$router->post('/{id}', RoutesController::class . '::post');
|
||||||
|
$router->patch('/{id}', RoutesController::class . '::patch');
|
||||||
|
$router->delete('/{id}', RoutesController::class . '::delete');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->router->group('/http/services', function (RouteGroup $router) {
|
||||||
|
$router->get('/', ServicesController::class . '::get');
|
||||||
|
$router->get('/{id}', ServicesController::class . '::get');
|
||||||
|
$router->post('/{id}', ServicesController::class . '::post');
|
||||||
|
$router->delete('/{id}', ServicesController::class . '::delete');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->router->group('/http/middlewares', function (RouteGroup $router) {
|
||||||
|
$router->get('/', MiddlewaresController::class . '::get');
|
||||||
|
$router->get('/{id}', MiddlewaresController::class . '::get');
|
||||||
|
$router->post('/{id}', MiddlewaresController::class . '::post');
|
||||||
|
$router->delete('/{id}', MiddlewaresController::class . '::delete');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->router->middleware(new CorsMiddleware());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the server and handles incoming requests.
|
||||||
|
*
|
||||||
|
* This method enters an infinite loop to continuously handle incoming HTTP requests.
|
||||||
|
* It decodes the request body, routes the request, and sends the response. It also handles
|
||||||
|
* exceptions and ensures proper cleanup after each request.
|
||||||
|
*
|
||||||
|
* @throws \JsonException If there is an error decoding the JSON request body.
|
||||||
|
*/
|
||||||
|
public function startServer(): void
|
||||||
|
{
|
||||||
|
Logger::info(sprintf('Server started: %s', microtime(true)));
|
||||||
|
Logger::info(sprintf('Server PID: %s', getmypid()));
|
||||||
|
Logger::info(sprintf('Server Listening on: 0.0.0.0:%s', Config::get('server.port')));
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
$request = $this->worker->waitRequest();
|
||||||
|
|
||||||
|
if ($request === null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = $request->withParsedBody(json_decode($request->getBody()->getContents(), true));
|
||||||
|
|
||||||
|
$response = $this->router->handle($request);
|
||||||
|
$this->worker->respond($response);
|
||||||
|
} catch (MethodNotAllowedException | NotFoundException) {
|
||||||
|
$this->worker->respond(
|
||||||
|
JsonResponseFactory::createJsonResponse(
|
||||||
|
['status_code' => 404, 'reason_phrase' => 'Not Found'],
|
||||||
|
404
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} 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, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/Traefik/EntityEnum.php
Normal file
21
src/Traefik/EntityEnum.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Traefik;
|
||||||
|
|
||||||
|
enum EntityEnum
|
||||||
|
{
|
||||||
|
case ROUTER;
|
||||||
|
case SERVICE;
|
||||||
|
case MIDDLEWARE;
|
||||||
|
|
||||||
|
public function getValue(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::ROUTER => 'routers',
|
||||||
|
self::SERVICE => 'services',
|
||||||
|
self::MIDDLEWARE => 'middlewares',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/Traefik/ProtocolEnum.php
Normal file
21
src/Traefik/ProtocolEnum.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Traefik;
|
||||||
|
|
||||||
|
enum ProtocolEnum
|
||||||
|
{
|
||||||
|
case HTTP;
|
||||||
|
case TCP;
|
||||||
|
case UDP;
|
||||||
|
|
||||||
|
public function getValue(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::HTTP => 'http',
|
||||||
|
self::TCP => 'tcp',
|
||||||
|
self::UDP => 'udp',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
217
src/Traefik/RedisClient.php
Normal file
217
src/Traefik/RedisClient.php
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Traefik;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Predis\Collection\Iterator\Keyspace;
|
||||||
|
use Siteworxpro\App\Facades\Redis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class RedisClient
|
||||||
|
*/
|
||||||
|
class RedisClient
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @param array $data
|
||||||
|
* @param EntityEnum $entity
|
||||||
|
* @param ProtocolEnum $type
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function createOrReplace(string $name, array $data, EntityEnum $entity, ProtocolEnum $type = ProtocolEnum::HTTP): void
|
||||||
|
{
|
||||||
|
self::deleteAllKeys($name, $entity, $type);
|
||||||
|
|
||||||
|
$collection = self::flattenToDotArray($data);
|
||||||
|
|
||||||
|
foreach ($collection as $key => $value) {
|
||||||
|
$redisKey = 'traefik/' . $type->getValue() . '/' . $entity->getValue() . "/$name/" . str_replace('.', '/', $key);
|
||||||
|
Redis::set($redisKey, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @param array $data
|
||||||
|
* @param EntityEnum $entity
|
||||||
|
* @param ProtocolEnum $type
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function patchEntity(string $name, array $data, EntityEnum $entity, ProtocolEnum $type = ProtocolEnum::HTTP): void
|
||||||
|
{
|
||||||
|
$collection = self::flattenToDotArray($data);
|
||||||
|
|
||||||
|
$checkKey = 'traefik/' . $type->getValue() . '/' . $entity->getValue() . "/$name";
|
||||||
|
|
||||||
|
$keys = Redis::keys($checkKey . '/*');
|
||||||
|
if (empty($keys)) {
|
||||||
|
throw new \InvalidArgumentException("The key $checkKey does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$cleanedUpKeys = [];
|
||||||
|
|
||||||
|
foreach ($collection as $key => $value) {
|
||||||
|
// regex if key matches [key].[digit]
|
||||||
|
if (preg_match('/\.[0-9]+$/', $key)) {
|
||||||
|
$arrayKey = preg_replace('/\.[0-9]+$/', '', $key);
|
||||||
|
|
||||||
|
if (in_array($arrayKey, $cleanedUpKeys)) {
|
||||||
|
$redisKey = 'traefik/' . $type->getValue() . '/' . $entity->getValue() . "/$name/" . str_replace('.', '/', $key);
|
||||||
|
Redis::set($redisKey, $value);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// key is an array, delete keys under array
|
||||||
|
$arrayKeys = Redis::keys($checkKey . '/' . str_replace('.', '/', $arrayKey) . '/*');
|
||||||
|
foreach ($arrayKeys as $k) {
|
||||||
|
Redis::del($k);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cleanedUpKeys[] = $arrayKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
$redisKey = 'traefik/' . $type->getValue() . '/' . $entity->getValue() . "/$name/" . str_replace('.', '/', $key);
|
||||||
|
Redis::set($redisKey, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @param EntityEnum $entity
|
||||||
|
* @param ProtocolEnum $protocol
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function deleteAllKeys(string $name, EntityEnum $entity, ProtocolEnum $protocol = ProtocolEnum::HTTP): bool
|
||||||
|
{
|
||||||
|
$pattern = 'traefik/' . $protocol->getValue() . '/' . $entity->getValue() . "/$name/*";
|
||||||
|
|
||||||
|
foreach (new Keyspace(Redis::getFacadeRoot(), $pattern) as $key) {
|
||||||
|
Redis::del($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @param ProtocolEnum $type
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getMiddleware(string $name, ProtocolEnum $type = ProtocolEnum::HTTP): array
|
||||||
|
{
|
||||||
|
$pattern = 'traefik/' . $type->getValue() . '/' . EntityEnum::MIDDLEWARE->getValue() . "/$name/*";
|
||||||
|
|
||||||
|
return self::fetchValuesToArray($pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ProtocolEnum $type
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getAllMiddlewares(ProtocolEnum $type = ProtocolEnum::HTTP): array
|
||||||
|
{
|
||||||
|
$pattern = 'traefik/' . $type->getValue() . '/' . EntityEnum::MIDDLEWARE->getValue() . '/*';
|
||||||
|
|
||||||
|
return self::getUniqueKeys($pattern, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ProtocolEnum $type
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getAllServices(ProtocolEnum $type = ProtocolEnum::HTTP): array
|
||||||
|
{
|
||||||
|
$pattern = 'traefik/' . $type->getValue() . '/' . EntityEnum::SERVICE->getValue() . '/*';
|
||||||
|
|
||||||
|
return self::getUniqueKeys($pattern, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $serviceName
|
||||||
|
* @param ProtocolEnum $type
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getService(string $serviceName, ProtocolEnum $type = ProtocolEnum::HTTP): array
|
||||||
|
{
|
||||||
|
$pattern = 'traefik/' . $type->getValue() . '/' . EntityEnum::SERVICE->getValue() . "/$serviceName/*";
|
||||||
|
|
||||||
|
return self::fetchValuesToArray($pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ProtocolEnum $type
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getAllRouters(ProtocolEnum $type = ProtocolEnum::HTTP): array
|
||||||
|
{
|
||||||
|
$pattern = 'traefik/' . $type->getValue() . '/' . EntityEnum::ROUTER->getValue() . '/*';
|
||||||
|
|
||||||
|
return self::getUniqueKeys($pattern, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @param ProtocolEnum $type
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getRouter(string $name, ProtocolEnum $type = ProtocolEnum::HTTP): array
|
||||||
|
{
|
||||||
|
$pattern = 'traefik/' . $type->getValue() . '/' . EntityEnum::ROUTER->getValue() . "/$name/*";
|
||||||
|
|
||||||
|
return self::fetchValuesToArray($pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $pattern
|
||||||
|
* @param int $position
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function getUniqueKeys(string $pattern, int $position): array
|
||||||
|
{
|
||||||
|
$values = new Collection();
|
||||||
|
$redis = Redis::getFacadeRoot();
|
||||||
|
|
||||||
|
foreach (new Keyspace($redis, $pattern) as $key) {
|
||||||
|
$parts = explode('/', $key);
|
||||||
|
if (isset($parts[$position])) {
|
||||||
|
$values->push($parts[$position]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $values->unique()->values()->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $pattern
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function fetchValuesToArray(string $pattern): array
|
||||||
|
{
|
||||||
|
$redis = Redis::getFacadeRoot();
|
||||||
|
|
||||||
|
$values = new Collection();
|
||||||
|
|
||||||
|
foreach (new Keyspace($redis, $pattern) as $key) {
|
||||||
|
$parts = explode('/', $key);
|
||||||
|
$arrayPath = array_slice($parts, 4);
|
||||||
|
$keyPath = implode('.', $arrayPath);
|
||||||
|
$values->put($keyPath, $redis->get($key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $values->undot()->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function flattenToDotArray(array $data): array
|
||||||
|
{
|
||||||
|
$collection = new Collection($data);
|
||||||
|
|
||||||
|
return $collection->dot()->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
46
tests/Helpers/EnvTest.php
Normal file
46
tests/Helpers/EnvTest.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\Helpers;
|
||||||
|
|
||||||
|
use Siteworxpro\App\Helpers\Env;
|
||||||
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
|
class EnvTest extends Unit
|
||||||
|
{
|
||||||
|
public function testGetReturnsStringByDefault(): void
|
||||||
|
{
|
||||||
|
putenv('TEST_KEY=example');
|
||||||
|
$result = Env::get('TEST_KEY');
|
||||||
|
$this->assertSame('example', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetReturnsDefaultIfKeyNotSet(): void
|
||||||
|
{
|
||||||
|
putenv('TEST_KEY'); // Unset the environment variable
|
||||||
|
$result = Env::get('TEST_KEY', 'default_value');
|
||||||
|
$this->assertSame('default_value', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCastsToBoolean(): void
|
||||||
|
{
|
||||||
|
putenv('TEST_KEY=true');
|
||||||
|
$result = Env::get('TEST_KEY', null, 'bool');
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCastsToInteger(): void
|
||||||
|
{
|
||||||
|
putenv('TEST_KEY=123');
|
||||||
|
$result = Env::get('TEST_KEY', null, 'int');
|
||||||
|
$this->assertSame(123, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCastsToFloat(): void
|
||||||
|
{
|
||||||
|
putenv('TEST_KEY=123.45');
|
||||||
|
$result = Env::get('TEST_KEY', null, 'float');
|
||||||
|
$this->assertSame(123.45, $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
tests/Http/JsonResponseFactoryTest.php
Normal file
43
tests/Http/JsonResponseFactoryTest.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\Http;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||||
|
|
||||||
|
class JsonResponseFactoryTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testCreateJsonResponseReturnsValidResponse(): void
|
||||||
|
{
|
||||||
|
$data = ['key' => 'value'];
|
||||||
|
$statusCode = 200;
|
||||||
|
|
||||||
|
$response = JsonResponseFactory::createJsonResponse($data, $statusCode);
|
||||||
|
|
||||||
|
$this->assertSame($statusCode, $response->getStatusCode());
|
||||||
|
$this->assertSame('application/json', $response->getHeaderLine('Content-Type'));
|
||||||
|
$this->assertSame(json_encode($data), (string) $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateJsonResponseHandlesEmptyData(): void
|
||||||
|
{
|
||||||
|
$data = [];
|
||||||
|
$statusCode = 204;
|
||||||
|
|
||||||
|
$response = JsonResponseFactory::createJsonResponse($data, $statusCode);
|
||||||
|
|
||||||
|
$this->assertSame($statusCode, $response->getStatusCode());
|
||||||
|
$this->assertSame('application/json', $response->getHeaderLine('Content-Type'));
|
||||||
|
$this->assertSame(json_encode($data), (string) $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateJsonResponseThrowsExceptionOnInvalidData(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\JsonException::class);
|
||||||
|
|
||||||
|
$data = ["invalid" => "\xB1\x31"];
|
||||||
|
JsonResponseFactory::createJsonResponse($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
101
tests/Http/Middleware/CorsMiddlewareTest.php
Normal file
101
tests/Http/Middleware/CorsMiddlewareTest.php
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\Http\Middleware;
|
||||||
|
|
||||||
|
use Nyholm\Psr7\Response;
|
||||||
|
use Nyholm\Psr7\ServerRequest;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use Siteworxpro\App\Facades\Config;
|
||||||
|
use Siteworxpro\App\Http\Middleware\CorsMiddleware;
|
||||||
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
|
class CorsMiddlewareTest extends Unit
|
||||||
|
{
|
||||||
|
public function testAllowsConfiguredOrigin(): void
|
||||||
|
{
|
||||||
|
Config::shouldReceive('get')
|
||||||
|
->with('cors.allowed_origins')
|
||||||
|
->andReturn('https://example.com,https://another.com');
|
||||||
|
|
||||||
|
Config::shouldReceive('get')->with('cors.allow_credentials')->andReturn(false);
|
||||||
|
Config::shouldReceive('get')->with('cors.max_age')->andReturn('');
|
||||||
|
|
||||||
|
$middleware = new CorsMiddleware();
|
||||||
|
$request = new ServerRequest('GET', '/')->withHeader('Origin', 'https://example.com');
|
||||||
|
$handler = $this->mockHandler(new Response(200));
|
||||||
|
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
|
||||||
|
$this->assertEquals('https://example.com', $response->getHeaderLine('Access-Control-Allow-Origin'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBlocksUnconfiguredOrigin(): void
|
||||||
|
{
|
||||||
|
Config::shouldReceive('get')
|
||||||
|
->with('cors.allowed_origins')
|
||||||
|
->andReturn('https://example.com,https://another.com');
|
||||||
|
|
||||||
|
$middleware = new CorsMiddleware();
|
||||||
|
$request = new ServerRequest('GET', '/')->withHeader('Origin', 'https://unauthorized.com');
|
||||||
|
$handler = $this->mockHandler(new Response(200));
|
||||||
|
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
|
||||||
|
$this->assertEmpty($response->getHeaderLine('Access-Control-Allow-Origin'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandlesOptionsRequest(): void
|
||||||
|
{
|
||||||
|
Config::shouldReceive('get')->with('cors.allowed_origins')->andReturn('https://example.com');
|
||||||
|
Config::shouldReceive('get')->with('cors.allow_credentials')->andReturn(false);
|
||||||
|
Config::shouldReceive('get')->with('cors.max_age')->andReturn('86400');
|
||||||
|
|
||||||
|
$middleware = new CorsMiddleware();
|
||||||
|
$request = new ServerRequest('OPTIONS', '/')->withHeader('Origin', 'https://example.com');
|
||||||
|
$handler = $this->mockHandler(new Response(200));
|
||||||
|
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
|
||||||
|
$this->assertEquals(204, $response->getStatusCode());
|
||||||
|
$this->assertEquals('86400', $response->getHeaderLine('Access-Control-Max-Age'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddsAllowCredentialsHeader(): void
|
||||||
|
{
|
||||||
|
Config::shouldReceive('get')
|
||||||
|
->with('cors.allowed_origins')
|
||||||
|
->andReturn('https://example.com');
|
||||||
|
|
||||||
|
Config::shouldReceive('get')->with('cors.allowed_origins')->andReturn('https://example.com');
|
||||||
|
Config::shouldReceive('get')->with('cors.allow_credentials')->andReturn(true);
|
||||||
|
Config::shouldReceive('get')->with('cors.max_age')->andReturn('86400');
|
||||||
|
|
||||||
|
$middleware = new CorsMiddleware();
|
||||||
|
$request = new ServerRequest('GET', '/')->withHeader('Origin', 'https://example.com');
|
||||||
|
$handler = $this->mockHandler(new Response(200));
|
||||||
|
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
|
||||||
|
$this->assertEquals('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mockHandler(Response $response): RequestHandlerInterface
|
||||||
|
{
|
||||||
|
return new class ($response) implements RequestHandlerInterface {
|
||||||
|
private Response $response;
|
||||||
|
|
||||||
|
public function __construct(Response $response)
|
||||||
|
{
|
||||||
|
$this->response = $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(
|
||||||
|
\Psr\Http\Message\ServerRequestInterface $request
|
||||||
|
): \Psr\Http\Message\ResponseInterface {
|
||||||
|
return $this->response;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
25
tests/Unit.php
Normal file
25
tests/Unit.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests;
|
||||||
|
|
||||||
|
use Illuminate\Container\Container;
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Siteworxpro\App\Facades\Config;
|
||||||
|
|
||||||
|
abstract class Unit extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$container = new Container();
|
||||||
|
Facade::setFacadeApplication($container);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
Config::clearResolvedInstances();
|
||||||
|
Facade::setFacadeApplication(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
tests/reports/.gitignore
vendored
Normal file
1
tests/reports/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
html/
|
||||||
Reference in New Issue
Block a user