9 Commits

Author SHA1 Message Date
e971d32f9d chore: update PHP CLI image to version 8.5.1 in Dockerfile
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 58s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m1s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m14s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 57s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m16s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 43s
2025-12-27 20:07:06 -05:00
2a060fb972 Docker build make target
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 54s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m1s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m15s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m9s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m13s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 48s
2025-12-27 19:56:02 -05:00
b53a95ebcf feat: register CommandBusProvider and simplify Logger facade usage in tests
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m50s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m40s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m55s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 3m9s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m56s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m22s
2025-12-27 19:23:24 -05:00
de0c95db2a feat: enhance service providers with provides method and integrate command bus in handlers
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m40s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m42s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m53s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m55s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m42s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m24s
2025-12-22 13:18:13 -05:00
cae1de6ef3 feat: implement command bus with attribute-based handler resolution and add example command and handler (#27)
All checks were successful
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m13s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m24s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m58s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m20s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m5s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m0s
Reviewed-on: #27
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-12-21 21:04:52 +00:00
84c3b392ba refactor: remove status_code from response classes and update related tests
Some checks failed
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 3m5s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Failing after 2m56s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 3m12s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 3m32s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 3m17s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m42s
2025-12-10 08:45:51 -05:00
f59dcb2dcc feat: update Docker configuration for SSL support and improve service registration (#26)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m34s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m44s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m49s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m41s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m47s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m30s
🏗️✨ Build Workflow / 🖥️ 🔨 Build (push) Successful in 19m32s
🏗️✨ Build Workflow / 🖥️ 🔨 Build Migrations (push) Successful in 1m39s
Reviewed-on: #26
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-12-04 16:49:58 +00:00
8252ae4e53 fix: optimize Dockerfile by removing unnecessary build dependencies and improving healthcheck
All checks were successful
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m43s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m37s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m48s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m42s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m43s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m19s
🏗️✨ Build Workflow / 🖥️ 🔨 Build (push) Successful in 19m58s
🏗️✨ Build Workflow / 🖥️ 🔨 Build Migrations (push) Successful in 1m53s
2025-12-04 10:05:51 -05:00
68ab2dcdd7 feat/grpc (#25)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m30s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m43s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m49s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m39s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m48s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m21s
Reviewed-on: #25
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-12-04 13:55:28 +00:00
47 changed files with 841 additions and 158 deletions

View File

@@ -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,7 +42,7 @@ 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:
@@ -53,8 +64,8 @@ services:
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;'"
@@ -79,12 +90,21 @@ 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: "0"
context: . UID: 0
USER: root
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:
@@ -103,6 +123,7 @@ services:
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

83
.dev/ssl/localhost.crt Normal file
View 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
View 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
View 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

View File

@@ -11,12 +11,9 @@ grpc:
pool: pool:
command: "php grpc-worker.php" command: "php grpc-worker.php"
num_workers: ${GRPC_WORKERS:-4} num_workers: ${GRPC_WORKERS:-4}
allocate_timeout: 5s debug: ${DEBUG:-false}
reset_timeout: 5s
destroy_timeout: 5s
stream_timeout: 5s
reflection: ${GRPC_REFLECTION:-true} reflection: ${GRPC_REFLECTION:-true}
health_check: ${GRPC_HEALTH_CHECK:-true} destroy_timeout: 5s
proto: proto:
- "protos/example.proto" - "protos/example.proto"

View File

@@ -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" />

View File

@@ -1,5 +1,5 @@
# Use the RoadRunner image as a base for the first stage # Use the RoadRunner image as a base for the first stage
FROM ghcr.io/roadrunner-server/roadrunner:2025.1.4 AS roadrunner FROM ghcr.io/roadrunner-server/roadrunner:2025.1.6 AS roadrunner
# Use the official Composer image as the base for the library stage # Use the official Composer image as the base for the library stage
FROM siteworxpro/composer AS library FROM siteworxpro/composer AS library
@@ -12,9 +12,11 @@ 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 siteworxpro/php:8.5.0-cli-alpine AS php FROM siteworxpro/php:8.5.1-cli-alpine AS php
ARG KAFKA_ENABLED=0 ARG KAFKA_ENABLED=0
ARG USER=appuser
ARG UID=1000
# Move the production PHP configuration file to the default location # Move the production PHP configuration file to the default location
RUN mv /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini \ RUN mv /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini \
@@ -26,6 +28,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 ; \
@@ -43,11 +46,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 generated generated/ ADD generated generated/
ADD server.php . ADD protos protos/
ADD .rr.yaml . ADD server.php cli.php grpc-worker.php .rr.yaml config.php ./
ADD config.php .
EXPOSE 9501 EXPOSE 9501 9001
# Create a non-root user and set ownership of the /app directory
RUN if [ ! $UID -eq 0 ] ; then addgroup -g $UID $USER && adduser -D -u $UID -G $USER $USER && chown -R $USER:$USER /app ; fi
USER $USER
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
View File

@@ -2,55 +2,132 @@
![pipeline status](https://gitea.siteworxpro.com/siteworxpro/Php-Template/actions/workflows/tests.yml/badge.svg?branch=master&style=flat-square) ![pipeline status](https://gitea.siteworxpro.com/siteworxpro/Php-Template/actions/workflows/tests.yml/badge.svg?branch=master&style=flat-square)
## 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

View File

@@ -27,7 +27,8 @@
"react/async": "^4", "react/async": "^4",
"guzzlehttp/guzzle": "^7.10", "guzzlehttp/guzzle": "^7.10",
"zircote/swagger-php": "^5.7", "zircote/swagger-php": "^5.7",
"spiral/roadrunner-grpc": "^3.5" "spiral/roadrunner-grpc": "^3.5",
"league/tactician": "^1.1"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^12.4", "phpunit/phpunit": "^12.4",

57
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "977f74570c671e4d59fd70d5e732c3d2", "content-hash": "d027bee8e875c5542f7ff9612bfac4e2",
"packages": [ "packages": [
{ {
"name": "adhocore/cli", "name": "adhocore/cli",
@@ -1360,6 +1360,61 @@
], ],
"time": "2024-11-25T08:10:15+00:00" "time": "2024-11-25T08:10:15+00:00"
}, },
{
"name": "league/tactician",
"version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/tactician.git",
"reference": "e79f763170f3d5922ec29e85cffca0bac5cd8975"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/tactician/zipball/e79f763170f3d5922ec29e85cffca0bac5cd8975",
"reference": "e79f763170f3d5922ec29e85cffca0bac5cd8975",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"mockery/mockery": "^1.3",
"phpunit/phpunit": "^7.5.20 || ^9.3.8",
"squizlabs/php_codesniffer": "^3.5.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"League\\Tactician\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ross Tuck",
"homepage": "http://tactician.thephpleague.com"
}
],
"description": "A small, flexible command bus. Handy for building service layers.",
"keywords": [
"command",
"command bus",
"service layer"
],
"support": {
"issues": "https://github.com/thephpleague/tactician/issues",
"source": "https://github.com/thephpleague/tactician/tree/v1.1.0"
},
"time": "2021-02-14T15:29:04+00:00"
},
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
"version": "3.9.0", "version": "3.9.0",

View File

@@ -14,7 +14,6 @@ return [
*/ */
'server' => [ 'server' => [
'port' => Env::get('HTTP_PORT', 9501, 'int'), 'port' => Env::get('HTTP_PORT', 9501, 'int'),
'dev_mode' => Env::get('DEV_MODE', false, 'bool'),
], ],
/** /**

View File

@@ -6,7 +6,7 @@ require __DIR__ . '/vendor/autoload.php';
try { try {
$server = new Grpc(); $server = new Grpc();
$server->start(); exit($server->start());
} catch (\Exception $e) { } catch (\Exception $e) {
echo $e->getMessage(); echo $e->getMessage();

View File

@@ -2,8 +2,11 @@
SHELL := /bin/sh SHELL := /bin/sh
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
# Docker Compose file
COMPOSE_FILE := -f .dev/docker-compose.yml
# Reusable vars # Reusable vars
DOCKER := docker compose DOCKER := docker compose $(COMPOSE_FILE)
COMPOSER_RUNTIME := composer-runtime COMPOSER_RUNTIME := composer-runtime
DEV_RUNTIME := dev-runtime DEV_RUNTIME := dev-runtime
MIGRATION_CONTAINER := migration-container MIGRATION_CONTAINER := migration-container
@@ -12,24 +15,56 @@ PROTOC_GEN_DIR := ./protoc-gen-php-grpc-2025.1.5-darwin-arm64
PROTOC_GEN := $(PROTOC_GEN_DIR)/protoc-gen-php-grpc 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 PROTOC_URL := https://github.com/roadrunner-server/roadrunner/releases/download/v2025.1.5/protoc-gen-php-grpc-2025.1.5-darwin-arm64.tar.gz
COMPOSER := $(DOCKER) exec $(COMPOSER_RUNTIME) sh -c
DEV := $(DOCKER) exec $(DEV_RUNTIME) sh -c DEV := $(DOCKER) exec $(DEV_RUNTIME) sh -c
COMPOSER := $(DOCKER) exec $(COMPOSER_RUNTIME) sh -c
# Colors # Colors
GREEN := \033[32m GREEN := \033[32m
YELLOW := \033[33m YELLOW := \033[33m
RESET := \033[0m RESET := \033[0m
# Fancy emoji
SPARK :=
ROCKET := 🚀
WARN := ⚠️
MAGNIFY := 🔍
BUG := 🐞
COMPOSE := 🐳
TRASH := 🧹
PROTO := 🧩
CHECK :=
CROSS :=
# Align width for help display # Align width for help display
HELP_COL_WIDTH := 26 HELP_COL_WIDTH := 26
# Help: auto-generate from targets with "##" comments # Help: auto-generate from targets with "##" comments
help: ## Show this help help: ## Show this help
@echo "Available commands:" @echo "$(SPARK) Available commands:"
@awk -F':|##' '/^[a-zA-Z0-9._-]+:.*##/ {printf " %-$(HELP_COL_WIDTH)s - %s\n", $$1, $$3}' $(MAKEFILE_LIST) | sort @awk -F':|##' '/^[a-zA-Z0-9._-]+:.*##/ {printf " %-$(HELP_COL_WIDTH)s - %s\n", $$1, $$3}' $(MAKEFILE_LIST) | sort
docker-build: ## Build Docker image
@push_flag=""; \
if [ -n "$(push)" ]; then \
push="--push"; \
fi
@if [ -z "$(image)" ]; then \
echo "image variable is required: make docker-build image=your-image-name tag=your"; \
exit 1; \
fi
@if [ -z "$(tag)" ]; then \
echo "tag variable is required: make docker-build image=your-image-name tag=your"; \
exit 1; \
fi
@platform_flag=""; \
if [ -n "$(platform)" ]; then \
platform_flag="--platform=$(platform)"; \
fi; \
printf "$(YELLOW)$(SPARK) Building Docker image: $(image):$(tag)$(RESET)\n"; \
docker buildx build $$platform_flag --provenance=true --sbom=true $$push_flag --tag $(image):$(tag) .
start: ## Start the development runtime container start: ## Start the development runtime container
@printf "$(GREEN)Starting $(DEV_RUNTIME)$(RESET)\n" @printf "$(GREEN)$(ROCKET) Starting $(DEV_RUNTIME)$(RESET)\n"
$(DOCKER) up $(DEV_RUNTIME) -d --no-recreate $(DOCKER) up $(DEV_RUNTIME) -d --no-recreate
sh: ## Open a shell in the development runtime container sh: ## Open a shell in the development runtime container
@@ -41,7 +76,7 @@ run: ## Run the application server in the development runtime container
$(DEV) "rr serve" $(DEV) "rr serve"
stop: ## Stop and remove the development runtime container stop: ## Stop and remove the development runtime container
@printf "$(YELLOW)Stopping all containers$(RESET)\n" @printf "$(YELLOW)$(WARN) Stopping all containers$(RESET)\n"
$(DOCKER) down $(DOCKER) down
restart: ## Restart dev container (stop + start) restart: ## Restart dev container (stop + start)
@@ -49,9 +84,11 @@ restart: ## Restart dev container (stop + start)
@$(MAKE) start @$(MAKE) start
rebuild: ## Rebuild containers (useful after Dockerfile changes) rebuild: ## Rebuild containers (useful after Dockerfile changes)
@printf "$(YELLOW)Rebuilding containers$(RESET)\n" @printf "$(YELLOW)$(SPARK) Rebuilding containers$(RESET)\n"
$(DOCKER) build @$(MAKE) stop
$(DOCKER) up --force-recreate --build -d @printf "$(YELLOW)$(TRASH) Deleting all Docker resources$(RESET)\n"
docker system prune --all --volumes --force
@$(MAKE) start
ps: ## Show docker compose ps ps: ## Show docker compose ps
$(DOCKER) ps $(DOCKER) ps
@@ -61,60 +98,90 @@ migrate: ## Run database migrations in the migration container
# Composer helpers # Composer helpers
composer-install: ## Install PHP dependencies in the composer runtime container 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) "composer install --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-reqs"
composer-install-no-dev: ## Install PHP dependencies without dev packages in the composer runtime container
@printf "$(COMPOSE) $(GREEN)Installing PHP dependencies (no-dev) in $(COMPOSER_RUNTIME)$(RESET)\n"
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate
$(COMPOSER) "composer install --no-dev --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) composer-require: ## Require a PHP package in the composer runtime container (usage: make composer-require package=vendor/package)
ifndef package ifndef package
$(error package variable is required: make composer-require package=vendor/package) $(error package variable is required: make composer-require package=vendor/package)
endif 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) "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) composer-require-dev: ## Require a PHP package as dev in the composer runtime container (usage: make composer-require-dev package=vendor/package)
ifndef package ifndef package
$(error package variable is required: make composer-require-dev package=vendor/package) $(error package variable is required: make composer-require-dev package=vendor/package)
endif 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) "composer require --dev $(package) --ignore-platform-reqs"
composer-update: ## Update PHP dependencies in the composer runtime container 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" $(COMPOSER) "composer update --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-reqs"
enable-debug: ## Enable Xdebug in the development runtime container enable-debug: ## Enable Xdebug in the development runtime container
@$(MAKE) start @$(DOCKER) up $(DEV_RUNTIME) -d --no-recreate
@printf "$(GREEN)$(BUG) Enabling Xdebug in $(DEV_RUNTIME)$(RESET)\n"
$(DEV) "bin/xdebug.sh" $(DEV) "bin/xdebug.sh"
enable-coverage: ## Enable PCOV code coverage in the composer runtime container enable-coverage: ## Enable PCOV code coverage in the composer runtime container
@$(MAKE) start @printf "$(GREEN)$(MAGNIFY) Enabling PCOV in $(COMPOSER_RUNTIME)$(RESET)\n"
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate
$(COMPOSER) "bin/pcov.sh" $(COMPOSER) "bin/pcov.sh"
protoc: ## Generate PHP gRPC code from .proto files protoc: ## Generate PHP gRPC code from .proto files
@printf "$(GREEN)Setting up protoc-gen-php-grpc plugin$(RESET)\n" @printf "$(PROTO) $(GREEN)Setting up protoc-gen-php-grpc plugin$(RESET)\n"
@curl -LOs $(PROTOC_URL) @curl -LOs $(PROTOC_URL)
@tar -xzf protoc-gen-php-grpc-2025.1.5-darwin-arm64.tar.gz @tar -xzf protoc-gen-php-grpc-2025.1.5-darwin-arm64.tar.gz
@printf "$(GREEN)Generating PHP gRPC code from .proto files$(RESET)\n" @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 \ @protoc --plugin=./protoc-gen-php-grpc-2025.1.5-darwin-arm64/protoc-gen-php-grpc \
--php_out=./generated \ --php_out=./generated \
--php-grpc_out=./generated \ --php-grpc_out=./generated \
protos/example.proto protos/example.proto
@printf "$(GREEN)Cleaning up protoc-gen-php-grpc plugin files$(RESET)\n" @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 @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 # Developer tasks
lint: ## Run linting (phpcs/phpstan) in composer runtime lint: ## Run linting (phpcs/phpstan) in composer runtime
@printf "$(MAGNIFY) $(GREEN)Running linters$(RESET)\n"
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate @$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate
$(COMPOSER) "composer run-script tests:lint || true" $(COMPOSER) "composer run-script tests:lint || true"
$(COMPOSER) "composer run-script tests:phpstan || true" $(COMPOSER) "composer run-script tests:phpstan || true"
fmt: ## Format code (php-cs-fixer) fmt: ## Format code (php-cs-fixer)
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate @printf "$(MAGNIFY) $(GREEN)Formatting code$(RESET)\n"
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate
$(COMPOSER) "composer run-script tests:lint:fix" $(COMPOSER) "composer run-script tests:lint:fix"
test: ## Run test suite (phpunit) 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" $(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 # Convenience aliases
dev: run ## Alias for start dev: run ## Alias for start
ci: composer-install test ## CI-like local flow ci: composer-install migrate license-check lint test ## CI-like local flow
down: stop ## Alias for stop down: stop ## Alias for stop
up: start ## Alias for start up: start ## Alias for start
.PHONY: help start sh run stop restart rebuild ps logs migrate composer-install composer-require composer-require-dev composer-update enable-debug enable-coverage protoc lint fmt test check-lock dev ci .PHONY: help start sh run stop docker-build 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

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Attributes\CommandBus;
use Attribute;
/**
* Class HandlesCommand
* @package Siteworxpro\App\Attributes\CommandBus
*/
#[Attribute(Attribute::TARGET_CLASS)]
readonly class HandlesCommand
{
/**
* @param class-string $commandClass
*/
public function __construct(public string $commandClass)
{
}
}

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace Siteworxpro\App\Cli\Commands; namespace Siteworxpro\App\Cli\Commands;
use Ahc\Cli\Input\Command; use Ahc\Cli\Input\Command;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\Services\Facades\CommandBus;
class DemoCommand extends Command implements CommandInterface class DemoCommand extends Command implements CommandInterface
{ {
@@ -28,18 +30,14 @@ class DemoCommand extends Command implements CommandInterface
$pb->finish(); $pb->finish();
$this->writer()->boldBlue("Demo Command Executed!\n"); $this->writer()->boldBlue("Demo Command Executed!\n");
$name = $this->values()['name'];
if ($this->values()['name']) { $greet = $this->values()['greet'] ?? false;
$name = $this->values()['name'];
$greet = $this->values()['greet'] ?? false;
} else {
return 0;
}
if ($greet) { if ($greet) {
$this->writer()->green("Hello, $name! Welcome to the CLI demo.\n"); $this->writer()->green("Hello, $name! Welcome to the CLI demo.\n");
} else { } else {
$this->writer()->yellow("Name provided: {$name}\n"); $exampleCommand = new ExampleCommand($name);
$this->writer()->yellow(CommandBus::handle($exampleCommand));
} }
return 0; return 0;

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\CommandBus;
use League\Tactician\Exception\CanNotInvokeHandlerException;
use League\Tactician\Handler\Locator\HandlerLocator;
use Siteworxpro\App\Attributes\CommandBus\HandlesCommand;
class AttributeLocator implements HandlerLocator
{
private const string HANDLER_NAMESPACE = 'Siteworxpro\\App\\CommandBus\\Handlers\\';
private array $handlers;
public function __construct()
{
$directory = __DIR__ . '/Handlers';
$files = scandir($directory);
foreach ($files as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
$className = pathinfo($file, PATHINFO_FILENAME);
$fullClassName = self::HANDLER_NAMESPACE . $className;
if (class_exists($fullClassName)) {
$reflectionClass = new \ReflectionClass($fullClassName);
$attributes = $reflectionClass->getAttributes(HandlesCommand::class);
foreach ($attributes as $attribute) {
$instance = $attribute->newInstance();
$commandClass = $instance->commandClass;
$this->handlers[$commandClass] = $fullClassName;
}
}
}
}
}
public function getHandlerForCommand($commandName)
{
if (isset($this->handlers[$commandName])) {
$handlerClass = $this->handlers[$commandName];
return new $handlerClass();
}
throw new CanNotInvokeHandlerException("No handler found for command: " . $commandName);
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\CommandBus\Commands;
readonly abstract class Command
{
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\CommandBus\Commands;
readonly class ExampleCommand extends Command
{
public function __construct(
private string $name
) {
}
public function getName(): string
{
return $this->name;
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\CommandBus\Handlers;
abstract class CommandHandler implements CommandHandlerInterface
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\CommandBus\Handlers;
use Siteworxpro\App\CommandBus\Commands\Command;
interface CommandHandlerInterface
{
public function __invoke(Command $command): mixed;
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\CommandBus\Handlers;
use Siteworxpro\App\Attributes\CommandBus\HandlesCommand;
use Siteworxpro\App\CommandBus\Commands\Command;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\Services\Facades\Logger;
#[HandlesCommand(ExampleCommand::class)]
class ExampleHandler extends CommandHandler
{
/**
* @param Command|ExampleCommand $command
* @return string
*/
public function __invoke(Command|ExampleCommand $command): string
{
if (!method_exists($command, 'getName')) {
throw new \TypeError('Invalid command type provided to ExampleHandler.');
}
$name = $command->getName();
Logger::info('Handling ExampleCommand for name: ' . $name);
return 'Hello, ' . $name . '!';
}
}

View File

@@ -9,7 +9,9 @@ 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\Http\Responses\GenericResponse;
use Siteworxpro\App\Http\Responses\ServerErrorResponse;
use Siteworxpro\App\Models\Model; use Siteworxpro\App\Models\Model;
use Siteworxpro\App\Services\Facades\Logger;
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; use OpenApi\Attributes as OA;
@@ -43,18 +45,19 @@ class HealthcheckController extends Controller
throw new \Exception('Redis ping failed'); throw new \Exception('Redis ping failed');
} }
} catch (\Exception $e) { } catch (\Exception $e) {
Logger::emergency(
'Healthcheck failed: ' . $e->getMessage(),
['exception' => $e]
);
return JsonResponseFactory::createJsonResponse( return JsonResponseFactory::createJsonResponse(
[ new ServerErrorResponse($e),
'status_code' => CodesEnum::SERVICE_UNAVAILABLE->value,
'message' => 'Healthcheck Failed',
'error' => $e->getMessage(),
],
CodesEnum::SERVICE_UNAVAILABLE CodesEnum::SERVICE_UNAVAILABLE
); );
} }
return JsonResponseFactory::createJsonResponse( return JsonResponseFactory::createJsonResponse(
new GenericResponse('Healthcheck OK', CodesEnum::OK->value) new GenericResponse('Healthcheck OK')
); );
} }
} }

View File

@@ -7,11 +7,13 @@ 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\Attributes\Guards; use Siteworxpro\App\Attributes\Guards;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\Docs\TokenSecurity; use Siteworxpro\App\Docs\TokenSecurity;
use Siteworxpro\App\Docs\UnauthorizedResponse; use Siteworxpro\App\Docs\UnauthorizedResponse;
use Siteworxpro\App\Http\JsonResponseFactory; use Siteworxpro\App\Http\JsonResponseFactory;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Siteworxpro\App\Http\Responses\GenericResponse; use Siteworxpro\App\Http\Responses\GenericResponse;
use Siteworxpro\App\Services\Facades\CommandBus;
/** /**
* Class IndexController * Class IndexController
@@ -37,7 +39,10 @@ class IndexController extends Controller
#[UnauthorizedResponse] #[UnauthorizedResponse]
public function get(ServerRequest $request): ResponseInterface public function get(ServerRequest $request): ResponseInterface
{ {
return JsonResponseFactory::createJsonResponse(new GenericResponse('Server is running')); $command = new ExampleCommand($request->getQueryParams()['name'] ?? 'Guest');
$greeting = CommandBus::handle($command);
return JsonResponseFactory::createJsonResponse(new GenericResponse('Server is running. ' . $greeting));
} }
/** /**

View File

@@ -16,7 +16,6 @@ class UnauthorizedResponse extends OA\Response
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
properties: [ properties: [
new OA\Property(property: 'status_code', type: 'integer', example: 401),
new OA\Property(property: 'message', type: 'string', example: 'Unauthorized'), new OA\Property(property: 'message', type: 'string', example: 'Unauthorized'),
] ]
) )

View File

@@ -20,6 +20,10 @@ use Spiral\RoadRunner\Worker;
*/ */
class Grpc class Grpc
{ {
private const array SERVICES = [
GreeterInterface::class => GreeterHandler::class,
];
/** /**
* @throws \ReflectionException * @throws \ReflectionException
*/ */
@@ -36,10 +40,13 @@ class Grpc
public function start(): int public function start(): int
{ {
$server = new Server(new Invoker(), [ $server = new Server(new Invoker(), [
'debug' => (bool) Config::get('app.debug'), 'debug' => Config::get('app.dev_mode'),
]); ]);
$server->registerService(GreeterInterface::class, new GreeterHandler()); foreach (self::SERVICES as $interface => $handler) {
$server->registerService($interface, new $handler());
}
$server->serve(Worker::create()); $server->serve(Worker::create());
return 0; return 0;

View File

@@ -7,14 +7,18 @@ namespace Siteworxpro\App\GrpcHandlers;
use GRPC\Greeter\GreeterInterface; use GRPC\Greeter\GreeterInterface;
use GRPC\Greeter\HelloReply; use GRPC\Greeter\HelloReply;
use GRPC\Greeter\HelloRequest; use GRPC\Greeter\HelloRequest;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\Services\Facades\CommandBus;
use Spiral\RoadRunner\GRPC; use Spiral\RoadRunner\GRPC;
class GreeterHandler implements GreeterInterface class GreeterHandler implements GreeterInterface
{ {
public function SayHello(GRPC\ContextInterface $ctx, HelloRequest $in): HelloReply // phpcs:ignore public function SayHello(GRPC\ContextInterface $ctx, HelloRequest $in): HelloReply // phpcs:ignore
{ {
$command = new ExampleCommand($in->getName());
$reply = new HelloReply(); $reply = new HelloReply();
$reply->setMessage('Hello ' . $in->getName()); $reply->setMessage(CommandBus::handle($command));
return $reply; return $reply;
} }

View File

@@ -11,14 +11,12 @@ use OpenApi\Attributes as OA;
schema: 'GenericResponse', schema: 'GenericResponse',
properties: [ properties: [
new OA\Property(property: 'message', type: 'string', example: 'Operation completed successfully.'), 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 readonly class GenericResponse implements Arrayable
{ {
public function __construct( public function __construct(
private string $message = '', private string $message = '',
private int $statusCode = 200
) { ) {
} }
@@ -26,7 +24,6 @@ readonly class GenericResponse implements Arrayable
{ {
return [ return [
'message' => $this->message, 'message' => $this->message,
'status_code' => $this->statusCode,
]; ];
} }
} }

View File

@@ -14,7 +14,6 @@ use OpenApi\Attributes as OA;
type: 'string', type: 'string',
example: 'The requested resource /api/resource was not found.' example: 'The requested resource /api/resource was not found.'
), ),
new OA\Property(property: 'status_code', type: 'integer', example: 404),
new OA\Property( new OA\Property(
property: 'context', property: 'context',
description: 'Additional context about the not found error.', description: 'Additional context about the not found error.',
@@ -32,7 +31,6 @@ readonly class NotFoundResponse implements Arrayable
public function toArray(): array public function toArray(): array
{ {
return [ return [
'status_code' => CodesEnum::NOT_FOUND->value,
'message' => 'The requested resource ' . $this->uri . ' was not found.', 'message' => 'The requested resource ' . $this->uri . ' was not found.',
'context' => $this->context, 'context' => $this->context,
]; ];

View File

@@ -11,7 +11,7 @@ use OpenApi\Attributes as OA;
schema: 'ServerErrorResponse', schema: 'ServerErrorResponse',
properties: array( properties: array(
new OA\Property(property: 'message', type: 'string', example: 'An internal server error occurred.'), 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: 'code', type: 'integer', example: 500),
new OA\Property( new OA\Property(
property: 'file', property: 'file',
type: 'string', type: 'string',
@@ -35,7 +35,7 @@ readonly class ServerErrorResponse implements Arrayable
{ {
if (Config::get('app.dev_mode')) { if (Config::get('app.dev_mode')) {
return [ return [
'status_code' => $this->e->getCode() != 0 ? 'code' => $this->e->getCode() != 0 ?
$this->e->getCode() : $this->e->getCode() :
CodesEnum::INTERNAL_SERVER_ERROR->value, CodesEnum::INTERNAL_SERVER_ERROR->value,
'message' => $this->e->getMessage(), 'message' => $this->e->getMessage(),
@@ -47,7 +47,7 @@ readonly class ServerErrorResponse implements Arrayable
} }
return [ return [
'status_code' => $this->e->getCode() != 0 ? 'code' => $this->e->getCode() != 0 ?
$this->e->getCode() : $this->e->getCode() :
CodesEnum::INTERNAL_SERVER_ERROR->value, CodesEnum::INTERNAL_SERVER_ERROR->value,
'message' => 'An internal server error occurred.', 'message' => 'An internal server error occurred.',

View File

@@ -10,6 +10,7 @@ use Siteworxpro\App\Services\Facade;
use Siteworxpro\App\Services\Facades\Config; use Siteworxpro\App\Services\Facades\Config;
use Siteworxpro\App\Services\Facades\Dispatcher; use Siteworxpro\App\Services\Facades\Dispatcher;
use Siteworxpro\App\Services\ServiceProviders\BrokerServiceProvider; use Siteworxpro\App\Services\ServiceProviders\BrokerServiceProvider;
use Siteworxpro\App\Services\ServiceProviders\CommandBusProvider;
use Siteworxpro\App\Services\ServiceProviders\DispatcherServiceProvider; use Siteworxpro\App\Services\ServiceProviders\DispatcherServiceProvider;
use Siteworxpro\App\Services\ServiceProviders\LoggerServiceProvider; use Siteworxpro\App\Services\ServiceProviders\LoggerServiceProvider;
use Siteworxpro\App\Services\ServiceProviders\RedisServiceProvider; use Siteworxpro\App\Services\ServiceProviders\RedisServiceProvider;
@@ -33,7 +34,8 @@ class Kernel
LoggerServiceProvider::class, LoggerServiceProvider::class,
RedisServiceProvider::class, RedisServiceProvider::class,
DispatcherServiceProvider::class, DispatcherServiceProvider::class,
BrokerServiceProvider::class BrokerServiceProvider::class,
CommandBusProvider::class,
]; ];
/** /**

View File

@@ -55,24 +55,6 @@ class Facade
}); });
} }
/**
* Convert the facade into a Mockery spy.
*
* @return HigherOrderTapProxy | MockInterface
*/
public static function spy(): HigherOrderTapProxy | MockInterface
{
if (! static::isMock()) {
$class = static::getMockableClass();
return tap($class ? Mockery::spy($class) : Mockery::spy(), function ($spy) {
static::swap($spy);
});
}
throw new \RuntimeException('Cannot spy on an existing mock instance.');
}
/** /**
* Initiate a partial mock on the facade. * Initiate a partial mock on the facade.
* *
@@ -189,19 +171,6 @@ class Facade
} }
} }
/**
* Determines whether a "fake" has been set as the facade instance.
*
* @return bool
*/
public static function isFake(): bool
{
$name = static::getFacadeAccessor();
return isset(static::$resolvedInstance[$name]) &&
static::$resolvedInstance[$name] instanceof Fake;
}
/** /**
* Get the root object behind the facade. * Get the root object behind the facade.
* *

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Services\Facades;
use Siteworxpro\App\CommandBus\Commands\Command;
use Siteworxpro\App\Services\Facade;
/**
* Broker Facade
*
* @package Siteworxpro\App\Services\Facades
* @method static mixed handle(Command $command)
*/
class CommandBus extends Facade
{
/**
* Get the registered name of the component.
*
* @return string The name of the component.
*/
protected static function getFacadeAccessor(): string
{
return \League\Tactician\CommandBus::class;
}
}

View File

@@ -6,6 +6,10 @@ namespace Siteworxpro\App\Services\ServiceProviders;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Siteworxpro\App\Async\Brokers\Broker; use Siteworxpro\App\Async\Brokers\Broker;
use Siteworxpro\App\Async\Brokers\Kafka;
use Siteworxpro\App\Async\Brokers\RabbitMQ;
use Siteworxpro\App\Async\Brokers\Redis;
use Siteworxpro\App\Async\Brokers\Sqs;
use Siteworxpro\App\Services\Facades\Config; use Siteworxpro\App\Services\Facades\Config;
/** /**
@@ -18,6 +22,11 @@ use Siteworxpro\App\Services\Facades\Config;
*/ */
class BrokerServiceProvider extends ServiceProvider class BrokerServiceProvider extends ServiceProvider
{ {
public function provides(): array
{
return [Kafka::class, RabbitMQ::class, Redis::class, Sqs::class];
}
/** /**
* Register services. * Register services.
* *

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Services\ServiceProviders;
use Illuminate\Support\ServiceProvider;
use League\Tactician\CommandBus;
use League\Tactician\Handler\CommandHandlerMiddleware;
use League\Tactician\Handler\CommandNameExtractor\ClassNameExtractor;
use League\Tactician\Handler\MethodNameInflector\InvokeInflector;
use Siteworxpro\App\CommandBus\AttributeLocator;
/**
* Class CommandBusProvider
*
* @package Siteworxpro\App\Services\ServiceProviders
*/
class CommandBusProvider extends ServiceProvider
{
public function provides(): array
{
return [CommandBus::class];
}
public function register(): void
{
$this->app->singleton(CommandBus::class, function () {
return new CommandBus([
new CommandHandlerMiddleware(
new ClassNameExtractor(),
new AttributeLocator(),
new InvokeInflector()
),
]);
});
}
}

View File

@@ -14,6 +14,11 @@ use Siteworxpro\App\Events\Dispatcher;
*/ */
class DispatcherServiceProvider extends ServiceProvider class DispatcherServiceProvider extends ServiceProvider
{ {
public function provides(): array
{
return [Dispatcher::class];
}
public function register(): void public function register(): void
{ {
$this->app->singleton(Dispatcher::class, function () { $this->app->singleton(Dispatcher::class, function () {

View File

@@ -15,6 +15,11 @@ use Siteworxpro\App\Services\Facades\Config;
*/ */
class LoggerServiceProvider extends ServiceProvider class LoggerServiceProvider extends ServiceProvider
{ {
public function provides(): array
{
return [Logger::class];
}
public function register(): void public function register(): void
{ {
$this->app->singleton(Logger::class, function () { $this->app->singleton(Logger::class, function () {

View File

@@ -17,6 +17,11 @@ use Siteworxpro\App\Services\Facades\Config;
*/ */
class RedisServiceProvider extends ServiceProvider class RedisServiceProvider extends ServiceProvider
{ {
public function provides(): array
{
return [Client::class];
}
public function register(): void public function register(): void
{ {
$this->app->singleton(Client::class, function () { $this->app->singleton(Client::class, function () {

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\Tests\CommandBus;
use League\Tactician\Exception\CanNotInvokeHandlerException;
use Siteworxpro\App\CommandBus\AttributeLocator;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\CommandBus\Handlers\ExampleHandler;
use Siteworxpro\Tests\Unit;
class AttributeLocatorTest extends Unit
{
private const array HANDLERS = [
ExampleCommand::class => ExampleHandler::class,
];
public function testResolvesFiles(): void
{
$attributeLocator = new AttributeLocator();
foreach (self::HANDLERS as $command => $handler) {
$class = $attributeLocator->getHandlerForCommand($command);
$this->assertInstanceOf($handler, $class);
}
}
public function testThrowsOnCannotResolve(): void
{
$attributeLocator = new AttributeLocator();
$this->expectException(CanNotInvokeHandlerException::class);
$attributeLocator->getHandlerForCommand('NonExistentCommand');
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Siteworxpro\Tests\CommandBus\Handlers;
use Siteworxpro\App\CommandBus\Commands\Command;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\CommandBus\Handlers\ExampleHandler;
use Siteworxpro\Tests\Unit;
class ExampleHandlerTest extends Unit
{
public function testExampleCommand(): void
{
$command = new ExampleCommand('test payload');
$this->assertEquals('test payload', $command->getName());
$handler = new ExampleHandler();
$result = $handler($command);
$this->assertEquals('Hello, test payload!', $result);
}
public function testThrowsException(): void
{
$class = new readonly class extends Command
{
};
$this->expectException(\TypeError::class);
$handler = new ExampleHandler();
$handler($class);
}
}

View File

@@ -4,23 +4,31 @@ declare(strict_types=1);
namespace Siteworxpro\Tests\Controllers; namespace Siteworxpro\Tests\Controllers;
use League\Tactician\CommandBus;
use Siteworxpro\App\Controllers\IndexController; use Siteworxpro\App\Controllers\IndexController;
class IndexControllerTest extends AbstractController class IndexControllerTest extends AbstractController
{ {
/** /**
* @throws \JsonException * @throws \JsonException|\ReflectionException
*/ */
public function testGet(): void public function testGet(): void
{ {
$this->assertTrue(true); $this->assertTrue(true);
$this->getContainer()->bind(CommandBus::class, function () {
return \Mockery::mock(CommandBus::class)
->shouldReceive('handle')
->andReturn('Hello World')
->getMock();
});
$controller = new IndexController(); $controller = new IndexController();
$response = $controller->get($this->getMockRequest()); $response = $controller->get($this->getMockRequest());
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('{"message":"Server is running","status_code":200}', (string)$response->getBody()); $this->assertEquals('{"message":"Server is running. Hello World"}', (string)$response->getBody());
} }
/** /**
@@ -35,6 +43,6 @@ class IndexControllerTest extends AbstractController
$response = $controller->post($this->getMockRequest()); $response = $controller->post($this->getMockRequest());
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('{"message":"POST request received","status_code":200}', (string)$response->getBody()); $this->assertEquals('{"message":"POST request received"}', (string)$response->getBody());
} }
} }

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\Tests\GrpcHandlers;
use GRPC\Greeter\HelloRequest;
use League\Tactician\CommandBus;
use Siteworxpro\App\GrpcHandlers\GreeterHandler;
use Siteworxpro\Tests\Unit;
use Spiral\RoadRunner\GRPC\ContextInterface;
class GreeterHandlerTest extends Unit
{
/**
* @throws \ReflectionException
*/
public function testSayHello(): void
{
$this->getContainer()->bind(CommandBus::class, function () {
return \Mockery::mock(CommandBus::class)
->shouldReceive('handle')
->andReturn('Hello World')
->getMock();
});
$request = new HelloRequest();
$request->setName('World');
$context = \Mockery::mock(ContextInterface::class);
$handler = new GreeterHandler();
$response = $handler->SayHello($context, $request);
$this->assertEquals('Hello World', $response->getMessage());
}
}

View File

@@ -12,7 +12,6 @@ class NotFoundResponseTest extends Unit
$response = new NotFoundResponse('/api/resource', ['key' => 'value']); $response = new NotFoundResponse('/api/resource', ['key' => 'value']);
$expected = [ $expected = [
'status_code' => 404,
'message' => 'The requested resource /api/resource was not found.', 'message' => 'The requested resource /api/resource was not found.',
'context' => ['key' => 'value'], 'context' => ['key' => 'value'],
]; ];

View File

@@ -19,7 +19,7 @@ class ServerErrorResponseTest extends Unit
$response = new ServerErrorResponse($e, ['operation' => 'data_processing']); $response = new ServerErrorResponse($e, ['operation' => 'data_processing']);
$expected = [ $expected = [
'status_code' => 500, 'code' => 500,
'message' => 'A Test Error occurred.', 'message' => 'A Test Error occurred.',
'context' => [ 'context' => [
'operation' => 'data_processing' 'operation' => 'data_processing'
@@ -35,13 +35,15 @@ class ServerErrorResponseTest extends Unit
public function testToArrayNotInDevMode(): void public function testToArrayNotInDevMode(): void
{ {
Config::set('app.dev_mode', false);
try { try {
throw new \Exception('A Test Error occurred.'); throw new \Exception('A Test Error occurred.');
} catch (\Exception $exception) { } catch (\Exception $exception) {
$response = new ServerErrorResponse($exception); $response = new ServerErrorResponse($exception);
$expected = [ $expected = [
'status_code' => 500, 'code' => 500,
'message' => 'An internal server error occurred.', 'message' => 'An internal server error occurred.',
]; ];
@@ -51,13 +53,15 @@ class ServerErrorResponseTest extends Unit
public function testToArrayIfCodeIsSet(): void public function testToArrayIfCodeIsSet(): void
{ {
Config::set('app.dev_mode', false);
try { try {
throw new \Exception('A Test Error occurred.', 1234); throw new \Exception('A Test Error occurred.', 1234);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$response = new ServerErrorResponse($exception); $response = new ServerErrorResponse($exception);
$expected = [ $expected = [
'status_code' => 1234, 'code' => 1234,
'message' => 'An internal server error occurred.', 'message' => 'An internal server error occurred.',
]; ];
@@ -75,7 +79,7 @@ class ServerErrorResponseTest extends Unit
$response = new ServerErrorResponse($exception); $response = new ServerErrorResponse($exception);
$expected = [ $expected = [
'status_code' => 1234, 'code' => 1234,
'message' => 'A Test Error occurred.', 'message' => 'A Test Error occurred.',
'file' => $exception->getFile(), 'file' => $exception->getFile(),
'line' => $exception->getLine(), 'line' => $exception->getLine(),

View File

@@ -9,7 +9,9 @@ use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface; use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
use RoadRunner\Logger\Logger as RRLogger;
use Siteworxpro\App\Log\Logger; use Siteworxpro\App\Log\Logger;
use Siteworxpro\App\Services\Facades\Logger as LoggerFacade;
use Siteworxpro\Tests\Unit; use Siteworxpro\Tests\Unit;
class LoggerRpcTest extends Unit class LoggerRpcTest extends Unit
@@ -36,8 +38,8 @@ class LoggerRpcTest extends Unit
->with('message', ['key' => 'value']) ->with('message', ['key' => 'value'])
->times(1); ->times(1);
\Siteworxpro\App\Services\Facades\Logger::getFacadeContainer() LoggerFacade::getFacadeContainer()
->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) { ->bind(RRLogger::class, function () use ($mock) {
return $mock; return $mock;
}); });
@@ -63,8 +65,8 @@ class LoggerRpcTest extends Unit
->with('message', ['key' => 'value']) ->with('message', ['key' => 'value'])
->times(2); ->times(2);
\Siteworxpro\App\Services\Facades\Logger::getFacadeContainer() LoggerFacade::getFacadeContainer()
->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) { ->bind(RRLogger::class, function () use ($mock) {
return $mock; return $mock;
}); });
@@ -91,8 +93,8 @@ class LoggerRpcTest extends Unit
->with('message', ['key' => 'value']) ->with('message', ['key' => 'value'])
->times(1); ->times(1);
\Siteworxpro\App\Services\Facades\Logger::getFacadeContainer() LoggerFacade::getFacadeContainer()
->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) { ->bind(RRLogger::class, function () use ($mock) {
return $mock; return $mock;
}); });
@@ -118,8 +120,8 @@ class LoggerRpcTest extends Unit
->with('message', ['key' => 'value']) ->with('message', ['key' => 'value'])
->times(4); ->times(4);
\Siteworxpro\App\Services\Facades\Logger::getFacadeContainer() LoggerFacade::getFacadeContainer()
->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) { ->bind(RRLogger::class, function () use ($mock) {
return $mock; return $mock;
}); });
@@ -147,8 +149,8 @@ class LoggerRpcTest extends Unit
$mock->expects('log') $mock->expects('log')
->with('notaloglevel', 'message', ['key' => 'value']); ->with('notaloglevel', 'message', ['key' => 'value']);
\Siteworxpro\App\Services\Facades\Logger::getFacadeContainer() LoggerFacade::getFacadeContainer()
->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) { ->bind(RRLogger::class, function () use ($mock) {
return $mock; return $mock;
}); });

View File

@@ -28,16 +28,16 @@ abstract class AbstractServiceProvider extends Unit
$this->assertInstanceOf($providerClass, $provider); $this->assertInstanceOf($providerClass, $provider);
$provider->register(); $provider->register();
$bindings = $provider->bindings; $abstract = $provider->provides()[0];
foreach ($bindings as $abstract => $concrete) { $concrete = get_class($container->make($abstract));
$this->assertTrue($container->bound($abstract), "The $abstract is not bound in the container.");
$this->assertNotNull($container->make($abstract), "The $abstract could not be resolved.");
$this->assertInstanceOf( $this->assertTrue($container->bound($abstract), "The $abstract is not bound in the container.");
$concrete, $this->assertNotNull($container->make($abstract), "The $abstract could not be resolved.");
$container->make($abstract),
"The $abstract is not an instance of $concrete." $this->assertInstanceOf(
); $concrete,
} $container->make($abstract),
"The $abstract is not an instance of $concrete."
);
} }
} }

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\Tests\ServiceProviders;
use Siteworxpro\App\Services\ServiceProviders\CommandBusProvider;
class CommandBusServiceProviderTest extends AbstractServiceProvider
{
protected function getProviderClass(): string
{
return CommandBusProvider::class;
}
}

View File

@@ -8,6 +8,7 @@ use Illuminate\Container\Container;
use Mockery; 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\Kernel;
use Siteworxpro\App\Services\Facade; use Siteworxpro\App\Services\Facade;
use Siteworxpro\App\Services\Facades\Config; use Siteworxpro\App\Services\Facades\Config;