You've already forked Php-Template
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
f59dcb2dcc
|
|||
|
8252ae4e53
|
|||
|
68ab2dcdd7
|
|||
|
1ac5075b37
|
|||
|
ba2beca107
|
|||
|
b5779afde9
|
|||
|
c91f35c0b1
|
|||
|
88098837a3
|
|||
|
cd49507140
|
|||
|
7792cac8b8
|
|||
|
eaff49b6a4
|
|||
|
721008bdfc
|
|||
|
a9a5cb6216
|
|||
|
0504956d9a
|
|||
|
e9d4cee336
|
|||
|
7d9eb96bea
|
|||
|
9b736eb879
|
|||
|
7aa14c0db3
|
|||
|
474134c654
|
@@ -5,6 +5,12 @@ volumes:
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.traefik.entrypoints=web-secure"
|
||||||
|
- "traefik.http.routers.traefik.rule=Host(`127.0.0.1`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api`))"
|
||||||
|
- "traefik.http.routers.traefik.tls=true"
|
||||||
|
- "traefik.http.routers.traefik.service=api@internal"
|
||||||
image: traefik:latest
|
image: traefik:latest
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -15,15 +21,20 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
|
- "9001:9001"
|
||||||
volumes:
|
volumes:
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||||
|
- "./ssl:/etc/ssl"
|
||||||
restart: always
|
restart: always
|
||||||
command:
|
command:
|
||||||
- "--providers.docker=true"
|
- "--providers.docker=true"
|
||||||
|
- "--api.insecure=true"
|
||||||
- "--ping"
|
- "--ping"
|
||||||
|
- "--providers.file.filename=/etc/ssl/traefik.yml"
|
||||||
- "--providers.docker.exposedByDefault=false"
|
- "--providers.docker.exposedByDefault=false"
|
||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
- "--entrypoints.web-secure.address=:443"
|
- "--entrypoints.web-secure.address=:443"
|
||||||
|
- "--entrypoints.grpc.address=:9001"
|
||||||
- "--accesslog=true"
|
- "--accesslog=true"
|
||||||
- "--entrypoints.web.http.redirections.entryPoint.to=web-secure"
|
- "--entrypoints.web.http.redirections.entryPoint.to=web-secure"
|
||||||
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
|
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
|
||||||
@@ -31,16 +42,30 @@ services:
|
|||||||
|
|
||||||
composer-runtime:
|
composer-runtime:
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- ..:/app
|
||||||
image: siteworxpro/composer
|
image: siteworxpro/composer
|
||||||
entrypoint: "/bin/sh -c 'while true; do sleep 30; done;'"
|
entrypoint: "/bin/sh -c 'while true; do sleep 30; done;'"
|
||||||
environment:
|
environment:
|
||||||
PHP_IDE_CONFIG: serverName=localhost
|
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:
|
migration-container:
|
||||||
volumes:
|
volumes:
|
||||||
- ./db/migrations:/app/db/migrations
|
- ../db/migrations:/app/db/migrations
|
||||||
- ./bin:/app/bin
|
- ../bin:/app/bin
|
||||||
image: siteworxpro/migrate:v4.18.3
|
image: siteworxpro/migrate:v4.18.3
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
# entrypoint: "/bin/sh -c 'while true; do sleep 30; done;'"
|
# entrypoint: "/bin/sh -c 'while true; do sleep 30; done;'"
|
||||||
@@ -65,12 +90,19 @@ services:
|
|||||||
- "traefik.http.services.api.loadbalancer.healthcheck.path=/healthz"
|
- "traefik.http.services.api.loadbalancer.healthcheck.path=/healthz"
|
||||||
- "traefik.http.services.api.loadbalancer.healthcheck.interval=5s"
|
- "traefik.http.services.api.loadbalancer.healthcheck.interval=5s"
|
||||||
- "traefik.http.services.api.loadbalancer.healthcheck.timeout=60s"
|
- "traefik.http.services.api.loadbalancer.healthcheck.timeout=60s"
|
||||||
|
- "traefik.tcp.services.api.loadbalancer.server.port=9001"
|
||||||
|
- "traefik.http.services.api.loadbalancer.server.port=9501"
|
||||||
|
- "traefik.tcp.routers.grpc.entrypoints=grpc"
|
||||||
|
- "traefik.tcp.routers.grpc.rule=HostSNI(`localhost`) || HostSNI(`127.0.0.1`)"
|
||||||
|
- "traefik.tcp.routers.grpc.tls=true"
|
||||||
|
- "traefik.tcp.routers.grpc.service=api"
|
||||||
|
container_name: dev-runtime
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- ..:/app
|
||||||
build:
|
build:
|
||||||
args:
|
args:
|
||||||
KAFKA_ENABLED: "1"
|
KAFKA_ENABLED: "1"
|
||||||
context: .
|
context: ..
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
entrypoint: "/bin/sh -c 'while true; do sleep 30; done;'"
|
entrypoint: "/bin/sh -c 'while true; do sleep 30; done;'"
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -83,13 +115,17 @@ services:
|
|||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
|
JWT_ISSUER: https://auth.siteworxpro.com/application/o/postman/
|
||||||
|
JWT_AUDIENCE: 1RWyqJFlyA4hmsDzq6kSxs0LXvk7UgEAfgmBCpQ9
|
||||||
|
JWT_SIGNING_KEY: https://auth.siteworxpro.com/application/o/postman/.well-known/openid-configuration
|
||||||
QUEUE_BROKER: redis
|
QUEUE_BROKER: redis
|
||||||
PHP_IDE_CONFIG: serverName=localhost
|
PHP_IDE_CONFIG: serverName=localhost
|
||||||
WORKERS: 1
|
WORKERS: 1
|
||||||
|
GRPC_WORKERS: 1
|
||||||
DEBUG: 1
|
DEBUG: 1
|
||||||
REDIS_HOST: redis
|
REDIS_HOST: redis
|
||||||
DB_HOST: postgres
|
DB_HOST: postgres
|
||||||
JWT_SIGNING_KEY: a-string-secret-at-least-256-bits-long
|
DEV_MODE: 1
|
||||||
|
|
||||||
## Kafka and Zookeeper for local development
|
## Kafka and Zookeeper for local development
|
||||||
kafka-ui:
|
kafka-ui:
|
||||||
83
.dev/ssl/localhost.crt
Normal file
83
.dev/ssl/localhost.crt
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEFzCCA52gAwIBAgIURfvF11Q9R3Ue38Tr0BzIoUe0TKQwCgYIKoZIzj0EAwMw
|
||||||
|
MDEuMCwGA1UEAxMlU2l0ZXdvcnggSW50ZXJtZWRpYXRlIEVDMzg0IEF1dGhvcml0
|
||||||
|
eTAeFw0yNTEyMDQxNjM1NTFaFw0yNjEyMDQxNjM2MjFaMHExCzAJBgNVBAYTAlVT
|
||||||
|
MREwDwYDVQQIEwhWaXJnaW5pYTEVMBMGA1UEBxMMUHVyY2VsbHZpbGxlMSQwIgYD
|
||||||
|
VQQKExtTaXRld29yeCBQcm9mZXNzaW9uYWxzLCBMTEMxEjAQBgNVBAMTCWxvY2Fs
|
||||||
|
aG9zdDB2MBAGByqGSM49AgEGBSuBBAAiA2IABM+jXangYCOi01IMblAXJ6iFZE4v
|
||||||
|
SBBOZKNQCwGz8kKi5jyXtVwz6U26DMlBSK+InhhOFQlCRcP9ow8LtlQdaY2XnGKr
|
||||||
|
3X3zxdUZJVhLi/wog+I4igU3+xuyn1E/BgEZx6OCAjUwggIxMA4GA1UdDwEB/wQE
|
||||||
|
AwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQW
|
||||||
|
BBRAtanjiWMYAdpCCz0rEkyqf691bzAfBgNVHSMEGDAWgBQYBC15lPoGGGxbmwqY
|
||||||
|
MWL7jjI6azBJBggrBgEFBQcBAQQ9MDswOQYIKwYBBQUHMAGGLWh0dHBzOi8vdmF1
|
||||||
|
bHQuc2l0ZXdvcnhwcm8uY29tL3YxL3N3eF9pbnQvb2NzcDAaBgNVHREEEzARggls
|
||||||
|
b2NhbGhvc3SHBH8AAAEwHAYDVR0gBBUwEzAIBgZngQwBAgIwBwYFZ4EMAQEwggE1
|
||||||
|
BgNVHR8EggEsMIIBKDBioGCgXoZcaHR0cHM6Ly92YXVsdC5zaXRld29yeHByby5j
|
||||||
|
b20vdjEvc3d4X2ludC9pc3N1ZXIvMjVmMWRiNTAtZDQxOS1kZWQ3LTZiZjktZWNh
|
||||||
|
Y2E4NGEwMmY0L2NybC9wZW0wXqBcoFqGWGh0dHBzOi8vdmF1bHQuc2l0ZXdvcnhw
|
||||||
|
cm8uY29tL3YxL3N3eF9pbnQvaXNzdWVyLzI1ZjFkYjUwLWQ0MTktZGVkNy02YmY5
|
||||||
|
LWVjYWNhODRhMDJmNC9jcmwwYqBgoF6GXGh0dHBzOi8vdmF1bHQuc2l0ZXdvcnhw
|
||||||
|
cm8uY29tL3YxL3N3eF9pbnQvaXNzdWVyLzI1ZjFkYjUwLWQ0MTktZGVkNy02YmY5
|
||||||
|
LWVjYWNhODRhMDJmNC9jcmwvZGVyMAoGCCqGSM49BAMDA2gAMGUCMGxgZmKITQFu
|
||||||
|
H6j3j/t9MOTxhVsfOuoD0q3pMlp9d1u4Lg0THKUOzN06BVuXwC1eagIxAL2I/2a1
|
||||||
|
MMJmhky2EavzOsYt37Ae+1KGyELiwcWe5f/lActlw97pqRajpmqEmdo7PA==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEETCCAfmgAwIBAgIUIRpRFzFBITweYJETytgbPBgwbWgwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwgZ0xCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhWaXJnaW5pYTEVMBMGA1UEBwwM
|
||||||
|
UHVyY2VsbHZpbGxlMSQwIgYDVQQKDBtTaXRld29yeCBQcm9mZXNzaW9uYWxzLCBM
|
||||||
|
TEMxFDASBgNVBAMMC1NXWCBSb290IENBMSgwJgYJKoZIhvcNAQkBFhl3ZWJtYXN0
|
||||||
|
ZXJAc2l0ZXdvcnhwcm8uY29tMB4XDTIzMDMyMTE2MzAxNVoXDTMzMDMxODE2MzA0
|
||||||
|
NVowMDEuMCwGA1UEAxMlU2l0ZXdvcnggSW50ZXJtZWRpYXRlIEVDMzg0IEF1dGhv
|
||||||
|
cml0eTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIhlP1W1O1WjoDFGFi5XbE0zVy90
|
||||||
|
76pQQ8VmSYtaZI9Jz5pAZTOQ073t/QkTWge8uhDaJ2J2uBhjQJGr5BPttvBcLJFI
|
||||||
|
52X7hJuck4oL0aukXiHYA5gZbC5LhKVvCyZcWqNjMGEwDgYDVR0PAQH/BAQDAgEG
|
||||||
|
MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBgELXmU+gYYbFubCpgxYvuOMjpr
|
||||||
|
MB8GA1UdIwQYMBaAFHWCysIFrdsWYZJSjBO1pSQPETkTMA0GCSqGSIb3DQEBCwUA
|
||||||
|
A4ICAQBbw5roegt0tUc+gu0IcHDt56cUoqChmIZXzla8gTgg820ww/+Wm+vNAl8W
|
||||||
|
r3Y67LzK19CygoujD2o7M25syaByRiw9JdIfNGvBzklOOM+sus9DDmwSUBMCuljS
|
||||||
|
KLBhWzIrXDZwemzklGEbj+RL4o2ZiL01nx8xygDF55eaudNS0VzRzd2Hv0C+rm2i
|
||||||
|
nnwRNoKsL14YXc41rFBWwb5ViRuD2Wp0c9CivEOd4UNKgOnGyNxcNhjzNlY05t3c
|
||||||
|
NEeskEXiz21sj0vnrwM7olKyXPXDFUCCKGb21Sn9sWKldicumU1i1HdDGA1w50uh
|
||||||
|
NS4G4wqGQ8iZCq3h6JkpBMGPJPG3Dq6yuzrh8fmh56IqtKY4MxdKHb91MtFHnkw5
|
||||||
|
jCrxqpTKShRyqcBSx8QmXRXpec5FEB88NQ3aKhtFlNqXYphNRAI9bLIyGkdxUF/r
|
||||||
|
PCkZkKBhbsRvXT8Ii/K1PQHzliQqJxXhrrJEsIg2jiSQItBg52ZySzuw+Y6++h11
|
||||||
|
73XMKJ53oOeLcxvp2qJRwMkNTwVfNxDmKC0tIRdI+KoJYbYeN0Ev/pEdPdYl+hjY
|
||||||
|
uQhKMt1KtpUyYwPzTGPKGMnklKj/T3Qu7fmpsWxtAOuK7yLLMayBwXBlVBD23md+
|
||||||
|
UAfPR3FfVX+aRqqsvT7WI+SnlycJuYXs41ZPxBjLq2aB7fhAwQ==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIGIjCCBAqgAwIBAgIJAKU+Idu5bncNMA0GCSqGSIb3DQEBCwUAMIGdMQswCQYD
|
||||||
|
VQQGEwJVUzERMA8GA1UECAwIVmlyZ2luaWExFTATBgNVBAcMDFB1cmNlbGx2aWxs
|
||||||
|
ZTEkMCIGA1UECgwbU2l0ZXdvcnggUHJvZmVzc2lvbmFscywgTExDMRQwEgYDVQQD
|
||||||
|
DAtTV1ggUm9vdCBDQTEoMCYGCSqGSIb3DQEJARYZd2VibWFzdGVyQHNpdGV3b3J4
|
||||||
|
cHJvLmNvbTAeFw0yMDA5MDgxMjU3NTJaFw00MDA5MDMxMjU3NTJaMIGdMQswCQYD
|
||||||
|
VQQGEwJVUzERMA8GA1UECAwIVmlyZ2luaWExFTATBgNVBAcMDFB1cmNlbGx2aWxs
|
||||||
|
ZTEkMCIGA1UECgwbU2l0ZXdvcnggUHJvZmVzc2lvbmFscywgTExDMRQwEgYDVQQD
|
||||||
|
DAtTV1ggUm9vdCBDQTEoMCYGCSqGSIb3DQEJARYZd2VibWFzdGVyQHNpdGV3b3J4
|
||||||
|
cHJvLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAN9JNyWot7VX
|
||||||
|
ODvru8S5/o6gdFuynA1l5T0uXSzWhROMYHndmY+n7pwQCwf1R8iLL3aat9sDRxqM
|
||||||
|
RRScD3nNW6UzC5xNz12wiuemf2KT82cTjmUBU3CvtjstbgkrQ/SrpR/Arvu2YwUe
|
||||||
|
tmL9ft/xaoGvZXx8LKpyRMrHA1FlS2st+RFWBC0yXTU/nL4/7YQKVEcbc3YZvgCT
|
||||||
|
P4/8pxH9u8W7kgnufQHHKEIZR9lxIUhQ7yvc61B3zMntbJsZV1N+0c7j5DXY5cfT
|
||||||
|
6zXlfG2hSX1dbhM56y8O8KiCFaWaDRZ9mwkfZGM0W58gkhXUPXOrIOwewLmvl2Z4
|
||||||
|
Vu43UkLfKhtQApxk6zodHRq1e2rNWSpBCGznT9XyoeO/spJ7yggNkleTa+SnnlmV
|
||||||
|
rHJS/YUp3/jAvJY2bCHQKFu/mguMY3Ub2X6eEBsVZOmUqDMbya1TPP6GCVqh4gUu
|
||||||
|
yip6qS9UksaTF6IN3IcrGhwtTyvp8BFqwVA0tMhgraf1rv6ZoXjY/NDuGjE1xXJg
|
||||||
|
Hg+gg2pIIRcXjcsG1tXFXTgxDqoh127ADg/gtq9cIyarMx4LdNTjnR+CnhjqvRkT
|
||||||
|
uiUBB1bwDc9pbX0ulfnR+VuIZtQ6PSuWwChnMdNBKmCgQT1J1AHWpQqnFTjg42NV
|
||||||
|
5QAdFOQxAnsq2DxkurVFEz2J3euZx1ZdAgMBAAGjYzBhMB0GA1UdDgQWBBR1gsrC
|
||||||
|
Ba3bFmGSUowTtaUkDxE5EzAfBgNVHSMEGDAWgBR1gsrCBa3bFmGSUowTtaUkDxE5
|
||||||
|
EzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF
|
||||||
|
AAOCAgEAZDjIlaAoMfRGdb3/i40s6nN18iWN2Chttd8dLXgV+/SZ8GrAU89JNJrK
|
||||||
|
ODaLZT1wHeWVz0LP3miByuvfrnH4qzPEOI2L6zEy/FJr8SCivjm7aUExyb5kTSXp
|
||||||
|
LkwVcOI9UfQb6lCy9Gs/rUEcWQjs5KS3dy6ZwBMaywq6sRj7MeXmhqXhj7aAyWFA
|
||||||
|
psnQsuP2XweWa9OX6Z+u78sebfoiJlOEUvV9VRNHQYpLUd75p6sti1Dm9blWkZEO
|
||||||
|
hyssi3kOJMH+g5pc9xNbD9gS+/pFUWxEVAhHOc0xdEIcHfV5oiiOUDD5EOIPi3xv
|
||||||
|
/NYTV7o7pv2/QlH09vO2PHdsy07lhsg7NoM3U+zYq609Ox78/b4PNd+TkdtYKebO
|
||||||
|
VumZ0xXab0lWbTVuno52k473ODQRA/v9YWHtuovW0Lzf5fDcBhVXTDeW21SmMJIx
|
||||||
|
B+dgJDh7ql7ruZqjMj+kePjM9Mm+M5pDZ6vrEtgiR2yQj/IE+LoQh/bxFHpFkIK8
|
||||||
|
I6AWoxABAvLZB+KHl1ufR5yOauJG2+SQRuzHNZvkAcdjmwpgfxcsB2mY7o0RbGmZ
|
||||||
|
VWm97P4P9iJhje/W4C0cGwVY5wRAMAg6SI1BpcW7YghB14UrKaxpEzHCdZIeeT94
|
||||||
|
GYzN2XNSSGW3s1anFedd5PQyRM7PlJIcloLYrqyWW6M7OwWnMXA=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
6
.dev/ssl/localhost.key
Normal file
6
.dev/ssl/localhost.key
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MIGkAgEBBDBrpJYaCMqgu490fpZoIphGVspE33v3JwyD9B55HwSX/jykySs9NTOv
|
||||||
|
68YndzE9LNCgBwYFK4EEACKhZANiAATPo12p4GAjotNSDG5QFyeohWROL0gQTmSj
|
||||||
|
UAsBs/JCouY8l7VcM+lNugzJQUiviJ4YThUJQkXD/aMPC7ZUHWmNl5xiq91988XV
|
||||||
|
GSVYS4v8KIPiOIoFN/sbsp9RPwYBGcc=
|
||||||
|
-----END EC PRIVATE KEY-----
|
||||||
14
.dev/ssl/traefik.yml
Normal file
14
.dev/ssl/traefik.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
tls:
|
||||||
|
stores:
|
||||||
|
default:
|
||||||
|
defaultCertificate:
|
||||||
|
certFile: /etc/ssl/localhost.crt
|
||||||
|
keyFile: /etc/ssl/localhost.key
|
||||||
|
|
||||||
|
options:
|
||||||
|
default:
|
||||||
|
minVersion: VersionTLS13
|
||||||
|
preferServerCipherSuites: true
|
||||||
|
|
||||||
|
mintls13:
|
||||||
|
minVersion: VersionTLS13
|
||||||
@@ -26,6 +26,12 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
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
|
- name: 🏗️ 🔧 Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
-e POSTGRES_PASSWORD=postgres \
|
-e POSTGRES_PASSWORD=postgres \
|
||||||
-e POSTGRES_DB=postgres \
|
-e POSTGRES_DB=postgres \
|
||||||
-p 5432 \
|
-p 5432 \
|
||||||
-d postgres:17
|
-d postgres:18
|
||||||
|
|
||||||
echo "Waiting for Postgres to start"
|
echo "Waiting for Postgres to start"
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|||||||
11
.rr.yaml
11
.rr.yaml
@@ -6,6 +6,17 @@ server:
|
|||||||
rpc:
|
rpc:
|
||||||
listen: tcp://127.0.0.1:6001
|
listen: tcp://127.0.0.1:6001
|
||||||
|
|
||||||
|
grpc:
|
||||||
|
listen: "tcp://0.0.0.0:9001"
|
||||||
|
pool:
|
||||||
|
command: "php grpc-worker.php"
|
||||||
|
num_workers: ${GRPC_WORKERS:-4}
|
||||||
|
debug: ${DEBUG:-false}
|
||||||
|
reflection: ${GRPC_REFLECTION:-true}
|
||||||
|
destroy_timeout: 5s
|
||||||
|
proto:
|
||||||
|
- "protos/example.proto"
|
||||||
|
|
||||||
http:
|
http:
|
||||||
pool:
|
pool:
|
||||||
allocate_timeout: 5s
|
allocate_timeout: 5s
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name=" Compose Deployment" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
|
<configuration default="false" name=".dev: Compose Deployment" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
|
||||||
<deployment type="docker-compose.yml">
|
<deployment type="docker-compose.yml">
|
||||||
<settings>
|
<settings>
|
||||||
<option name="sourceFilePath" value="docker-compose.yml" />
|
<option name="sourceFilePath" value=".dev/docker-compose.yml" />
|
||||||
</settings>
|
</settings>
|
||||||
</deployment>
|
</deployment>
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<option name="interpreterName" value="composer-runtime" />
|
<option name="interpreterName" value="composer-runtime" />
|
||||||
</PhpTestInterpreterSettings>
|
</PhpTestInterpreterSettings>
|
||||||
</CommandLine>
|
</CommandLine>
|
||||||
<TestRunner configuration_file="$PROJECT_DIR$/phpunit.xml" scope="XML" use_alternative_configuration_file="true" />
|
<TestRunner configuration_file="$PROJECT_DIR$/phpunit.xml" coverage_engine="PCov" scope="XML" use_alternative_configuration_file="true" />
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
</component>
|
</component>
|
||||||
20
Dockerfile
20
Dockerfile
@@ -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
|
# 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
|
ARG KAFKA_ENABLED=0
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ RUN if [ "$KAFKA_ENABLED" -eq 1 ] ; then \
|
|||||||
echo "Kafka support enabled" ; \
|
echo "Kafka support enabled" ; \
|
||||||
apk add autoconf g++ librdkafka-dev make --no-cache ; \
|
apk add autoconf g++ librdkafka-dev make --no-cache ; \
|
||||||
pecl install rdkafka && docker-php-ext-enable rdkafka ; \
|
pecl install rdkafka && docker-php-ext-enable rdkafka ; \
|
||||||
|
apk del autoconf g++ make ; \
|
||||||
else \
|
else \
|
||||||
echo "Kafka support disabled" ; \
|
echo "Kafka support disabled" ; \
|
||||||
exit 0 ; \
|
exit 0 ; \
|
||||||
@@ -42,11 +43,18 @@ COPY --from=library /app/vendor /app/vendor
|
|||||||
|
|
||||||
# Copy the RoadRunner configuration file and source
|
# Copy the RoadRunner configuration file and source
|
||||||
ADD src src/
|
ADD src src/
|
||||||
ADD server.php .
|
ADD generated generated/
|
||||||
ADD .rr.yaml .
|
ADD protos protos/
|
||||||
ADD config.php .
|
ADD server.php cli.php grpc-worker.php .rr.yaml config.php ./
|
||||||
|
|
||||||
EXPOSE 9501
|
EXPOSE 9501 9001
|
||||||
|
|
||||||
|
RUN addgroup -g 1000 appuser && adduser -D -u 1000 -G appuser appuser && chown -R appuser:appuser /app
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
|
||||||
|
CMD curl -f http://localhost:9501/healthz || exit 1
|
||||||
|
|
||||||
# Entrypoint command to run the RoadRunner server with the specified configuration
|
# Entrypoint command to run the RoadRunner server with the specified configuration
|
||||||
ENTRYPOINT ["rr", "serve", "-c", ".rr.yaml", "-s"]
|
ENTRYPOINT ["rr", "serve"]
|
||||||
|
CMD ["-c", ".rr.yaml", "-s"]
|
||||||
139
README.md
139
README.md
@@ -2,55 +2,132 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This is a PHP project template that provides a structured development environment using Docker Compose and Make.
|
||||||
|
It includes tools for code quality, testing, dependency management, and gRPC support.
|
||||||
|
|
||||||
## Dev Environment
|
## Dev Environment
|
||||||
|
|
||||||
### Prerequisites
|
This project uses Docker Compose and Make to manage the development environment. The `makefile` provides convenient
|
||||||
- Docker
|
commands for common development tasks.
|
||||||
- Docker Compose
|
|
||||||
|
|
||||||
### migrations
|
## Prerequisites
|
||||||
|
|
||||||
create a new migration
|
- Docker and Docker Compose
|
||||||
```shell
|
- Make
|
||||||
docker run --rm -v $(PWD):/app siteworxpro/migrate:v4.18.3 create -ext sql -dir /app/db/migrations -seq create_users_table
|
- protoc (Protocol Buffers compiler) - for gRPC code generation
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install PHP dependencies
|
||||||
|
make composer-install
|
||||||
|
|
||||||
|
# Start the development container
|
||||||
|
make start
|
||||||
|
|
||||||
|
# Run the application server
|
||||||
|
make run
|
||||||
```
|
```
|
||||||
|
|
||||||
```text
|
## Available Commands
|
||||||
postgres://siteworxpro:password@localhost:5432/siteworxpro?sslmode=disable
|
|
||||||
|
### Container Management
|
||||||
|
|
||||||
|
- `make start` - Start the development runtime container
|
||||||
|
- `make stop` - Stop and remove all containers
|
||||||
|
- `make restart` - Restart the development container
|
||||||
|
- `make rebuild` - Rebuild containers (use after Dockerfile changes)
|
||||||
|
- `make sh` - Open a shell in the development container
|
||||||
|
- `make ps` - Show running containers
|
||||||
|
|
||||||
|
### Application
|
||||||
|
|
||||||
|
- `make run` - Run the application server (RoadRunner)
|
||||||
|
- `make migrate` - Run database migrations
|
||||||
|
|
||||||
|
### Composer & Dependencies
|
||||||
|
|
||||||
|
- `make composer-install` - Install PHP dependencies
|
||||||
|
- `make composer-update` - Update PHP dependencies
|
||||||
|
- `make composer-require package=vendor/package` - Add a new dependency
|
||||||
|
- `make composer-require-dev package=vendor/package` - Add a new dev dependency
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
- `make lint` - Run linters (phpcs and phpstan)
|
||||||
|
- `make fmt` - Format code with php-cs-fixer
|
||||||
|
- `make test` - Run the test suite (phpunit)
|
||||||
|
- `make license-check` - Check license headers in source files
|
||||||
|
|
||||||
|
### Debugging & Coverage
|
||||||
|
|
||||||
|
- `make enable-debug` - Enable Xdebug for debugging
|
||||||
|
- `make enable-coverage` - Enable PCOV for code coverage
|
||||||
|
|
||||||
|
### gRPC
|
||||||
|
|
||||||
|
- `make protoc` - Generate PHP gRPC code from `.proto` files
|
||||||
|
|
||||||
|
### CI Workflow
|
||||||
|
|
||||||
|
- `make ci` - Run the full CI pipeline locally (install, license check, lint, test)
|
||||||
|
|
||||||
|
### Help
|
||||||
|
|
||||||
|
- `make help` - Show all available commands with descriptions
|
||||||
|
|
||||||
|
## Common Workflows
|
||||||
|
|
||||||
|
### Starting Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make composer-install
|
||||||
|
make start
|
||||||
|
make run
|
||||||
```
|
```
|
||||||
|
|
||||||
```shell
|
### Adding a New Package
|
||||||
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
|
|
||||||
|
```bash
|
||||||
|
make composer-require package=vendor/package-name
|
||||||
```
|
```
|
||||||
|
|
||||||
### Starting the Runtime
|
### Running Tests
|
||||||
```shell
|
|
||||||
docker-compose up -d
|
```bash
|
||||||
```
|
make test
|
||||||
### Start the server
|
|
||||||
```shell
|
|
||||||
docker exec -it template-dev-runtime-1 rr serve
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can access the api at `http://localhost:9501/`
|
### Code Quality Check
|
||||||
|
|
||||||
### Xdebug
|
```bash
|
||||||
|
make lint
|
||||||
xdebug needs to be built into the container before it will work
|
make fmt
|
||||||
```shell
|
|
||||||
docker exec -it php-template-composer-runtime-1 bin/xdebug.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install the dependencies
|
### Debugging
|
||||||
```shell
|
|
||||||
docker run --rm -v $(PWD):/app siteworxpro/composer install --ignore-platform-reqs
|
```bash
|
||||||
|
make enable-debug
|
||||||
|
make run
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running all tests
|
## Notes
|
||||||
```shell
|
|
||||||
docker run --rm -v $(PWD):/app siteworxpro/composer run tests:all
|
|
||||||
```
|
|
||||||
|
|
||||||
|
- All commands run inside Docker containers, ensuring a consistent environment
|
||||||
|
- The development runtime uses RoadRunner as the application server
|
||||||
|
- Composer commands run in a separate `composer-runtime` container
|
||||||
|
- Database migrations run in a dedicated `migration-container`
|
||||||
|
|
||||||
|
## Additional Information
|
||||||
|
|
||||||
|
### Accessing Services
|
||||||
|
|
||||||
|
- You can access the api at [https://localhost](https://localhost)
|
||||||
|
- Traefik dashboard is at [https://127.0.0.1/dashboard/](https://127.0.0.1/dashboard/)
|
||||||
|
- the grpc server is at [tcp://localhost:9001](tcp://localhost:9001)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ echo "Installing xDebug"
|
|||||||
|
|
||||||
apk add make gcc linux-headers autoconf alpine-sdk
|
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
|
curl -sL https://github.com/xdebug/xdebug/archive/3.5.0alpha3.tar.gz -o 3.5.0alpha3.tar.gz
|
||||||
tar -xvf 3.4.0.tar.gz
|
tar -xvf 3.5.0alpha3.tar.gz
|
||||||
cd xdebug-3.4.0 || exit
|
cd xdebug-3.5.0alpha3 || exit
|
||||||
phpize
|
phpize
|
||||||
./configure --enable-xdebug
|
./configure --enable-xdebug
|
||||||
make
|
make
|
||||||
@@ -20,5 +20,5 @@ xdebug.client_host = host.docker.internal
|
|||||||
" > /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
" > /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
rm -rf xdebug-3.4.0
|
rm -rf xdebug-3.5.0alpha3
|
||||||
rm -rf 3.4.0.tar.gz
|
rm -rf 3.5.0alpha3.tar.gz
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Siteworxpro\\App\\": "src/",
|
"Siteworxpro\\App\\": "src/",
|
||||||
"Siteworxpro\\Tests\\": "tests/"
|
"Siteworxpro\\Tests\\": "tests/",
|
||||||
|
"GRPC\\": "generated/GRPC"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.4",
|
"php": "^8.5",
|
||||||
"league/route": "^6.2.0",
|
"league/route": "^6.2.0",
|
||||||
"illuminate/database": "^v12.34.0",
|
"illuminate/database": "^v12.34.0",
|
||||||
"spiral/roadrunner-http": "^v3.6.0",
|
"spiral/roadrunner-http": "^v3.6.0",
|
||||||
@@ -21,12 +22,17 @@
|
|||||||
"lcobucci/jwt": "^5.6",
|
"lcobucci/jwt": "^5.6",
|
||||||
"adhocore/cli": "^1.9",
|
"adhocore/cli": "^1.9",
|
||||||
"robinvdvleuten/ulid": "^5.0",
|
"robinvdvleuten/ulid": "^5.0",
|
||||||
"monolog/monolog": "^3.9"
|
"monolog/monolog": "^3.9",
|
||||||
|
"react/promise": "^3",
|
||||||
|
"react/async": "^4",
|
||||||
|
"guzzlehttp/guzzle": "^7.10",
|
||||||
|
"zircote/swagger-php": "^5.7",
|
||||||
|
"spiral/roadrunner-grpc": "^3.5"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^12.4",
|
"phpunit/phpunit": "^12.4",
|
||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.6",
|
||||||
"squizlabs/php_codesniffer": "^3.12",
|
"squizlabs/php_codesniffer": "^4.0",
|
||||||
"lendable/composer-license-checker": "^1.2",
|
"lendable/composer-license-checker": "^1.2",
|
||||||
"phpstan/phpstan": "^2.1.31",
|
"phpstan/phpstan": "^2.1.31",
|
||||||
"kwn/php-rdkafka-stubs": "^2.2"
|
"kwn/php-rdkafka-stubs": "^2.2"
|
||||||
|
|||||||
1433
composer.lock
generated
1433
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,16 @@ use Siteworxpro\App\Helpers\Env;
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
|
'app' => [
|
||||||
|
'log_level' => Env::get('LOG_LEVEL', 'debug'),
|
||||||
|
'dev_mode' => Env::get('DEV_MODE', false, 'bool'),
|
||||||
|
],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The server configuration.
|
* The server configuration.
|
||||||
*/
|
*/
|
||||||
'server' => [
|
'server' => [
|
||||||
'port' => Env::get('HTTP_PORT', 9501, 'int'),
|
'port' => Env::get('HTTP_PORT', 9501, 'int'),
|
||||||
'dev_mode' => Env::get('DEV_MODE', false, 'bool'),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,7 +51,7 @@ return [
|
|||||||
'signing_key' => Env::get('JWT_SIGNING_KEY', 'a_super_secret_key'),
|
'signing_key' => Env::get('JWT_SIGNING_KEY', 'a_super_secret_key'),
|
||||||
'audience' => Env::get('JWT_AUDIENCE', 'my_audience'),
|
'audience' => Env::get('JWT_AUDIENCE', 'my_audience'),
|
||||||
'issuer' => Env::get('JWT_ISSUER', 'my_issuer'),
|
'issuer' => Env::get('JWT_ISSUER', 'my_issuer'),
|
||||||
'strict_validation' => Env::get('JWT_STRICT_VALIDATION', true, 'bool'),
|
'strict_validation' => Env::get('JWT_STRICT_VALIDATION', false, 'bool'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'queue' => [
|
'queue' => [
|
||||||
|
|||||||
25
generated/GRPC/GPBMetadata/Example.php
Normal file
25
generated/GRPC/GPBMetadata/Example.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# NO CHECKED-IN PROTOBUF GENCODE
|
||||||
|
# source: protos/example.proto
|
||||||
|
|
||||||
|
namespace GRPC\GPBMetadata;
|
||||||
|
|
||||||
|
class Example
|
||||||
|
{
|
||||||
|
public static $is_initialized = false;
|
||||||
|
|
||||||
|
public static function initOnce() {
|
||||||
|
$pool = \Google\Protobuf\Internal\DescriptorPool::getGeneratedPool();
|
||||||
|
|
||||||
|
if (static::$is_initialized == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$pool->internalAddGeneratedFile(
|
||||||
|
"\x0A\xE5\x01\x0A\x14protos/example.proto\x12\x0Ahelloworld\"\x1C\x0A\x0CHelloRequest\x12\x0C\x0A\x04name\x18\x01 \x01(\x09\"\x1D\x0A\x0AHelloReply\x12\x0F\x0A\x07message\x18\x01 \x01(\x092I\x0A\x07Greeter\x12>\x0A\x08SayHello\x12\x18.helloworld.HelloRequest\x1A\x16.helloworld.HelloReply\"\x00B1Z\x0Dproto/greeter\xCA\x02\x0CGRPC\\Greeter\xE2\x02\x10GRPC\\GPBMetadatab\x06proto3"
|
||||||
|
, true);
|
||||||
|
|
||||||
|
static::$is_initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
22
generated/GRPC/Greeter/GreeterInterface.php
Normal file
22
generated/GRPC/Greeter/GreeterInterface.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
# Generated by the protocol buffer compiler (roadrunner-server/grpc). DO NOT EDIT!
|
||||||
|
# source: protos/example.proto
|
||||||
|
|
||||||
|
namespace GRPC\Greeter;
|
||||||
|
|
||||||
|
use Spiral\RoadRunner\GRPC;
|
||||||
|
|
||||||
|
interface GreeterInterface extends GRPC\ServiceInterface
|
||||||
|
{
|
||||||
|
// GRPC specific service name.
|
||||||
|
public const NAME = "helloworld.Greeter";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param GRPC\ContextInterface $ctx
|
||||||
|
* @param HelloRequest $in
|
||||||
|
* @return HelloReply
|
||||||
|
*
|
||||||
|
* @throws GRPC\Exception\InvokeException
|
||||||
|
*/
|
||||||
|
public function SayHello(GRPC\ContextInterface $ctx, HelloRequest $in): HelloReply;
|
||||||
|
}
|
||||||
61
generated/GRPC/Greeter/HelloReply.php
Normal file
61
generated/GRPC/Greeter/HelloReply.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# NO CHECKED-IN PROTOBUF GENCODE
|
||||||
|
# source: protos/example.proto
|
||||||
|
|
||||||
|
namespace GRPC\Greeter;
|
||||||
|
|
||||||
|
use Google\Protobuf\Internal\GPBType;
|
||||||
|
use Google\Protobuf\Internal\GPBUtil;
|
||||||
|
use Google\Protobuf\RepeatedField;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response message containing the greetings
|
||||||
|
*
|
||||||
|
* Generated from protobuf message <code>helloworld.HelloReply</code>
|
||||||
|
*/
|
||||||
|
class HelloReply extends \Google\Protobuf\Internal\Message
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Generated from protobuf field <code>string message = 1;</code>
|
||||||
|
*/
|
||||||
|
protected $message = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param array $data {
|
||||||
|
* Optional. Data for populating the Message object.
|
||||||
|
*
|
||||||
|
* @type string $message
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function __construct($data = NULL) {
|
||||||
|
\GRPC\GPBMetadata\Example::initOnce();
|
||||||
|
parent::__construct($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generated from protobuf field <code>string message = 1;</code>
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getMessage()
|
||||||
|
{
|
||||||
|
return $this->message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generated from protobuf field <code>string message = 1;</code>
|
||||||
|
* @param string $var
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setMessage($var)
|
||||||
|
{
|
||||||
|
GPBUtil::checkString($var, True);
|
||||||
|
$this->message = $var;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
61
generated/GRPC/Greeter/HelloRequest.php
Normal file
61
generated/GRPC/Greeter/HelloRequest.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# NO CHECKED-IN PROTOBUF GENCODE
|
||||||
|
# source: protos/example.proto
|
||||||
|
|
||||||
|
namespace GRPC\Greeter;
|
||||||
|
|
||||||
|
use Google\Protobuf\Internal\GPBType;
|
||||||
|
use Google\Protobuf\Internal\GPBUtil;
|
||||||
|
use Google\Protobuf\RepeatedField;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The request message containing the user's name.
|
||||||
|
*
|
||||||
|
* Generated from protobuf message <code>helloworld.HelloRequest</code>
|
||||||
|
*/
|
||||||
|
class HelloRequest extends \Google\Protobuf\Internal\Message
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Generated from protobuf field <code>string name = 1;</code>
|
||||||
|
*/
|
||||||
|
protected $name = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param array $data {
|
||||||
|
* Optional. Data for populating the Message object.
|
||||||
|
*
|
||||||
|
* @type string $name
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function __construct($data = NULL) {
|
||||||
|
\GRPC\GPBMetadata\Example::initOnce();
|
||||||
|
parent::__construct($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generated from protobuf field <code>string name = 1;</code>
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generated from protobuf field <code>string name = 1;</code>
|
||||||
|
* @param string $var
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setName($var)
|
||||||
|
{
|
||||||
|
GPBUtil::checkString($var, True);
|
||||||
|
$this->name = $var;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
3
generated/README.md
Normal file
3
generated/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
### Note to Developers
|
||||||
|
Only generated files are allowed in this directory.
|
||||||
|
Please do not add any other files here manually.
|
||||||
14
grpc-worker.php
Normal file
14
grpc-worker.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Siteworxpro\App\Grpc;
|
||||||
|
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$server = new Grpc();
|
||||||
|
exit($server->start());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo $e->getMessage();
|
||||||
|
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
162
makefile
Normal file
162
makefile
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# Makefile (enhanced)
|
||||||
|
SHELL := /bin/sh
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
# Docker Compose file
|
||||||
|
COMPOSE_FILE := -f .dev/docker-compose.yml
|
||||||
|
|
||||||
|
# Reusable vars
|
||||||
|
DOCKER := docker compose $(COMPOSE_FILE)
|
||||||
|
COMPOSER_RUNTIME := composer-runtime
|
||||||
|
DEV_RUNTIME := dev-runtime
|
||||||
|
MIGRATION_CONTAINER := migration-container
|
||||||
|
|
||||||
|
PROTOC_GEN_DIR := ./protoc-gen-php-grpc-2025.1.5-darwin-arm64
|
||||||
|
PROTOC_GEN := $(PROTOC_GEN_DIR)/protoc-gen-php-grpc
|
||||||
|
PROTOC_URL := https://github.com/roadrunner-server/roadrunner/releases/download/v2025.1.5/protoc-gen-php-grpc-2025.1.5-darwin-arm64.tar.gz
|
||||||
|
|
||||||
|
DEV := $(DOCKER) exec $(DEV_RUNTIME) sh -c
|
||||||
|
COMPOSER := $(DOCKER) exec $(COMPOSER_RUNTIME) sh -c
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
GREEN := \033[32m
|
||||||
|
YELLOW := \033[33m
|
||||||
|
RESET := \033[0m
|
||||||
|
|
||||||
|
# Fancy emoji
|
||||||
|
SPARK := ✨
|
||||||
|
ROCKET := 🚀
|
||||||
|
WARN := ⚠️
|
||||||
|
MAGNIFY := 🔍
|
||||||
|
BUG := 🐞
|
||||||
|
COMPOSE := 🐳
|
||||||
|
TRASH := 🧹
|
||||||
|
PROTO := 🧩
|
||||||
|
CHECK := ✅
|
||||||
|
CROSS := ❌
|
||||||
|
|
||||||
|
# Align width for help display
|
||||||
|
HELP_COL_WIDTH := 26
|
||||||
|
|
||||||
|
# Help: auto-generate from targets with "##" comments
|
||||||
|
help: ## Show this help
|
||||||
|
@echo "$(SPARK) Available commands:"
|
||||||
|
@awk -F':|##' '/^[a-zA-Z0-9._-]+:.*##/ {printf " %-$(HELP_COL_WIDTH)s - %s\n", $$1, $$3}' $(MAKEFILE_LIST) | sort
|
||||||
|
|
||||||
|
start: ## Start the development runtime container
|
||||||
|
@printf "$(GREEN)$(ROCKET) Starting $(DEV_RUNTIME)$(RESET)\n"
|
||||||
|
$(DOCKER) up $(DEV_RUNTIME) -d --no-recreate
|
||||||
|
|
||||||
|
sh: ## Open a shell in the development runtime container
|
||||||
|
@$(MAKE) start
|
||||||
|
$(DOCKER) exec $(DEV_RUNTIME) sh
|
||||||
|
|
||||||
|
run: ## Run the application server in the development runtime container
|
||||||
|
@$(MAKE) start
|
||||||
|
$(DEV) "rr serve"
|
||||||
|
|
||||||
|
stop: ## Stop and remove the development runtime container
|
||||||
|
@printf "$(YELLOW)$(WARN) Stopping all containers$(RESET)\n"
|
||||||
|
$(DOCKER) down
|
||||||
|
|
||||||
|
restart: ## Restart dev container (stop + start)
|
||||||
|
@$(MAKE) stop
|
||||||
|
@$(MAKE) start
|
||||||
|
|
||||||
|
rebuild: ## Rebuild containers (useful after Dockerfile changes)
|
||||||
|
@printf "$(YELLOW)$(SPARK) Rebuilding containers$(RESET)\n"
|
||||||
|
@$(MAKE) stop
|
||||||
|
@printf "$(YELLOW)$(TRASH) Deleting all Docker resources$(RESET)\n"
|
||||||
|
docker system prune --all --volumes --force
|
||||||
|
@$(MAKE) start
|
||||||
|
|
||||||
|
ps: ## Show docker compose ps
|
||||||
|
$(DOCKER) ps
|
||||||
|
|
||||||
|
migrate: ## Run database migrations in the migration container
|
||||||
|
$(DOCKER) up $(MIGRATION_CONTAINER)
|
||||||
|
|
||||||
|
# Composer helpers
|
||||||
|
composer-install: ## Install PHP dependencies in the composer runtime container
|
||||||
|
@printf "$(COMPOSE) $(GREEN)Installing PHP dependencies in $(COMPOSER_RUNTIME)$(RESET)\n"
|
||||||
|
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate
|
||||||
|
$(COMPOSER) "composer install --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-reqs"
|
||||||
|
|
||||||
|
composer-require: ## Require a PHP package in the composer runtime container (usage: make composer-require package=vendor/package)
|
||||||
|
ifndef package
|
||||||
|
$(error package variable is required: make composer-require package=vendor/package)
|
||||||
|
endif
|
||||||
|
@printf "$(COMPOSE) $(MAGNIFY) Requiring package $(package) in $(COMPOSER_RUNTIME)$(RESET)\n"
|
||||||
|
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate
|
||||||
|
$(COMPOSER) "composer require $(package) --ignore-platform-reqs"
|
||||||
|
|
||||||
|
composer-require-dev: ## Require a PHP package as dev in the composer runtime container (usage: make composer-require-dev package=vendor/package)
|
||||||
|
ifndef package
|
||||||
|
$(error package variable is required: make composer-require-dev package=vendor/package)
|
||||||
|
endif
|
||||||
|
@printf "$(COMPOSE) $(MAGNIFY) Requiring dev package $(package) in $(COMPOSER_RUNTIME)$(RESET)\n"
|
||||||
|
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate
|
||||||
|
$(COMPOSER) "composer require --dev $(package) --ignore-platform-reqs"
|
||||||
|
|
||||||
|
composer-update: ## Update PHP dependencies in the composer runtime container
|
||||||
|
@printf "$(COMPOSE) $(MAGNIFY) Updating PHP dependencies$(RESET)\n"
|
||||||
|
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate
|
||||||
|
$(COMPOSER) "composer update --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-reqs"
|
||||||
|
|
||||||
|
enable-debug: ## Enable Xdebug in the development runtime container
|
||||||
|
@$(DOCKER) up $(DEV_RUNTIME) -d --no-recreate
|
||||||
|
@printf "$(GREEN)$(BUG) Enabling Xdebug in $(DEV_RUNTIME)$(RESET)\n"
|
||||||
|
$(DEV) "bin/xdebug.sh"
|
||||||
|
|
||||||
|
enable-coverage: ## Enable PCOV code coverage in the composer runtime container
|
||||||
|
@printf "$(GREEN)$(MAGNIFY) Enabling PCOV in $(COMPOSER_RUNTIME)$(RESET)\n"
|
||||||
|
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate
|
||||||
|
$(COMPOSER) "bin/pcov.sh"
|
||||||
|
|
||||||
|
protoc: ## Generate PHP gRPC code from .proto files
|
||||||
|
@printf "$(PROTO) $(GREEN)Setting up protoc-gen-php-grpc plugin$(RESET)\n"
|
||||||
|
@curl -LOs $(PROTOC_URL)
|
||||||
|
@tar -xzf protoc-gen-php-grpc-2025.1.5-darwin-arm64.tar.gz
|
||||||
|
@printf "$(PROTO) $(GREEN)Generating PHP gRPC code from .proto files$(RESET)\n"
|
||||||
|
@protoc --plugin=./protoc-gen-php-grpc-2025.1.5-darwin-arm64/protoc-gen-php-grpc \
|
||||||
|
--php_out=./generated \
|
||||||
|
--php-grpc_out=./generated \
|
||||||
|
protos/example.proto
|
||||||
|
@printf "$(TRASH) $(GREEN)Cleaning up protoc-gen-php-grpc plugin files$(RESET)\n"
|
||||||
|
@rm -rf $(PROTOC_GEN_DIR) protoc-gen-php-grpc-2025.1.5-darwin-arm64.tar.gz
|
||||||
|
|
||||||
|
license-check: ## Check license headers in source files
|
||||||
|
@printf "$(MAGNIFY) $(GREEN)Checking license headers$(RESET)\n"
|
||||||
|
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate
|
||||||
|
$(COMPOSER) "composer run-script tests:license || true"
|
||||||
|
|
||||||
|
# Developer tasks
|
||||||
|
lint: ## Run linting (phpcs/phpstan) in composer runtime
|
||||||
|
@printf "$(MAGNIFY) $(GREEN)Running linters$(RESET)\n"
|
||||||
|
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate
|
||||||
|
$(COMPOSER) "composer run-script tests:lint || true"
|
||||||
|
$(COMPOSER) "composer run-script tests:phpstan || true"
|
||||||
|
|
||||||
|
fmt: ## Format code (php-cs-fixer)
|
||||||
|
@printf "$(MAGNIFY) $(GREEN)Formatting code$(RESET)\n"
|
||||||
|
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate
|
||||||
|
$(COMPOSER) "composer run-script tests:lint:fix"
|
||||||
|
|
||||||
|
test: ## Run test suite (phpunit)
|
||||||
|
@printf "$(CHECK) $(GREEN)Running unit tests$(RESET)\n"
|
||||||
|
@$(DOCKER) up $(COMPOSER_RUNTIME) -d
|
||||||
|
$(COMPOSER) "composer run-script tests:unit || true"
|
||||||
|
|
||||||
|
test-coverage: ## Run test suite with coverage report
|
||||||
|
@printf "$(CHECK) $(GREEN)Running unit tests with coverage report$(RESET)\n"
|
||||||
|
@$(DOCKER) up $(COMPOSER_RUNTIME) -d
|
||||||
|
@$(MAKE) enable-coverage
|
||||||
|
$(COMPOSER) "composer run-script tests:unit:coverage || true"
|
||||||
|
|
||||||
|
# Convenience aliases
|
||||||
|
dev: run ## Alias for start
|
||||||
|
ci: composer-install migrate license-check lint test ## CI-like local flow
|
||||||
|
down: stop ## Alias for stop
|
||||||
|
up: start ## Alias for start
|
||||||
|
|
||||||
|
.PONY: help start sh run stop restart rebuild ps migrate composer-install composer-require composer-require-dev composer-update enable-debug enable-coverage protoc license-check lint fmt test test-coverage dev ci down up
|
||||||
23
protos/example.proto
Normal file
23
protos/example.proto
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option go_package = "proto/greeter";
|
||||||
|
option php_namespace = "GRPC\\Greeter";
|
||||||
|
option php_metadata_namespace = "GRPC\\GPBMetadata";
|
||||||
|
|
||||||
|
package helloworld;
|
||||||
|
|
||||||
|
// The greeting service definition.
|
||||||
|
service Greeter {
|
||||||
|
// Sends a greeting
|
||||||
|
rpc SayHello (HelloRequest) returns (HelloReply) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The request message containing the user's name.
|
||||||
|
message HelloRequest {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The response message containing the greetings
|
||||||
|
message HelloReply {
|
||||||
|
string message = 1;
|
||||||
|
}
|
||||||
@@ -5,10 +5,7 @@ use Siteworxpro\App\Api;
|
|||||||
require __DIR__ . '/vendor/autoload.php';
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Instantiate the ExternalServer class
|
|
||||||
$server = new Api();
|
$server = new Api();
|
||||||
|
|
||||||
// Start the server
|
|
||||||
$server->startServer();
|
$server->startServer();
|
||||||
} catch (JsonException $e) {
|
} catch (JsonException $e) {
|
||||||
echo $e->getMessage();
|
echo $e->getMessage();
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Siteworxpro\App\Annotations\Guards;
|
|
||||||
|
|
||||||
use Attribute;
|
|
||||||
|
|
||||||
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
|
|
||||||
readonly class Scope
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private array $scopes = []
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getScopes(): array
|
|
||||||
{
|
|
||||||
return $this->scopes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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\MethodNotAllowedException;
|
||||||
use League\Route\Http\Exception\NotFoundException;
|
use League\Route\Http\Exception\NotFoundException;
|
||||||
|
use League\Route\RouteGroup;
|
||||||
use League\Route\Router;
|
use League\Route\Router;
|
||||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||||
use Siteworxpro\App\Controllers\HealthcheckController;
|
use Siteworxpro\App\Controllers\HealthcheckController;
|
||||||
use Siteworxpro\App\Controllers\IndexController;
|
use Siteworxpro\App\Controllers\IndexController;
|
||||||
|
use Siteworxpro\App\Controllers\OpenApiController;
|
||||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||||
use Siteworxpro\App\Http\Middleware\CorsMiddleware;
|
use Siteworxpro\App\Http\Middleware\CorsMiddleware;
|
||||||
use Siteworxpro\App\Http\Middleware\JwtMiddleware;
|
use Siteworxpro\App\Http\Middleware\JwtMiddleware;
|
||||||
use Siteworxpro\App\Http\Middleware\ScopeMiddleware;
|
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\Config;
|
||||||
use Siteworxpro\App\Services\Facades\Logger;
|
use Siteworxpro\App\Services\Facades\Logger;
|
||||||
use Siteworxpro\HttpStatus\CodesEnum;
|
|
||||||
use Spiral\RoadRunner\Http\PSR7Worker;
|
use Spiral\RoadRunner\Http\PSR7Worker;
|
||||||
use Spiral\RoadRunner\Worker;
|
use Spiral\RoadRunner\Worker;
|
||||||
|
|
||||||
@@ -69,8 +72,14 @@ class Api
|
|||||||
|
|
||||||
$this->router = new Router();
|
$this->router = new Router();
|
||||||
$this->router->get('/', IndexController::class . '::get');
|
$this->router->get('/', IndexController::class . '::get');
|
||||||
|
$this->router->post('/', IndexController::class . '::post');
|
||||||
$this->router->get('/healthz', HealthcheckController::class . '::get');
|
$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 CorsMiddleware());
|
||||||
$this->router->middleware(new JwtMiddleware());
|
$this->router->middleware(new JwtMiddleware());
|
||||||
$this->router->middleware(new ScopeMiddleware());
|
$this->router->middleware(new ScopeMiddleware());
|
||||||
@@ -104,28 +113,20 @@ class Api
|
|||||||
$response = $this->router->handle($request);
|
$response = $this->router->handle($request);
|
||||||
$this->worker->respond($response);
|
$this->worker->respond($response);
|
||||||
} catch (MethodNotAllowedException | NotFoundException) {
|
} catch (MethodNotAllowedException | NotFoundException) {
|
||||||
|
$uri = '';
|
||||||
|
if (isset($request)) {
|
||||||
|
$uri = $request->getUri()->getPath();
|
||||||
|
}
|
||||||
|
|
||||||
$this->worker->respond(
|
$this->worker->respond(
|
||||||
JsonResponseFactory::createJsonResponse(
|
JsonResponseFactory::createJsonResponse(new NotFoundResponse($uri))
|
||||||
['status_code' => 404, 'reason_phrase' => 'Not Found'],
|
|
||||||
CodesEnum::NOT_FOUND
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Logger::error($e->getMessage());
|
Logger::error($e->getMessage());
|
||||||
Logger::error($e->getTraceAsString());
|
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(
|
$this->worker->respond(
|
||||||
JsonResponseFactory::createJsonResponse($json, CodesEnum::INTERNAL_SERVER_ERROR)
|
JsonResponseFactory::createJsonResponse(new ServerErrorResponse($e))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(ticks=1);
|
|||||||
|
|
||||||
namespace Siteworxpro\App\Async;
|
namespace Siteworxpro\App\Async;
|
||||||
|
|
||||||
use Siteworxpro\App\Annotations\Async\HandlesMessage;
|
use Siteworxpro\App\Attributes\Async\HandlesMessage;
|
||||||
use Siteworxpro\App\Async\Messages\Message;
|
use Siteworxpro\App\Async\Messages\Message;
|
||||||
use Siteworxpro\App\Async\Queues\Queue;
|
use Siteworxpro\App\Async\Queues\Queue;
|
||||||
use Siteworxpro\App\Services\Facades\Broker;
|
use Siteworxpro\App\Services\Facades\Broker;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Siteworxpro\App\Async\Handlers;
|
namespace Siteworxpro\App\Async\Handlers;
|
||||||
|
|
||||||
use Siteworxpro\App\Annotations\Async\HandlesMessage;
|
use Siteworxpro\App\Attributes\Async\HandlesMessage;
|
||||||
use Siteworxpro\App\Async\Messages\Message;
|
use Siteworxpro\App\Async\Messages\Message;
|
||||||
use Siteworxpro\App\Async\Messages\SayHelloMessage;
|
use Siteworxpro\App\Async\Messages\SayHelloMessage;
|
||||||
use Siteworxpro\App\Services\Facades\Logger;
|
use Siteworxpro\App\Services\Facades\Logger;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Siteworxpro\App\Annotations\Async;
|
namespace Siteworxpro\App\Attributes\Async;
|
||||||
|
|
||||||
use Attribute;
|
use Attribute;
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Siteworxpro\App\Annotations\Events;
|
namespace Siteworxpro\App\Attributes\Events;
|
||||||
|
|
||||||
use Attribute;
|
use Attribute;
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Siteworxpro\App\Annotations\Guards;
|
namespace Siteworxpro\App\Attributes\Guards;
|
||||||
|
|
||||||
use Attribute;
|
use Attribute;
|
||||||
use Siteworxpro\App\Services\Facades\Config;
|
use Siteworxpro\App\Services\Facades\Config;
|
||||||
@@ -32,16 +32,6 @@ readonly class Jwt
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the required audience from configuration, ignoring any local override.
|
|
||||||
*
|
|
||||||
* @return string The globally configured audience or an empty string if not set.
|
|
||||||
*/
|
|
||||||
public function getRequiredAudience(): string
|
|
||||||
{
|
|
||||||
return Config::get('jwt.audience') ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the expected audience for validation.
|
* Get the expected audience for validation.
|
||||||
*
|
*
|
||||||
12
src/Attributes/Guards/RequireAllScopes.php
Normal file
12
src/Attributes/Guards/RequireAllScopes.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Attributes\Guards;
|
||||||
|
|
||||||
|
use Attribute;
|
||||||
|
|
||||||
|
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
|
||||||
|
readonly class RequireAllScopes
|
||||||
|
{
|
||||||
|
}
|
||||||
38
src/Attributes/Guards/Scope.php
Normal file
38
src/Attributes/Guards/Scope.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Attributes\Guards;
|
||||||
|
|
||||||
|
use Attribute;
|
||||||
|
|
||||||
|
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||||
|
readonly class Scope
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $scopes the required scopes
|
||||||
|
* @param string $claim the claim to check for scopes
|
||||||
|
* @param string $separator the separator used to split scopes in the claim
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private array $scopes = [],
|
||||||
|
private string $claim = 'scope',
|
||||||
|
private string $separator = ' '
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getScopes(): array
|
||||||
|
{
|
||||||
|
return $this->scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClaim(): string
|
||||||
|
{
|
||||||
|
return $this->claim;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSeparator(): string
|
||||||
|
{
|
||||||
|
return $this->separator;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ use Ahc\Cli\Application;
|
|||||||
use Siteworxpro\App\Cli\Commands\DemoCommand;
|
use Siteworxpro\App\Cli\Commands\DemoCommand;
|
||||||
use Siteworxpro\App\Cli\Commands\Queue\Start;
|
use Siteworxpro\App\Cli\Commands\Queue\Start;
|
||||||
use Siteworxpro\App\Cli\Commands\Queue\TestJob;
|
use Siteworxpro\App\Cli\Commands\Queue\TestJob;
|
||||||
|
use Siteworxpro\App\Helpers\Version;
|
||||||
use Siteworxpro\App\Kernel;
|
use Siteworxpro\App\Kernel;
|
||||||
use Siteworxpro\App\Services\Facades\Config;
|
use Siteworxpro\App\Services\Facades\Config;
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ class App
|
|||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
Kernel::boot();
|
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 DemoCommand());
|
||||||
$this->app->add(new Start());
|
$this->app->add(new Start());
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ namespace Siteworxpro\App\Controllers;
|
|||||||
|
|
||||||
use League\Route\Http\Exception\NotFoundException;
|
use League\Route\Http\Exception\NotFoundException;
|
||||||
use Nyholm\Psr7\ServerRequest;
|
use Nyholm\Psr7\ServerRequest;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Siteworxpro\App\Helpers\Version;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Controller
|
* Class Controller
|
||||||
@@ -15,6 +17,18 @@ use Psr\Http\Message\ResponseInterface;
|
|||||||
*
|
*
|
||||||
* @package Siteworxpro\App\Controllers
|
* @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
|
abstract class Controller implements ControllerInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ use Illuminate\Database\PostgresConnection;
|
|||||||
use Nyholm\Psr7\ServerRequest;
|
use Nyholm\Psr7\ServerRequest;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||||
|
use Siteworxpro\App\Http\Responses\GenericResponse;
|
||||||
use Siteworxpro\App\Models\Model;
|
use Siteworxpro\App\Models\Model;
|
||||||
use Siteworxpro\App\Services\Facades\Redis;
|
use Siteworxpro\App\Services\Facades\Redis;
|
||||||
use Siteworxpro\HttpStatus\CodesEnum;
|
use Siteworxpro\HttpStatus\CodesEnum;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class HealthcheckController
|
* Class HealthcheckController
|
||||||
@@ -22,8 +24,13 @@ use Siteworxpro\HttpStatus\CodesEnum;
|
|||||||
class HealthcheckController extends Controller
|
class HealthcheckController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
* Handles the GET request for health check.
|
||||||
|
*
|
||||||
* @throws \JsonException
|
* @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
|
public function get(ServerRequest $request): ResponseInterface
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -47,7 +54,7 @@ class HealthcheckController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
return JsonResponseFactory::createJsonResponse(
|
return JsonResponseFactory::createJsonResponse(
|
||||||
['status_code' => 200, 'message' => 'Healthcheck OK']
|
new GenericResponse('Healthcheck OK', CodesEnum::OK->value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ namespace Siteworxpro\App\Controllers;
|
|||||||
|
|
||||||
use Nyholm\Psr7\ServerRequest;
|
use Nyholm\Psr7\ServerRequest;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Siteworxpro\App\Annotations\Guards;
|
use Siteworxpro\App\Attributes\Guards;
|
||||||
|
use Siteworxpro\App\Docs\TokenSecurity;
|
||||||
|
use Siteworxpro\App\Docs\UnauthorizedResponse;
|
||||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
|
use Siteworxpro\App\Http\Responses\GenericResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class IndexController
|
* Class IndexController
|
||||||
@@ -22,19 +26,36 @@ class IndexController extends Controller
|
|||||||
* @throws \JsonException
|
* @throws \JsonException
|
||||||
*/
|
*/
|
||||||
#[Guards\Jwt]
|
#[Guards\Jwt]
|
||||||
#[Guards\Scope(['get.index'])]
|
#[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
|
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
|
* @throws \JsonException
|
||||||
*/
|
*/
|
||||||
#[Guards\Jwt]
|
#[Guards\Jwt]
|
||||||
#[Guards\Scope(['post.index'])]
|
#[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
|
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'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,10 @@ namespace Siteworxpro\App\Events;
|
|||||||
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
|
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
|
||||||
use Illuminate\Contracts\Support\Arrayable;
|
use Illuminate\Contracts\Support\Arrayable;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Siteworxpro\App\Annotations\Events\ListensFor;
|
use Siteworxpro\App\Attributes\Events\ListensFor;
|
||||||
|
|
||||||
|
use function React\Async\await;
|
||||||
|
use function React\Async\coroutine;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Dispatcher
|
* Class Dispatcher
|
||||||
@@ -29,6 +32,8 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
*/
|
*/
|
||||||
private Collection $pushed;
|
private Collection $pushed;
|
||||||
|
|
||||||
|
private array $subscribers = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string LISTENERS_NAMESPACE The namespace where listeners are located
|
* @var string LISTENERS_NAMESPACE The namespace where listeners are located
|
||||||
*/
|
*/
|
||||||
@@ -40,6 +45,16 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
$this->registerListeners();
|
$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.
|
* Register event listeners based on the ListensFor attribute.
|
||||||
*
|
*
|
||||||
@@ -99,7 +114,7 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
*/
|
*/
|
||||||
public function subscribe($subscriber): void
|
public function subscribe($subscriber): void
|
||||||
{
|
{
|
||||||
$this->listeners = array_merge($this->listeners, (array) $subscriber);
|
$this->subscribers[] = $subscriber;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,6 +123,7 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
* @param $event
|
* @param $event
|
||||||
* @param array $payload
|
* @param array $payload
|
||||||
* @return array|null
|
* @return array|null
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function until($event, $payload = []): array|null
|
public function until($event, $payload = []): array|null
|
||||||
{
|
{
|
||||||
@@ -121,6 +137,7 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
* @param array $payload
|
* @param array $payload
|
||||||
* @param bool $halt
|
* @param bool $halt
|
||||||
* @return array|null
|
* @return array|null
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function dispatch($event, $payload = [], $halt = false): array|null
|
public function dispatch($event, $payload = [], $halt = false): array|null
|
||||||
{
|
{
|
||||||
@@ -130,23 +147,46 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
$eventClass = $event;
|
$eventClass = $event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle subscribers as a coroutine
|
||||||
|
$promise = coroutine(function () use ($event, $payload, $halt, $eventClass, &$responses) {
|
||||||
|
foreach ($this->subscribers as $subscriber) {
|
||||||
|
if (method_exists($subscriber, 'handle')) {
|
||||||
|
$response = $subscriber->handle($event, $payload);
|
||||||
|
$responses[$eventClass] = $response;
|
||||||
|
|
||||||
|
if ($halt && $response !== null) {
|
||||||
|
return $responses;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
$listeners = $this->listeners[$eventClass] ?? null;
|
$listeners = $this->listeners[$eventClass] ?? null;
|
||||||
|
|
||||||
|
// If no listeners, just await the subscriber promise
|
||||||
if ($listeners === null) {
|
if ($listeners === null) {
|
||||||
return null;
|
return await($promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
$responses = [];
|
$responses = [];
|
||||||
|
|
||||||
foreach ($listeners as $listener) {
|
foreach ($listeners as $listener) {
|
||||||
$response = $listener($event, $payload);
|
$response = $listener($event, $payload);
|
||||||
$responses[] = $response;
|
$responses[$eventClass] = $response;
|
||||||
|
|
||||||
if ($halt && $response !== null) {
|
if ($halt && $response !== null) {
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Await the subscriber promise and merge responses
|
||||||
|
$promiseResponses = await($promise);
|
||||||
|
|
||||||
|
if (is_array($promiseResponses)) {
|
||||||
|
$responses = array_merge($responses, $promiseResponses);
|
||||||
|
}
|
||||||
|
|
||||||
return $responses;
|
return $responses;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +207,7 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
*
|
*
|
||||||
* @param $event
|
* @param $event
|
||||||
* @return void
|
* @return void
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function flush($event): void
|
public function flush($event): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace Siteworxpro\App\Events\Listeners\Database;
|
|||||||
|
|
||||||
use Illuminate\Database\Events\ConnectionEstablished;
|
use Illuminate\Database\Events\ConnectionEstablished;
|
||||||
use Illuminate\Database\Events\ConnectionEvent;
|
use Illuminate\Database\Events\ConnectionEvent;
|
||||||
use Siteworxpro\App\Annotations\Events\ListensFor;
|
use Siteworxpro\App\Attributes\Events\ListensFor;
|
||||||
use Siteworxpro\App\Events\Listeners\Listener;
|
use Siteworxpro\App\Events\Listeners\Listener;
|
||||||
use Siteworxpro\App\Services\Facades\Logger;
|
use Siteworxpro\App\Services\Facades\Logger;
|
||||||
|
|
||||||
@@ -18,12 +18,15 @@ use Siteworxpro\App\Services\Facades\Logger;
|
|||||||
class Connected extends Listener
|
class Connected extends Listener
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param ConnectionEvent $event
|
* @param mixed $event
|
||||||
* @param array $payload
|
* @param array $payload
|
||||||
* @return null
|
* @return null
|
||||||
*/
|
*/
|
||||||
public function __invoke($event, array $payload = []): null
|
public function __invoke(mixed $event, array $payload = []): null
|
||||||
{
|
{
|
||||||
|
if (!($event instanceof ConnectionEvent)) {
|
||||||
|
throw new \TypeError("Invalid event type passed to listener " . static::class);
|
||||||
|
}
|
||||||
|
|
||||||
Logger::info("Database connection event", [get_class($event), $event->connectionName]);
|
Logger::info("Database connection event", [get_class($event), $event->connectionName]);
|
||||||
|
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
54
src/Grpc.php
Normal file
54
src/Grpc.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App;
|
||||||
|
|
||||||
|
use GRPC\Greeter\GreeterInterface;
|
||||||
|
use Siteworxpro\App\GrpcHandlers\GreeterHandler;
|
||||||
|
use Siteworxpro\App\Services\Facades\Config;
|
||||||
|
use Spiral\RoadRunner\GRPC\Invoker;
|
||||||
|
use Spiral\RoadRunner\GRPC\Server;
|
||||||
|
use Spiral\RoadRunner\Worker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Grpc
|
||||||
|
*
|
||||||
|
* starts a gRPC server using RoadRunner
|
||||||
|
*
|
||||||
|
* @package Siteworxpro\App
|
||||||
|
*/
|
||||||
|
class Grpc
|
||||||
|
{
|
||||||
|
private const array SERVICES = [
|
||||||
|
GreeterInterface::class => GreeterHandler::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \ReflectionException
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
Kernel::boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the gRPC server
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function start(): int
|
||||||
|
{
|
||||||
|
$server = new Server(new Invoker(), [
|
||||||
|
'debug' => Config::get('app.dev_mode'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach (self::SERVICES as $interface => $handler) {
|
||||||
|
$server->registerService($interface, new $handler());
|
||||||
|
}
|
||||||
|
|
||||||
|
$server->serve(Worker::create());
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/GrpcHandlers/GreeterHandler.php
Normal file
21
src/GrpcHandlers/GreeterHandler.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\GrpcHandlers;
|
||||||
|
|
||||||
|
use GRPC\Greeter\GreeterInterface;
|
||||||
|
use GRPC\Greeter\HelloReply;
|
||||||
|
use GRPC\Greeter\HelloRequest;
|
||||||
|
use Spiral\RoadRunner\GRPC;
|
||||||
|
|
||||||
|
class GreeterHandler implements GreeterInterface
|
||||||
|
{
|
||||||
|
public function SayHello(GRPC\ContextInterface $ctx, HelloRequest $in): HelloReply // phpcs:ignore
|
||||||
|
{
|
||||||
|
$reply = new HelloReply();
|
||||||
|
$reply->setMessage('Hello ' . $in->getName());
|
||||||
|
|
||||||
|
return $reply;
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
namespace Siteworxpro\App\Http;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Support\Arrayable;
|
||||||
use Nyholm\Psr7\Response;
|
use Nyholm\Psr7\Response;
|
||||||
use Siteworxpro\HttpStatus\CodesEnum;
|
use Siteworxpro\HttpStatus\CodesEnum;
|
||||||
|
|
||||||
@@ -17,13 +18,19 @@ class JsonResponseFactory
|
|||||||
/**
|
/**
|
||||||
* Create a JSON response with the given data and status code.
|
* 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.
|
* @param CodesEnum $statusCode The HTTP status code for the response.
|
||||||
* @return Response The JSON response.
|
* @return Response The JSON response.
|
||||||
* @throws \JsonException
|
* @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(
|
return new Response(
|
||||||
status: $statusCode->value,
|
status: $statusCode->value,
|
||||||
headers: [
|
headers: [
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ namespace Siteworxpro\App\Http\Middleware;
|
|||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Carbon\WrapperClock;
|
use Carbon\WrapperClock;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
use Lcobucci\JWT\JwtFacade;
|
use Lcobucci\JWT\JwtFacade;
|
||||||
use Lcobucci\JWT\Signer\Hmac\Sha256 as Hmac256;
|
use Lcobucci\JWT\Signer\Hmac\Sha256 as Hmac256;
|
||||||
|
use Lcobucci\JWT\Signer\Key;
|
||||||
use Lcobucci\JWT\Signer\Key\InMemory;
|
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||||
use Lcobucci\JWT\Signer\Rsa\Sha256;
|
use Lcobucci\JWT\Signer\Rsa\Sha256;
|
||||||
use Lcobucci\JWT\Token\InvalidTokenStructure;
|
use Lcobucci\JWT\Token\InvalidTokenStructure;
|
||||||
@@ -21,10 +23,12 @@ use League\Route\Dispatcher;
|
|||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Siteworxpro\App\Annotations\Guards\Jwt;
|
use Siteworxpro\App\Attributes\Guards\Jwt;
|
||||||
use Siteworxpro\App\Controllers\Controller;
|
use Siteworxpro\App\Controllers\Controller;
|
||||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||||
use Siteworxpro\App\Services\Facades\Config;
|
use Siteworxpro\App\Services\Facades\Config;
|
||||||
|
use Siteworxpro\App\Services\Facades\Guzzle;
|
||||||
|
use Siteworxpro\App\Services\Facades\Redis;
|
||||||
use Siteworxpro\HttpStatus\CodesEnum;
|
use Siteworxpro\HttpStatus\CodesEnum;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,7 +107,7 @@ class JwtMiddleware extends Middleware
|
|||||||
/** @var Jwt $jwtInstance */
|
/** @var Jwt $jwtInstance */
|
||||||
$jwtInstance = $attribute->newInstance();
|
$jwtInstance = $attribute->newInstance();
|
||||||
|
|
||||||
if ($jwtInstance->getRequiredAudience() !== '') {
|
if ($jwtInstance->getAudience() !== '') {
|
||||||
$requiredAudience = $jwtInstance->getAudience();
|
$requiredAudience = $jwtInstance->getAudience();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +118,7 @@ class JwtMiddleware extends Middleware
|
|||||||
// Parse and validate the token with signature, time, issuer and audience constraints.
|
// Parse and validate the token with signature, time, issuer and audience constraints.
|
||||||
$jwt = new JwtFacade()->parse(
|
$jwt = new JwtFacade()->parse(
|
||||||
$token,
|
$token,
|
||||||
$this->getSignedWith(),
|
$this->getSignedWith($token),
|
||||||
Config::get('jwt.strict_validation') ?
|
Config::get('jwt.strict_validation') ?
|
||||||
new StrictValidAt(new WrapperClock(Carbon::now())) :
|
new StrictValidAt(new WrapperClock(Carbon::now())) :
|
||||||
new LooseValidAt(new WrapperClock(Carbon::now())),
|
new LooseValidAt(new WrapperClock(Carbon::now())),
|
||||||
@@ -129,16 +133,21 @@ class JwtMiddleware extends Middleware
|
|||||||
}
|
}
|
||||||
|
|
||||||
return JsonResponseFactory::createJsonResponse([
|
return JsonResponseFactory::createJsonResponse([
|
||||||
'status_code' => 401,
|
'status_code' => CodesEnum::UNAUTHORIZED->value,
|
||||||
'message' => 'Unauthorized: Invalid token',
|
'message' => 'Unauthorized: Invalid token',
|
||||||
'errors' => $violations
|
'errors' => $violations
|
||||||
], CodesEnum::UNAUTHORIZED);
|
], CodesEnum::UNAUTHORIZED);
|
||||||
} catch (InvalidTokenStructure) {
|
} catch (InvalidTokenStructure) {
|
||||||
// Token could not be parsed due to malformed structure.
|
// Token could not be parsed due to malformed structure.
|
||||||
return JsonResponseFactory::createJsonResponse([
|
return JsonResponseFactory::createJsonResponse([
|
||||||
'status_code' => 401,
|
'status_code' => CodesEnum::UNAUTHORIZED->value,
|
||||||
'message' => 'Unauthorized: Invalid token',
|
'message' => 'Unauthorized: Invalid token',
|
||||||
], CodesEnum::UNAUTHORIZED);
|
], CodesEnum::UNAUTHORIZED);
|
||||||
|
} catch (GuzzleException | \RuntimeException) {
|
||||||
|
return JsonResponseFactory::createJsonResponse([
|
||||||
|
'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.
|
// Expose all token claims as request attributes for downstream consumers.
|
||||||
@@ -161,20 +170,30 @@ class JwtMiddleware extends Middleware
|
|||||||
* @return SignedWith Signature constraint used during JWT parsing.
|
* @return SignedWith Signature constraint used during JWT parsing.
|
||||||
*
|
*
|
||||||
* @throws \RuntimeException When no signing key is configured.
|
* @throws \RuntimeException When no signing key is configured.
|
||||||
|
* @throws \JsonException
|
||||||
*/
|
*/
|
||||||
private function getSignedWith(): SignedWith
|
private function getSignedWith(string $token): SignedWith
|
||||||
{
|
{
|
||||||
$key = Config::get('jwt.signing_key');
|
$keyConfig = Config::get('jwt.signing_key');
|
||||||
|
|
||||||
if ($key === null) {
|
if ($keyConfig === null) {
|
||||||
throw new \RuntimeException('JWT signing key is not configured.');
|
throw new \RuntimeException('JWT signing key is not configured.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load key either from file or raw text.
|
// file:// path to key
|
||||||
if (str_starts_with($key, 'file://')) {
|
if (str_starts_with($keyConfig, 'file://')) {
|
||||||
$key = InMemory::file(substr($key, 7));
|
$key = InMemory::file(substr($keyConfig, 7));
|
||||||
|
// openid jwks url
|
||||||
|
} elseif (str_contains($keyConfig, '.well-known/')) {
|
||||||
|
$jwt = explode('.', $token);
|
||||||
|
if (count($jwt) !== 3) {
|
||||||
|
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
|
||||||
|
$key = $this->getJwksKey($keyConfig, $keyId);
|
||||||
} else {
|
} else {
|
||||||
$key = InMemory::plainText($key);
|
$key = InMemory::plainText($keyConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heuristic: if PEM public key content is detected, use RSA; otherwise use HMAC.
|
// Heuristic: if PEM public key content is detected, use RSA; otherwise use HMAC.
|
||||||
@@ -184,4 +203,120 @@ class JwtMiddleware extends Middleware
|
|||||||
|
|
||||||
return new SignedWith(new Hmac256(), $key);
|
return new SignedWith(new Hmac256(), $key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getJwksKey(string $url, string $keyId): Key
|
||||||
|
{
|
||||||
|
$cached = Redis::get('jwks_key_' . $keyId);
|
||||||
|
if ($cached !== null) {
|
||||||
|
return InMemory::plainText($cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 = Guzzle::get($jwksUri);
|
||||||
|
$jwksBody = json_decode(
|
||||||
|
$jwksResponse->getBody()->getContents(),
|
||||||
|
true,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
// For simplicity, we take the first key in the JWKS.
|
||||||
|
$firstKey = array_filter(
|
||||||
|
$jwksBody['keys'],
|
||||||
|
fn($key) => $key['kid'] === $keyId
|
||||||
|
)[0] ?? $jwksBody['keys'][0] ?? null;
|
||||||
|
|
||||||
|
if (empty($firstKey)) {
|
||||||
|
throw new \RuntimeException('No matching key found in JWKS for key ID: ' . $keyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$n = $firstKey['n'];
|
||||||
|
$e = $firstKey['e'];
|
||||||
|
$publicKeyPem = "-----BEGIN PUBLIC KEY-----\n" .
|
||||||
|
chunk_split(base64_encode($this->convertJwkToPem($n, $e)), 64) .
|
||||||
|
"-----END PUBLIC KEY-----\n";
|
||||||
|
|
||||||
|
Redis::set('jwks_key_' . $keyId, $publicKeyPem, 'EX', 3600);
|
||||||
|
|
||||||
|
return InMemory::plainText($publicKeyPem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a DER-encoded SubjectPublicKeyInfo from JWK 'n' and 'e'.
|
||||||
|
* Returns raw DER bytes; caller base64-encodes and wraps with PEM headers.
|
||||||
|
*/
|
||||||
|
private function convertJwkToPem(string $n, string $e): string
|
||||||
|
{
|
||||||
|
$modulus = $this->base64UrlDecode($n);
|
||||||
|
$exponent = $this->base64UrlDecode($e);
|
||||||
|
|
||||||
|
$derN = $this->derEncodeInteger($modulus);
|
||||||
|
$derE = $this->derEncodeInteger($exponent);
|
||||||
|
|
||||||
|
// RSAPublicKey (PKCS#1): SEQUENCE { n INTEGER, e INTEGER }
|
||||||
|
$rsaPublicKey = $this->derEncodeSequence($derN . $derE);
|
||||||
|
|
||||||
|
// AlgorithmIdentifier for rsaEncryption: 1.2.840.113549.1.1.1 with NULL
|
||||||
|
$algId = hex2bin('300d06092a864886f70d0101010500');
|
||||||
|
|
||||||
|
// SubjectPublicKey (SPKI) BIT STRING, 0 unused bits + RSAPublicKey
|
||||||
|
$subjectPublicKey = $this->derEncodeBitString($rsaPublicKey);
|
||||||
|
|
||||||
|
// SubjectPublicKeyInfo: SEQUENCE { algorithm AlgorithmIdentifier, subjectPublicKey BIT STRING }
|
||||||
|
return $this->derEncodeSequence($algId . $subjectPublicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function base64UrlDecode(string $data): string
|
||||||
|
{
|
||||||
|
$data = strtr($data, '-_', '+/');
|
||||||
|
$pad = strlen($data) % 4;
|
||||||
|
if ($pad) {
|
||||||
|
$data .= str_repeat('=', 4 - $pad);
|
||||||
|
}
|
||||||
|
return base64_decode($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function derEncodeLength(int $len): string
|
||||||
|
{
|
||||||
|
if ($len < 0x80) {
|
||||||
|
return chr($len);
|
||||||
|
}
|
||||||
|
$bytes = '';
|
||||||
|
while ($len > 0) {
|
||||||
|
$bytes = chr($len & 0xFF) . $bytes;
|
||||||
|
$len >>= 8;
|
||||||
|
}
|
||||||
|
return chr(0x80 | strlen($bytes)) . $bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function derEncodeInteger(string $bytes): string
|
||||||
|
{
|
||||||
|
// Remove leading zeroes
|
||||||
|
$bytes = ltrim($bytes, "\x00");
|
||||||
|
if ($bytes === '') {
|
||||||
|
$bytes = "\x00";
|
||||||
|
}
|
||||||
|
// Ensure positive INTEGER (prepend 0x00 if MSB set)
|
||||||
|
if ((ord($bytes[0]) & 0x80) !== 0) {
|
||||||
|
$bytes = "\x00" . $bytes;
|
||||||
|
}
|
||||||
|
return "\x02" . $this->derEncodeLength(strlen($bytes)) . $bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function derEncodeSequence(string $bytes): string
|
||||||
|
{
|
||||||
|
return "\x30" . $this->derEncodeLength(strlen($bytes)) . $bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function derEncodeBitString(string $bytes): string
|
||||||
|
{
|
||||||
|
// 0 unused bits + data
|
||||||
|
$payload = "\x00" . $bytes;
|
||||||
|
return "\x03" . $this->derEncodeLength(strlen($payload)) . $payload;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ use League\Route\Dispatcher;
|
|||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Siteworxpro\App\Annotations\Guards\Scope;
|
use Siteworxpro\App\Attributes\Guards\RequireAllScopes;
|
||||||
|
use Siteworxpro\App\Attributes\Guards\Scope;
|
||||||
use Siteworxpro\App\Controllers\Controller;
|
use Siteworxpro\App\Controllers\Controller;
|
||||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||||
use Siteworxpro\HttpStatus\CodesEnum;
|
use Siteworxpro\HttpStatus\CodesEnum;
|
||||||
@@ -42,7 +43,7 @@ class ScopeMiddleware extends Middleware
|
|||||||
*/
|
*/
|
||||||
public function process(
|
public function process(
|
||||||
ServerRequestInterface $request,
|
ServerRequestInterface $request,
|
||||||
RequestHandlerInterface | Dispatcher $handler
|
RequestHandlerInterface|Dispatcher $handler
|
||||||
): ResponseInterface {
|
): ResponseInterface {
|
||||||
// Attempt to resolve the route's callable [Controller instance, method name].
|
// Attempt to resolve the route's callable [Controller instance, method name].
|
||||||
$callable = $this->extractRouteCallable($handler);
|
$callable = $this->extractRouteCallable($handler);
|
||||||
@@ -57,26 +58,49 @@ class ScopeMiddleware extends Middleware
|
|||||||
// Ensure the controller exists and the method is defined before reflecting.
|
// Ensure the controller exists and the method is defined before reflecting.
|
||||||
if (class_exists($class::class)) {
|
if (class_exists($class::class)) {
|
||||||
$reflectionClass = new \ReflectionClass($class);
|
$reflectionClass = new \ReflectionClass($class);
|
||||||
|
|
||||||
if ($reflectionClass->hasMethod($method)) {
|
if ($reflectionClass->hasMethod($method)) {
|
||||||
$reflectionMethod = $reflectionClass->getMethod($method);
|
$reflectionMethod = $reflectionClass->getMethod($method);
|
||||||
|
|
||||||
// Fetch all Scope attributes declared on the method.
|
// Fetch all Scope attributes declared on the method.
|
||||||
$attributes = $reflectionMethod->getAttributes(Scope::class);
|
$attributes = $reflectionMethod->getAttributes(Scope::class);
|
||||||
|
$requireAllAttributes = $reflectionMethod->getAttributes(RequireAllScopes::class);
|
||||||
|
|
||||||
|
if (empty($attributes)) {
|
||||||
|
// No scope attributes; delegate to the next handler.
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredScopes = [];
|
||||||
|
$userScopes = [];
|
||||||
|
$requireAll = false;
|
||||||
|
|
||||||
foreach ($attributes as $attribute) {
|
foreach ($attributes as $attribute) {
|
||||||
/** @var Scope $scopeInstance Concrete Scope attribute instance. */
|
/** @var Scope $scopeInstance Concrete Scope attribute instance. */
|
||||||
$scopeInstance = $attribute->newInstance();
|
$scopeInstance = $attribute->newInstance();
|
||||||
$requiredScopes = $scopeInstance->getScopes();
|
$requiredScopes = array_merge($requiredScopes, $scopeInstance->getScopes());
|
||||||
|
|
||||||
// Retrieve user scopes from the request (defaults to an empty array).
|
// If any attribute requires all scopes, set the flag.
|
||||||
$userScopes = $request->getAttribute('scopes', []);
|
$requireAll = $requireAll || !empty($requireAllAttributes);
|
||||||
|
|
||||||
|
$scopes = $request->getAttribute($scopeInstance->getClaim());
|
||||||
|
if (!is_array($scopes)) {
|
||||||
|
// If user scopes are not an array, treat as no scopes provided.
|
||||||
|
$scopes = explode($scopeInstance->getSeparator(), (string) $scopes);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userScopes = array_merge(
|
||||||
|
$userScopes,
|
||||||
|
$scopes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userScopes = array_unique($userScopes);
|
||||||
|
|
||||||
// Deny if any required scope is missing from the user's scopes.
|
// Deny if any required scope is missing from the user's scopes.
|
||||||
if (
|
if (
|
||||||
array_any(
|
(!$requireAll && array_intersect($userScopes, $requiredScopes) === []) ||
|
||||||
$requiredScopes,
|
($requireAll && array_diff($requiredScopes, $userScopes) !== [])
|
||||||
fn($requiredScope) => !in_array($requiredScope, $userScopes, true)
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
return JsonResponseFactory::createJsonResponse([
|
return JsonResponseFactory::createJsonResponse([
|
||||||
'error' => 'insufficient_scope',
|
'error' => 'insufficient_scope',
|
||||||
@@ -86,7 +110,6 @@ class ScopeMiddleware extends Middleware
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// All checks passed; continue down the middleware pipeline.
|
// All checks passed; continue down the middleware pipeline.
|
||||||
return $handler->handle($request);
|
return $handler->handle($request);
|
||||||
|
|||||||
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;
|
namespace Siteworxpro\App\Models;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
|
use Siteworxpro\App\Helpers\Ulid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class User
|
* Class User
|
||||||
*
|
*
|
||||||
* @property string $id
|
* @property-read string $id
|
||||||
* @property string $first_name
|
* @property string $first_name
|
||||||
* @property string $last_name
|
* @property string $last_name
|
||||||
* @property string $email
|
* @property string $email
|
||||||
@@ -19,6 +21,23 @@ use Carbon\Carbon;
|
|||||||
* @property-read string $full_name
|
* @property-read string $full_name
|
||||||
* @property-read string $formatted_email
|
* @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
|
class User extends Model
|
||||||
{
|
{
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@@ -36,6 +55,12 @@ class User extends Model
|
|||||||
'password',
|
'password',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function __construct(array $attributes = [])
|
||||||
|
{
|
||||||
|
parent::__construct($attributes);
|
||||||
|
$this->attributes['id'] = $this->attributes['id'] ?? Ulid::generate();
|
||||||
|
}
|
||||||
|
|
||||||
public function getFullNameAttribute(): string
|
public function getFullNameAttribute(): string
|
||||||
{
|
{
|
||||||
return "$this->first_name $this->last_name";
|
return "$this->first_name $this->last_name";
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use Siteworxpro\App\Services\Facade;
|
|||||||
* It extends the Facade class from the Illuminate\Support\Facades namespace.
|
* It extends the Facade class from the Illuminate\Support\Facades namespace.
|
||||||
*
|
*
|
||||||
* @method static array | bool | string | int | null get(string $key) Retrieve the configuration value for the given key. // @codingStandardsIgnoreStart
|
* @method static array | bool | string | int | null get(string $key) Retrieve the configuration value for the given key. // @codingStandardsIgnoreStart
|
||||||
|
* @method static void set(string $key, mixed $value) Set the configuration value for the given key. // @codingStandardsIgnoreEnd
|
||||||
*
|
*
|
||||||
* @package Siteworx\App\Facades
|
* @package Siteworx\App\Facades
|
||||||
*/
|
*/
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ namespace Siteworxpro\App\Services\ServiceProviders;
|
|||||||
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Siteworxpro\App\Log\Logger;
|
use Siteworxpro\App\Log\Logger;
|
||||||
|
use Siteworxpro\App\Services\Facades\Config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class LoggerServiceProvider
|
* Class LoggerServiceProvider
|
||||||
@@ -17,7 +18,7 @@ class LoggerServiceProvider extends ServiceProvider
|
|||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
$this->app->singleton(Logger::class, function () {
|
$this->app->singleton(Logger::class, function () {
|
||||||
return new Logger();
|
return new Logger(Config::get('app.log_level'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
tests/Attributes/Guards/JwtTest.php
Normal file
49
tests/Attributes/Guards/JwtTest.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\Attributes\Guards;
|
||||||
|
|
||||||
|
use Siteworxpro\App\Attributes\Guards\Jwt;
|
||||||
|
use Siteworxpro\App\Services\Facades\Config;
|
||||||
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
|
class JwtTest extends Unit
|
||||||
|
{
|
||||||
|
public function testGetsClassFromConfig(): void
|
||||||
|
{
|
||||||
|
Config::set('jwt.issuer', 'default-issuer');
|
||||||
|
Config::set('jwt.audience', 'default-audience');
|
||||||
|
|
||||||
|
$reflection = new \ReflectionClass(TestClass::class);
|
||||||
|
$attributes = $reflection->getAttributes(Jwt::class);
|
||||||
|
$this->assertCount(1, $attributes);
|
||||||
|
|
||||||
|
/** @var Jwt $instance */
|
||||||
|
$instance = $attributes[0]->newInstance();
|
||||||
|
$this->assertEquals('default-audience', $instance->getAudience());
|
||||||
|
$this->assertEquals('default-issuer', $instance->getIssuer());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetsClassFromCustom(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass(TestClassSpecific::class);
|
||||||
|
$attributes = $reflection->getAttributes(Jwt::class);
|
||||||
|
$this->assertCount(1, $attributes);
|
||||||
|
|
||||||
|
/** @var Jwt $instance */
|
||||||
|
$instance = $attributes[0]->newInstance();
|
||||||
|
$this->assertEquals('custom-audience', $instance->getAudience());
|
||||||
|
$this->assertEquals('custom-issuer', $instance->getIssuer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Jwt]
|
||||||
|
class TestClass // @codingStandardsIgnoreLine
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Jwt('custom-issuer', 'custom-audience')]
|
||||||
|
class TestClassSpecific // @codingStandardsIgnoreLine
|
||||||
|
{
|
||||||
|
}
|
||||||
43
tests/Attributes/Guards/ScopeTest.php
Normal file
43
tests/Attributes/Guards/ScopeTest.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\Attributes\Guards;
|
||||||
|
|
||||||
|
use Siteworxpro\App\Attributes\Guards\Scope;
|
||||||
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
|
class ScopeTest extends Unit
|
||||||
|
{
|
||||||
|
public function testGetsClassSingle(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass(TestClassSingle::class);
|
||||||
|
$attributes = $reflection->getAttributes(Scope::class);
|
||||||
|
$this->assertCount(1, $attributes);
|
||||||
|
|
||||||
|
/** @var Scope $instance */
|
||||||
|
$instance = $attributes[0]->newInstance();
|
||||||
|
$this->assertEquals(['read:users'], $instance->getScopes());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetsClassFromCustom(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass(TestClassMultiple::class);
|
||||||
|
$attributes = $reflection->getAttributes(Scope::class);
|
||||||
|
$this->assertCount(1, $attributes);
|
||||||
|
|
||||||
|
/** @var Scope $instance */
|
||||||
|
$instance = $attributes[0]->newInstance();
|
||||||
|
$this->assertEquals(['read:users', 'write:users'], $instance->getScopes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Scope(['read:users', 'write:users'])]
|
||||||
|
class TestClassMultiple // @codingStandardsIgnoreLine
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Scope(['read:users'])]
|
||||||
|
class TestClassSingle // @codingStandardsIgnoreLine
|
||||||
|
{
|
||||||
|
}
|
||||||
25
tests/Attributes/HandlesMessageTest.php
Normal file
25
tests/Attributes/HandlesMessageTest.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\Attributes;
|
||||||
|
|
||||||
|
use Siteworxpro\App\Attributes\Async\HandlesMessage;
|
||||||
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
|
class HandlesMessageTest extends Unit
|
||||||
|
{
|
||||||
|
public function testGetsClass(): void
|
||||||
|
{
|
||||||
|
$class = new #[HandlesMessage('Siteworxpro\Tests\Attributes\TestClass')] class {
|
||||||
|
};
|
||||||
|
|
||||||
|
$reflection = new \ReflectionClass($class);
|
||||||
|
$attributes = $reflection->getAttributes(HandlesMessage::class);
|
||||||
|
$this->assertCount(1, $attributes);
|
||||||
|
|
||||||
|
/** @var HandlesMessage $instance */
|
||||||
|
$instance = $attributes[0]->newInstance();
|
||||||
|
$this->assertEquals('Siteworxpro\Tests\Attributes\TestClass', $instance->getMessageClass());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,21 @@ class IndexControllerTest extends AbstractController
|
|||||||
$response = $controller->get($this->getMockRequest());
|
$response = $controller->get($this->getMockRequest());
|
||||||
|
|
||||||
$this->assertEquals(200, $response->getStatusCode());
|
$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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testPost(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(true);
|
||||||
|
|
||||||
|
$controller = new IndexController();
|
||||||
|
|
||||||
|
$response = $controller->post($this->getMockRequest());
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$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');
|
||||||
|
}
|
||||||
|
}
|
||||||
47
tests/Events/Listeners/ConnectedTest.php
Normal file
47
tests/Events/Listeners/ConnectedTest.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\Events\Listeners;
|
||||||
|
|
||||||
|
use Illuminate\Database\Events\ConnectionEstablished;
|
||||||
|
use Psr\Container\ContainerExceptionInterface;
|
||||||
|
use Psr\Container\NotFoundExceptionInterface;
|
||||||
|
use Psr\Log\LogLevel;
|
||||||
|
use Siteworxpro\App\Events\Listeners\Database\Connected;
|
||||||
|
use Siteworxpro\App\Log\Logger;
|
||||||
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
|
class ConnectedTest extends Unit
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws \ReflectionException
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$inputBuffer = fopen('php://memory', 'r+');
|
||||||
|
$logger = new Logger(LogLevel::DEBUG, $inputBuffer);
|
||||||
|
\Siteworxpro\App\Services\Facades\Logger::getFacadeContainer()->bind(Logger::class, fn() => $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandlesEvent()
|
||||||
|
{
|
||||||
|
$this->expectNotToPerformAssertions();
|
||||||
|
|
||||||
|
$connectedEvent = $this->createMock(ConnectionEstablished::class);
|
||||||
|
$listener = new Connected();
|
||||||
|
|
||||||
|
$listener->__invoke($connectedEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsException()
|
||||||
|
{
|
||||||
|
$this->expectException(\TypeError::class);
|
||||||
|
$listener = new Connected();
|
||||||
|
$listener->__invoke(new \stdClass());
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ use Siteworxpro\App\Http\Middleware\CorsMiddleware;
|
|||||||
use Siteworxpro\App\Services\Facades\Config;
|
use Siteworxpro\App\Services\Facades\Config;
|
||||||
use Siteworxpro\Tests\Unit;
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
class CorsMiddlewareTest extends Unit
|
class CorsMiddlewareTest extends Middleware
|
||||||
{
|
{
|
||||||
public function testAllowsConfiguredOrigin(): void
|
public function testAllowsConfiguredOrigin(): void
|
||||||
{
|
{
|
||||||
@@ -80,22 +80,4 @@ class CorsMiddlewareTest extends Unit
|
|||||||
|
|
||||||
$this->assertEquals('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
|
$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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
366
tests/Http/Middleware/JwtMiddlewareTest.php
Normal file
366
tests/Http/Middleware/JwtMiddlewareTest.php
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\Http\Middleware;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Lcobucci\JWT\JwtFacade;
|
||||||
|
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||||
|
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||||
|
use Lcobucci\JWT\Token\Builder;
|
||||||
|
use League\Route\Dispatcher;
|
||||||
|
use Nyholm\Psr7\Response;
|
||||||
|
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 {
|
||||||
|
public function getCallable(): array
|
||||||
|
{
|
||||||
|
return [$this, 'index'];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Jwt]
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Dummy method for testing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Config::set('jwt.signing_key', self::TEST_SIGNING_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testIgnoresNoJwtAttribute()
|
||||||
|
{
|
||||||
|
$class = new class {
|
||||||
|
public function getCallable(): array
|
||||||
|
{
|
||||||
|
return [$this, 'index'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Dummy method for testing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$handler
|
||||||
|
->shouldReceive('handle')
|
||||||
|
->once()
|
||||||
|
->andReturn(new Response(200));
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$middleware = new JwtMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::OK->value, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testIgnoresJwtAttributeButNoToken()
|
||||||
|
{
|
||||||
|
$class = $this->getClass();
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$middleware = new JwtMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testInvalidToken()
|
||||||
|
{
|
||||||
|
$class = $this->getClass();
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$request = $request->withHeader('Authorization', 'Bearer ' . 'invalid_token_string');
|
||||||
|
$middleware = new JwtMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
'Unauthorized: Invalid token',
|
||||||
|
$response->getBody()->getContents()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testJwtAttributeWithTokenButWrongAud()
|
||||||
|
{
|
||||||
|
$class = $this->getClass();
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwt());
|
||||||
|
$middleware = new JwtMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
'The token is not allowed to be used by this audience',
|
||||||
|
$response->getBody()->getContents()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testJwtAttributeWithTokenButWrongIss()
|
||||||
|
{
|
||||||
|
Config::set('jwt.audience', 'https://client-app.io');
|
||||||
|
|
||||||
|
$class = $this->getClass();
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwt());
|
||||||
|
$middleware = new JwtMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
'The token was not issued by the given issuers',
|
||||||
|
$response->getBody()->getContents()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testJwtAttributeWithTokenWithDiffIssuer()
|
||||||
|
{
|
||||||
|
Config::set('jwt.audience', 'https://client-app.io');
|
||||||
|
Config::set('jwt.issuer', 'https://different-issuer.io');
|
||||||
|
|
||||||
|
$class = $this->getClass();
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwt());
|
||||||
|
$middleware = new JwtMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
'The token was not issued by the given issuers',
|
||||||
|
$response->getBody()->getContents()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testJwtAttributeWithToken()
|
||||||
|
{
|
||||||
|
Config::set('jwt.audience', 'https://client-app.io');
|
||||||
|
Config::set('jwt.issuer', 'https://api.my-awesome-app.io');
|
||||||
|
|
||||||
|
$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->getJwt());
|
||||||
|
$middleware = new JwtMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$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);
|
||||||
|
$signer = new 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
tests/Http/Middleware/Middleware.php
Normal file
32
tests/Http/Middleware/Middleware.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\Http\Middleware;
|
||||||
|
|
||||||
|
use Nyholm\Psr7\Response;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
|
abstract class Middleware extends Unit
|
||||||
|
{
|
||||||
|
protected 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(
|
||||||
|
ServerRequestInterface $request
|
||||||
|
): ResponseInterface {
|
||||||
|
return $this->response;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
111
tests/Http/Middleware/ScopeMiddlewareTest.php
Normal file
111
tests/Http/Middleware/ScopeMiddlewareTest.php
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\Http\Middleware;
|
||||||
|
|
||||||
|
use League\Route\Dispatcher;
|
||||||
|
use Nyholm\Psr7\Response;
|
||||||
|
use Nyholm\Psr7\ServerRequest;
|
||||||
|
use Siteworxpro\App\Attributes\Guards\Scope;
|
||||||
|
use Siteworxpro\App\Http\Middleware\ScopeMiddleware;
|
||||||
|
use Siteworxpro\HttpStatus\CodesEnum;
|
||||||
|
|
||||||
|
class ScopeMiddlewareTest extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws \ReflectionException
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testHandlesNoScopes()
|
||||||
|
{
|
||||||
|
$class = new class {
|
||||||
|
public function getCallable(): array
|
||||||
|
{
|
||||||
|
return [ $this, 'index' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Dummy method for testing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$handler
|
||||||
|
->shouldReceive('handle')
|
||||||
|
->once()
|
||||||
|
->andReturn(new Response(200));
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$middleware = new ScopeMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \ReflectionException
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testAllowsWithScope()
|
||||||
|
{
|
||||||
|
$class = new class {
|
||||||
|
public function getCallable(): array
|
||||||
|
{
|
||||||
|
return [ $this, 'index' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Scope(['admin'])]
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Dummy method for testing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$handler
|
||||||
|
->shouldReceive('handle')
|
||||||
|
->once()
|
||||||
|
->andReturn(new Response(200));
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/')->withAttribute('scope', ['admin', 'user']);
|
||||||
|
$middleware = new ScopeMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::OK->value, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \ReflectionException
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testDisallowsWithScope()
|
||||||
|
{
|
||||||
|
$class = new class {
|
||||||
|
public function getCallable(): array
|
||||||
|
{
|
||||||
|
return [ $this, 'index' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Scope(['admin'])]
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Dummy method for testing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$middleware = new ScopeMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::FORBIDDEN->value, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ class LoggerRpcTest extends Unit
|
|||||||
$mock = Mockery::mock(LoggerInterface::class);
|
$mock = Mockery::mock(LoggerInterface::class);
|
||||||
$mock->expects('debug')
|
$mock->expects('debug')
|
||||||
->with('message', ['key' => 'value'])
|
->with('message', ['key' => 'value'])
|
||||||
->once();
|
->times(1);
|
||||||
|
|
||||||
\Siteworxpro\App\Services\Facades\Logger::getFacadeContainer()
|
\Siteworxpro\App\Services\Facades\Logger::getFacadeContainer()
|
||||||
->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) {
|
->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) {
|
||||||
@@ -46,8 +46,6 @@ class LoggerRpcTest extends Unit
|
|||||||
$logger->debug('message', ['key' => 'value']);
|
$logger->debug('message', ['key' => 'value']);
|
||||||
|
|
||||||
$mock->shouldHaveReceived('debug');
|
$mock->shouldHaveReceived('debug');
|
||||||
|
|
||||||
Mockery::close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +74,6 @@ class LoggerRpcTest extends Unit
|
|||||||
$logger->notice('message', ['key' => 'value']);
|
$logger->notice('message', ['key' => 'value']);
|
||||||
|
|
||||||
$mock->shouldHaveReceived('info')->times(2);
|
$mock->shouldHaveReceived('info')->times(2);
|
||||||
Mockery::close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,7 +101,6 @@ class LoggerRpcTest extends Unit
|
|||||||
$logger->warning('message', ['key' => 'value']);
|
$logger->warning('message', ['key' => 'value']);
|
||||||
|
|
||||||
$mock->shouldHaveReceived('warning');
|
$mock->shouldHaveReceived('warning');
|
||||||
Mockery::close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,7 +131,6 @@ class LoggerRpcTest extends Unit
|
|||||||
$logger->emergency('message', ['key' => 'value']);
|
$logger->emergency('message', ['key' => 'value']);
|
||||||
|
|
||||||
$mock->shouldHaveReceived('error')->times(4);
|
$mock->shouldHaveReceived('error')->times(4);
|
||||||
Mockery::close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -162,6 +157,5 @@ class LoggerRpcTest extends Unit
|
|||||||
$logger->log('notaloglevel', 'message', ['key' => 'value']);
|
$logger->log('notaloglevel', 'message', ['key' => 'value']);
|
||||||
|
|
||||||
$mock->shouldHaveReceived('log')->times(1);
|
$mock->shouldHaveReceived('log')->times(1);
|
||||||
Mockery::close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,18 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Siteworxpro\Tests\Log;
|
namespace Siteworxpro\Tests\Log;
|
||||||
|
|
||||||
|
use Psr\Container\ContainerExceptionInterface;
|
||||||
|
use Psr\Container\NotFoundExceptionInterface;
|
||||||
use Psr\Log\LogLevel;
|
use Psr\Log\LogLevel;
|
||||||
use Siteworxpro\App\Log\Logger;
|
use Siteworxpro\App\Log\Logger;
|
||||||
use Siteworxpro\Tests\Unit;
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
class LoggerTest extends Unit
|
class LoggerTest extends Unit
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
private function getLoggerWithBuffer(string $logLevel): array
|
private function getLoggerWithBuffer(string $logLevel): array
|
||||||
{
|
{
|
||||||
$inputBuffer = fopen('php://memory', 'r+');
|
$inputBuffer = fopen('php://memory', 'r+');
|
||||||
@@ -21,6 +27,10 @@ class LoggerTest extends Unit
|
|||||||
return stream_get_contents($inputBuffer, -1, 0);
|
return stream_get_contents($inputBuffer, -1, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
private function testLogLevel(string $level): void
|
private function testLogLevel(string $level): void
|
||||||
{
|
{
|
||||||
[$logger, $inputBuffer] = $this->getLoggerWithBuffer($level);
|
[$logger, $inputBuffer] = $this->getLoggerWithBuffer($level);
|
||||||
@@ -33,6 +43,10 @@ class LoggerTest extends Unit
|
|||||||
$this->assertEquals('value', $decoded['context']['key']);
|
$this->assertEquals('value', $decoded['context']['key']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
private function testLogLevelEmpty(string $configLevel, string $logLevel): void
|
private function testLogLevelEmpty(string $configLevel, string $logLevel): void
|
||||||
{
|
{
|
||||||
[$logger, $inputBuffer] = $this->getLoggerWithBuffer($configLevel);
|
[$logger, $inputBuffer] = $this->getLoggerWithBuffer($configLevel);
|
||||||
@@ -42,57 +56,101 @@ class LoggerTest extends Unit
|
|||||||
$this->assertEmpty($output);
|
$this->assertEmpty($output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
public function testLogsDebugMessageWhenLevelIsDebug(): void
|
public function testLogsDebugMessageWhenLevelIsDebug(): void
|
||||||
{
|
{
|
||||||
$this->testLogLevel(LogLevel::DEBUG);
|
$this->testLogLevel(LogLevel::DEBUG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
public function testLogsInfoMessageWhenLevelIsInfo(): void
|
public function testLogsInfoMessageWhenLevelIsInfo(): void
|
||||||
{
|
{
|
||||||
$this->testLogLevel(LogLevel::INFO);
|
$this->testLogLevel(LogLevel::INFO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
public function testLogsWarningMessageWhenLevelIsWarning(): void
|
public function testLogsWarningMessageWhenLevelIsWarning(): void
|
||||||
{
|
{
|
||||||
$this->testLogLevel(LogLevel::WARNING);
|
$this->testLogLevel(LogLevel::WARNING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
public function testLogsErrorMessageWhenLevelIsError(): void
|
public function testLogsErrorMessageWhenLevelIsError(): void
|
||||||
{
|
{
|
||||||
$this->testLogLevel(LogLevel::ERROR);
|
$this->testLogLevel(LogLevel::ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
public function testLogsCriticalMessageWhenLevelIsCritical(): void
|
public function testLogsCriticalMessageWhenLevelIsCritical(): void
|
||||||
{
|
{
|
||||||
$this->testLogLevel(LogLevel::CRITICAL);
|
$this->testLogLevel(LogLevel::CRITICAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
public function testLogsAlertMessageWhenLevelIsAlert(): void
|
public function testLogsAlertMessageWhenLevelIsAlert(): void
|
||||||
{
|
{
|
||||||
$this->testLogLevel(LogLevel::ALERT);
|
$this->testLogLevel(LogLevel::ALERT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
public function testLogsEmergencyMessageWhenLevelIsEmergency(): void
|
public function testLogsEmergencyMessageWhenLevelIsEmergency(): void
|
||||||
{
|
{
|
||||||
$this->testLogLevel(LogLevel::EMERGENCY);
|
$this->testLogLevel(LogLevel::EMERGENCY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
public function testLogsNoticeMessageWhenLevelIsNotice(): void
|
public function testLogsNoticeMessageWhenLevelIsNotice(): void
|
||||||
{
|
{
|
||||||
$this->testLogLevel(LogLevel::NOTICE);
|
$this->testLogLevel(LogLevel::NOTICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
public function testDoesNotLogWhenMinimumLevelIsInfo(): void
|
public function testDoesNotLogWhenMinimumLevelIsInfo(): void
|
||||||
{
|
{
|
||||||
$this->testLogLevelEmpty(LogLevel::INFO, LogLevel::DEBUG);
|
$this->testLogLevelEmpty(LogLevel::INFO, LogLevel::DEBUG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
public function testDoesNotLogWhenMinimumLevelIsWarning(): void
|
public function testDoesNotLogWhenMinimumLevelIsWarning(): void
|
||||||
{
|
{
|
||||||
$this->testLogLevelEmpty(LogLevel::WARNING, LogLevel::INFO);
|
$this->testLogLevelEmpty(LogLevel::WARNING, LogLevel::INFO);
|
||||||
$this->testLogLevelEmpty(LogLevel::WARNING, LogLevel::DEBUG);
|
$this->testLogLevelEmpty(LogLevel::WARNING, LogLevel::DEBUG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
*/
|
||||||
public function testDoesNotLogWhenMinimumLevelIsError(): void
|
public function testDoesNotLogWhenMinimumLevelIsError(): void
|
||||||
{
|
{
|
||||||
$this->testLogLevelEmpty(LogLevel::ERROR, LogLevel::DEBUG);
|
$this->testLogLevelEmpty(LogLevel::ERROR, LogLevel::DEBUG);
|
||||||
@@ -100,12 +158,20 @@ class LoggerTest extends Unit
|
|||||||
$this->testLogLevelEmpty(LogLevel::ERROR, LogLevel::WARNING);
|
$this->testLogLevelEmpty(LogLevel::ERROR, LogLevel::WARNING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
public function testDoesNotLogWhenMinimumLevelIsNotice(): void
|
public function testDoesNotLogWhenMinimumLevelIsNotice(): void
|
||||||
{
|
{
|
||||||
$this->testLogLevelEmpty(LogLevel::NOTICE, LogLevel::DEBUG);
|
$this->testLogLevelEmpty(LogLevel::NOTICE, LogLevel::DEBUG);
|
||||||
$this->testLogLevelEmpty(LogLevel::NOTICE, LogLevel::INFO);
|
$this->testLogLevelEmpty(LogLevel::NOTICE, LogLevel::INFO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
public function testLogsMessageWithEmptyContext(): void
|
public function testLogsMessageWithEmptyContext(): void
|
||||||
{
|
{
|
||||||
[$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO);
|
[$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO);
|
||||||
@@ -118,6 +184,10 @@ class LoggerTest extends Unit
|
|||||||
$this->assertEquals('Message without context', $decoded['message']);
|
$this->assertEquals('Message without context', $decoded['message']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
public function testLogsMessageWithComplexContext(): void
|
public function testLogsMessageWithComplexContext(): void
|
||||||
{
|
{
|
||||||
[$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO);
|
[$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO);
|
||||||
@@ -135,6 +205,10 @@ class LoggerTest extends Unit
|
|||||||
$this->assertEquals('value', $decoded['context']['nested']['key']);
|
$this->assertEquals('value', $decoded['context']['nested']['key']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ContainerExceptionInterface
|
||||||
|
* @throws NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
public function testLogsStringableMessage(): void
|
public function testLogsStringableMessage(): void
|
||||||
{
|
{
|
||||||
[$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO);
|
[$logger, $buffer] = $this->getLoggerWithBuffer(LogLevel::INFO);
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace Siteworxpro\Tests;
|
namespace Siteworxpro\Tests;
|
||||||
|
|
||||||
use Illuminate\Container\Container;
|
use Illuminate\Container\Container;
|
||||||
|
use Mockery;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Siteworx\Config\Config as SWConfig;
|
use Siteworx\Config\Config as SWConfig;
|
||||||
use Siteworxpro\App\Services\Facade;
|
use Siteworxpro\App\Services\Facade;
|
||||||
@@ -12,13 +13,25 @@ use Siteworxpro\App\Services\Facades\Config;
|
|||||||
|
|
||||||
abstract class Unit extends TestCase
|
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
|
* @throws \ReflectionException
|
||||||
*/
|
*/
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$container = new Container();
|
$container = $this->getContainer();
|
||||||
Facade::setFacadeContainer($container);
|
|
||||||
|
|
||||||
$container->bind(SWConfig::class, function () {
|
$container->bind(SWConfig::class, function () {
|
||||||
return SWConfig::load(__DIR__ . '/../config.php');
|
return SWConfig::load(__DIR__ . '/../config.php');
|
||||||
@@ -29,5 +42,6 @@ abstract class Unit extends TestCase
|
|||||||
{
|
{
|
||||||
Config::clearResolvedInstances();
|
Config::clearResolvedInstances();
|
||||||
Facade::setFacadeContainer(null);
|
Facade::setFacadeContainer(null);
|
||||||
|
Mockery::close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user