79 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
1ac5075b37 fix: update ServerErrorResponseTest to use dynamic file paths for exceptions
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m32s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m47s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m43s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m49s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m57s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m50s
🏗️✨ Build Workflow / 🖥️ 🔨 Build (push) Successful in 16m55s
🏗️✨ Build Workflow / 🖥️ 🔨 Build Migrations (push) Successful in 1m45s
2025-12-01 15:48:52 -05:00
ba2beca107 feat: implement NotFoundResponse and ServerErrorResponse classes with corresponding tests
Some checks failed
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 4m16s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 4m17s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 4m27s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 4m32s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 4m15s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Failing after 3m1s
2025-12-01 14:55:34 -05:00
b5779afde9 feat: add unit tests for OpenApiController to validate YAML and JSON responses
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m37s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m35s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m52s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m43s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m45s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m19s
2025-12-01 11:41:09 -05:00
c91f35c0b1 feat: add unit tests for OpenApiController to validate YAML and JSON responses
All checks were successful
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m34s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m37s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m30s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m39s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m43s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m19s
2025-12-01 11:30:46 -05:00
88098837a3 feat/swagger (#24)
Some checks are pending
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Waiting to run
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m39s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m44s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m41s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m55s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m59s
Reviewed-on: #24
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-12-01 16:22:42 +00:00
cd49507140 feat: add unit tests for User model name and email formatting
All checks were successful
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m57s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 3m7s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 3m8s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 3m42s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 3m19s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m55s
2025-11-30 19:43:55 -05:00
7792cac8b8 feat: add unit tests for User model name and email formatting
Some checks failed
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 3m59s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Failing after 3m51s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 4m4s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 4m25s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 4m10s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Has been cancelled
2025-11-30 19:39:49 -05:00
eaff49b6a4 feat: add event dispatcher destructor and implement subscriber interface with tests (#23)
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 2m2s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Failing after 1m52s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Failing after 1m58s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m30s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m29s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m24s
Reviewed-on: #23
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-11-30 20:28:22 +00:00
721008bdfc feat: implement Guzzle facade and update JwtMiddleware to use it (#22)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m59s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m55s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 3m9s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 3m5s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 3m51s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 3m11s
Reviewed-on: #22
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-11-25 16:51:45 +00:00
a9a5cb6216 chore: update Dockerfile to use official PHP CLI image for version 8.5.0
All checks were successful
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m4s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m32s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m32s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m45s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m28s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m10s
2025-11-22 10:43:34 -05:00
0504956d9a chore: update PHP version in composer.json and Dockerfile from 8.4 to 8.5
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 3m7s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m43s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 6m47s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 3m8s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m58s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m43s
2025-11-22 10:36:13 -05:00
e9d4cee336 chore: update Postgres version in test configuration from 17 to 18
All checks were successful
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 4m4s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 4m15s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 4m40s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 4m52s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 4m43s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 3m9s
🏗️✨ Build Workflow / 🖥️ 🔨 Build (push) Successful in 17m11s
🏗️✨ Build Workflow / 🖥️ 🔨 Build Migrations (push) Successful in 2m47s
2025-11-20 09:10:30 -05:00
7d9eb96bea fix: make Scope attribute repeatable and improve scope handling in middleware (#21)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m55s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m55s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m58s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 3m1s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m40s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 3m0s
Reviewed-on: #21
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-11-19 19:32:52 +00:00
9b736eb879 feat: add JWK support for JWT validation and update dependencies (#20)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 3m4s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m59s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 3m29s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 4m2s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 3m48s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m49s
Reviewed-on: #20
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-11-17 23:22:53 +00:00
7aa14c0db3 more tests (#19)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m32s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m48s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m33s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m44s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m53s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 3m5s
Reviewed-on: #19
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-11-16 16:40:09 +00:00
474134c654 chore: add deployment configurations and tests for logger and dispatcher (#18)
All checks were successful
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m54s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m48s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 3m9s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 3m9s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 3m4s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m44s
Reviewed-on: #18
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-11-14 18:04:49 +00:00
7fe2722fc1 chore: add deployment configurations and tests for logger and dispatcher (#17)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m40s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m36s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m4s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m24s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m46s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m27s
🏗️✨ Build Workflow / 🖥️ 🔨 Build (push) Successful in 16m7s
🏗️✨ Build Workflow / 🖥️ 🔨 Build Migrations (push) Successful in 1m29s
Reviewed-on: #17
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-11-13 19:38:26 +00:00
5542ad1e75 chore: added documentation (#16)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m42s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m38s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m5s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m44s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m28s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m27s
Reviewed-on: #16
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-11-13 04:14:13 +00:00
e4a55af694 feat/queue-kafka (#15)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m26s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m8s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 59s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m14s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m16s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m13s
Reviewed-on: #15
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-11-12 20:29:42 +00:00
2879cbe203 feat: implement queue system with consumer and message handling (#14)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 3m1s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 3m16s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 3m13s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 3m5s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 3m11s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m51s
Reviewed-on: #14
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-11-12 12:00:31 +00:00
eeb46bc982 feat: implement custom event dispatcher and listener system (#13)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m38s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m38s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m40s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m52s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m51s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m21s
Reviewed-on: #13
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-11-11 16:12:19 +00:00
7d0b00fb89 feat/cli-framework (#12)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m37s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m32s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m54s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m46s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m49s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m18s
Reviewed-on: #12
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-11-11 14:52:29 +00:00
13445a0719 feat: implement JWT authentication and scope validation middleware (#11)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m50s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m41s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 3m8s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 3m22s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 3m5s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m41s
Reviewed-on: #11
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-11-07 17:14:22 +00:00
54ea22c49a chore: update RoadRunner and PHP versions in Dockerfile (#10)
All checks were successful
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m23s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m14s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m31s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m37s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m27s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m44s
🏗️✨ Build Workflow / 🖥️ 🔨 Build (push) Successful in 10m37s
🏗️✨ Build Workflow / 🖥️ 🔨 Build Migrations (push) Successful in 1m33s
Reviewed-on: #10
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-11-07 04:36:49 +00:00
f8d3462cb7 added example model (#9)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m56s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m47s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m55s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m44s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 3m8s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m48s
Reviewed-on: #9
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-10-21 17:23:18 +00:00
68614958a9 feat: add migration container and healthchecks for services (#8)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m32s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m31s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m41s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m50s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m40s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m37s
🏗️✨ Build Workflow / 🖥️ 🔨 Build (push) Successful in 12m3s
🏗️✨ Build Workflow / 🖥️ 🔨 Build Migrations (push) Successful in 1m36s
Reviewed-on: #8
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-10-21 14:33:11 +00:00
413145f479 chore: update test configurations and ignore files for coverage reporting (#7)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m47s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m37s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m20s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m21s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m59s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m55s
Reviewed-on: #7
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-10-16 12:44:36 +00:00
d2bd9d2d1b chore/dev-env (#6)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m55s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m56s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m10s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m56s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m45s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 53s
Reviewed-on: #6
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-10-16 00:24:58 +00:00
78d5213892 feat/migrations-container (#5)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m34s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m24s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m38s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m49s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m41s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 56s
🏗️✨ Build Workflow / 🖥️ 🔨 Build (push) Successful in 12m39s
🏗️✨ Build Workflow / 🖥️ 🔨 Build Migrations (push) Successful in 1m35s
Reviewed-on: #5
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-10-15 15:59:17 +00:00
56b78f0102 refactor: update facade structure and add service providers for logging and Redis (#4)
All checks were successful
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m53s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m4s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m7s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m57s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m44s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 52s
🏗️✨ Build Workflow / 🖥️ 🔨 Build (push) Successful in 3m13s
Reviewed-on: #4
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-10-15 15:39:25 +00:00
05d8c5b813 fix-container-build (#3)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m11s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m29s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m27s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m15s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m20s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 3m26s
Reviewed-on: Siteworxpro/Php-Template#3
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-05-22 18:57:08 -04:00
2d6d0a43f3 The last time I tried this the monkey didn't survive. Let's hope it works better this time. (#2)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 6m44s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 6m53s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m35s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m29s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m39s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 4m3s
Reviewed-on: Siteworxpro/Php-Template#2
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-05-22 13:07:54 -04:00
e203975294 try our sister game minceraft! (#1)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m32s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m30s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m43s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m53s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 3m2s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 3m8s
Reviewed-on: Siteworxpro/Php-Template#1
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
2025-05-14 12:45:28 -04:00
b1e5470a07 what is estonia up to now ...
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m55s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m56s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m4s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m48s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 3m2s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 3m5s
2025-05-14 12:04:19 -04:00
5f9ee939d6 Spinning up the hamster...
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m18s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m29s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m44s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m17s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m25s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 3m7s
2025-05-14 11:55:29 -04:00
e9a3a5885f Ok
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 1m26s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m40s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m17s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m35s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m0s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m38s
2025-05-14 11:50:02 -04:00
f12559d2c3 If it's stupid and it works, it ain't stupid
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 1m55s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m54s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m0s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m3s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m5s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m12s
2025-05-14 11:47:46 -04:00
b466495ae1 I am Root. We are Root.
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 1m5s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m46s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m52s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m30s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m6s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 3m45s
2025-05-14 11:44:22 -04:00
4eb21ace55 Does not work.
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 55s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m25s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m31s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m45s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m59s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 3m21s
2025-05-14 11:42:52 -04:00
a3e54ce989 these confounded tests drive me nuts
Some checks failed
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m2s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m5s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m7s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m10s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m11s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 3m16s
2025-05-14 11:37:15 -04:00
403cb4c12e Never Run This Commit As Root
Some checks failed
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m0s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m59s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m3s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m5s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m7s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Has been cancelled
2025-05-14 11:28:58 -04:00
9f048e408c How is the target directory over 100 gigs?
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 1m6s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m5s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m11s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m15s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Failing after 2m31s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 57s
2025-05-14 11:22:45 -04:00
be570a3173 eppic fail Mike
Some checks failed
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m14s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m18s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m17s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m29s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m36s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 2m48s
2025-05-14 11:15:40 -04:00
6353a4f19e speling is difikult
Some checks failed
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 3m36s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 3m45s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 4m9s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 7m41s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 7m38s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 7m40s
2025-05-14 11:05:53 -04:00
59bcf4d2a7 Handled a particular error.
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 42s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m27s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m26s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m40s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m21s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m28s
2025-05-14 11:02:48 -04:00
89bc3e75ec It'd be nice if type errors caused the compiler to issue a type error
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 1m49s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m57s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m2s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m0s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m0s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m59s
2025-05-14 11:00:44 -04:00
fb04e9c390 Never gonna make you cry
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 2m19s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m55s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m51s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m2s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m58s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m3s
2025-05-14 10:53:51 -04:00
77d66abbe0 it is hump day _^_
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 1m18s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m1s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m1s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m2s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m59s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m5s
2025-05-14 10:52:20 -04:00
52aa7abc2d Landed.
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 1m51s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m58s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m2s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m1s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m0s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m2s
2025-05-14 10:37:10 -04:00
b6256a7e1d Automate Accounting
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 2m31s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m41s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m50s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m38s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m25s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m31s
2025-05-14 10:32:25 -04:00
59fa57d643 Moved something to somewhere... goodnight...
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 1m18s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m31s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m30s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m34s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m33s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m44s
2025-05-14 10:29:58 -04:00
199921fb83 Still can't get this right...
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 59s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m5s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m7s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m4s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m4s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m28s
2025-05-14 10:28:10 -04:00
aaf772dc6d apparently i did something…
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 1m13s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m27s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m24s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m17s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m39s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 3m36s
2025-05-14 10:25:33 -04:00
89fd169067 Never gonna tell a lie and hurt you
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 2m3s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m7s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m9s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m7s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m11s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m11s
2025-05-14 10:22:59 -04:00
a01a0c590b QuickFix.
Some checks failed
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m9s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m13s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m14s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m13s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m15s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 2m27s
2025-05-14 10:19:36 -04:00
dfccf9ca4f --help
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 1m51s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m3s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m1s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m50s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m11s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m5s
2025-05-14 10:16:47 -04:00
011a1e77c0 Use a real JS construct, WTF knows why this works in chromium.
Some checks failed
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 9m49s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 9m47s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 9m52s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 10m17s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 10m25s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 10m30s
2025-05-14 10:06:28 -04:00
7c7b538616 Does this work
Some checks failed
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m2s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 2m6s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m5s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m1s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m5s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m10s
2025-05-14 10:00:22 -04:00
71fa18bc17 bla
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 1m2s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m58s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m3s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m7s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m5s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m4s
2025-05-14 09:57:56 -04:00
7308f33de4 TDD: 1, Me: 0
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 1m41s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m3s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m2s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m0s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m23s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m36s
2025-05-14 09:37:47 -04:00
d0d18fd603 Definitely fixing a mistake Copilot made. Totally not mine.
Some checks failed
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m21s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 1m41s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m44s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m45s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m4s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m19s
2025-05-14 09:35:20 -04:00
f57406f981 Popping stash
Some checks failed
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m10s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m15s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m14s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 2m28s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m32s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m33s
2025-05-14 09:31:33 -04:00
e46bf5a6ec All your codebase are belong to us.
Some checks failed
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 9m29s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 10m3s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 10m8s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 10m18s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 10m20s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 10m23s
2025-05-14 09:09:41 -04:00
02acc9afbe  - Temporary commit.
All checks were successful
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 6m52s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 6m54s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 7m19s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 7m22s
2025-05-14 07:37:54 -04:00
cdb2b28cb7 removed echo and die statements, lolz.
All checks were successful
🧪✨ Tests Workflow / License Check (push) Successful in 2m1s
🧪✨ Tests Workflow / Code Sniffer (push) Successful in 2m6s
🧪✨ Tests Workflow / Code Lint (push) Successful in 2m11s
🧪✨ Tests Workflow / Unit Tests (push) Successful in 2m49s
2025-05-13 21:32:03 -04:00
118e83414d Continued development... 2025-05-13 20:53:50 -04:00
d013641191 I expected something different.
All checks were successful
🧪✨ Tests Workflow / Code Lint (push) Successful in 1m29s
🧪✨ Tests Workflow / Code Sniffer (push) Successful in 1m56s
🧪✨ Tests Workflow / Unit Tests (push) Successful in 3m54s
🧪✨ Tests Workflow / License Check (push) Successful in 4m29s
2025-05-13 20:52:10 -04:00
a2d500d398 Merging 'WIP: Do Not Merge This Branch' Into Master
All checks were successful
🧪✨ Tests Workflow / Unit Tests (push) Successful in 3m7s
🧪✨ Tests Workflow / Code Lint (push) Successful in 3m33s
🧪✨ Tests Workflow / License Check (push) Successful in 3m36s
🧪✨ Tests Workflow / Code Sniffer (push) Successful in 3m18s
2025-05-13 20:47:53 -04:00
7694f29f78 fixed the israeli-palestinian conflict
Some checks failed
🧪✨ Tests Workflow / Code Lint (push) Has been cancelled
🧪✨ Tests Workflow / Unit Tests (push) Has been cancelled
🧪✨ Tests Workflow / License Check (push) Has been cancelled
🧪✨ Tests Workflow / Code Sniffer (push) Has been cancelled
2025-05-13 20:45:19 -04:00
c56a77d32c The last time I tried this the monkey didn't survive. Let's hope it works better this time.
All checks were successful
🧪✨ Tests Workflow / License Check (push) Successful in 1m39s
🧪✨ Tests Workflow / Code Lint (push) Successful in 1m19s
🧪✨ Tests Workflow / Code Sniffer (push) Successful in 1m26s
🧪✨ Tests Workflow / Unit Tests (push) Successful in 1m31s
2025-05-13 20:41:54 -04:00
147 changed files with 9273 additions and 951 deletions

192
.dev/docker-compose.yml Normal file
View File

@@ -0,0 +1,192 @@
volumes:
redisdata: {}
pgdata: {}
services:
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
container_name: traefik
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
interval: 10s
timeout: 5s
retries: 5
ports:
- "80:80"
- "443:443"
- "9001:9001"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- "./ssl:/etc/ssl"
restart: always
command:
- "--providers.docker=true"
- "--api.insecure=true"
- "--ping"
- "--providers.file.filename=/etc/ssl/traefik.yml"
- "--providers.docker.exposedByDefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web-secure.address=:443"
- "--entrypoints.grpc.address=:9001"
- "--accesslog=true"
- "--entrypoints.web.http.redirections.entryPoint.to=web-secure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
- "--entrypoints.web.http.redirections.entrypoint.permanent=true"
composer-runtime:
volumes:
- ..:/app
image: siteworxpro/composer
entrypoint: "/bin/sh -c 'while true; do sleep 30; done;'"
environment:
PHP_IDE_CONFIG: serverName=localhost
swagger-ui:
labels:
- "traefik.enable=true"
- "traefik.http.routers.swagger-ui.entrypoints=web-secure"
- "traefik.http.routers.swagger-ui.rule=Host(`localhost`) && PathPrefix(`/docs`)"
- "traefik.http.routers.swagger-ui.tls=true"
- "traefik.http.routers.swagger-ui.service=swagger-ui"
- "traefik.http.services.swagger-ui.loadbalancer.server.port=8080"
image: swaggerapi/swagger-ui:latest
container_name: swagger-ui
environment:
BASE_URL: /docs
URL: /.well-known/swagger.yaml
migration-container:
volumes:
- ../db/migrations:/app/db/migrations
- ../bin:/app/bin
image: siteworxpro/migrate:v4.18.3
working_dir: /app
# entrypoint: "/bin/sh -c 'while true; do sleep 30; done;'"
entrypoint: /bin/sh -c '/app/bin/migrate.sh'
depends_on:
postgres:
condition: service_healthy
environment:
DB_USERNAME: ${DB_USERNAME:-siteworxpro}
DB_PASSWORD: ${DB_PASSWORD:-password}
DB_DATABASE: ${DB_DATABASE:-siteworxpro}
DB_HOST: ${DB_HOST-postgres}
DB_PORT: ${DB_PORT-5432}
dev-runtime:
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.entrypoints=web-secure"
- "traefik.http.routers.api.rule=Host(`localhost`) || Host(`127.0.0.1`)"
- "traefik.http.routers.api.tls=true"
- "traefik.http.routers.api.service=api"
- "traefik.http.services.api.loadbalancer.healthcheck.path=/healthz"
- "traefik.http.services.api.loadbalancer.healthcheck.interval=5s"
- "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:
- ..:/app
build:
args:
KAFKA_ENABLED: "0"
UID: 0
USER: root
context: ..
dockerfile: Dockerfile
entrypoint: "/bin/sh -c 'while true; do sleep 30; done;'"
depends_on:
migration-container:
condition: service_completed_successfully
traefik:
condition: service_healthy
redis:
condition: service_healthy
postgres:
condition: service_healthy
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
PHP_IDE_CONFIG: serverName=localhost
WORKERS: 1
GRPC_WORKERS: 1
DEBUG: 1
REDIS_HOST: redis
DB_HOST: postgres
DEV_MODE: 1
## Kafka and Zookeeper for local development
kafka-ui:
image: kafbat/kafka-ui:latest # Or kafbat/kafka-ui:latest for newer Kafka
container_name: kafka-ui
ports:
- "8080:8080" # Expose the UI port
environment:
KAFKA_CLUSTERS_0_NAME: local-kafka-cluster
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
depends_on:
kafka:
condition: service_started
zookeeper:
condition: service_started
zookeeper:
image: ubuntu/zookeeper:latest
environment:
ALLOW_ANONYMOUS_LOGIN: "yes"
ports:
- "2181:2181"
kafka:
image: ubuntu/kafka:latest
environment:
KAFKA_BROKER_ID: 1
KAFKA_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
ALLOW_PLAINTEXT_LISTENER: "yes"
ports:
- "9092:9092"
depends_on:
zookeeper:
condition: service_started
redis:
image: redis:latest
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
ports:
- "6379:6379"
volumes:
- redisdata:/data
postgres:
image: postgres:18
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-siteworxpro}"]
interval: 10s
timeout: 5s
retries: 5
environment:
POSTGRES_USER: ${DB_USERNAME:-siteworxpro}
POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
POSTGRES_DB: ${DB_DATABASE:-siteworxpro}
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql

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

@@ -1,3 +1,5 @@
.idea/
.DS_Store
vendor/
.phpunit.cache/
tests/

View File

@@ -1,19 +1,17 @@
on:
create:
push:
tags:
- '*'
- "v*"
name: 🏗️✨ Build Workflow
jobs:
Build:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
name: 🖥️🔨 Build
name: 🖥️ 🔨 Build
runs-on: ubuntu-latest
steps:
- name: 🛡️ 🔒 Add Siteworx CA Certificates
run: |
apt update && apt install -yq ca-certificates curl
curl -Ls https://siteworxpro.com/hosted/Siteworx+Root+CA.pem -o /usr/local/share/ca-certificates/sw.crt
update-ca-certificates
@@ -28,12 +26,54 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: 🏗️🔧 Set up Docker Buildx
- name: Write Version File
run: |
echo $GITEA_REF_NAME > VERSION
sed -i "s/dev-version/${GITEA_REF_NAME}/g" src/Helpers/Version.php
- name: 🏗️ 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 🐳🔨 Build Backend Container
- name: 🐳 🔨 Build Container
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
dockerfile: Dockerfile
tags: siteworxpro/template:${{ gitea.ref_name }}
Build-Migrations:
needs:
- Build
name: 🖥️ 🔨 Build Migrations
runs-on: ubuntu-latest
steps:
- name: 🛡️ 🔒 Add Siteworx CA Certificates
run: |
curl -Ls https://siteworxpro.com/hosted/Siteworx+Root+CA.pem -o /usr/local/share/ca-certificates/sw.crt
update-ca-certificates
- name: 🔑 🔐 Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: 📖 🔍 Checkout Repository Code
uses: actions/checkout@v2
with:
fetch-depth: 1
- name: 🏗️ 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 🐳 🔨 Build Migrations Container
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
sbom: true
provenance: true
context: .
file: migrations.Dockerfile
tags: siteworxpro/template:${{ gitea.ref_name }}-migrations

View File

@@ -1,18 +1,19 @@
on:
push: {}
push:
branches:
- "**"
name: 🧪✨ Tests Workflow
jobs:
LicenseCheck:
name: License Check
DatabaseMigrations:
name: 🧪 ✨ Database Migrations
runs-on: ubuntu-latest
steps:
- name: 🛡️ 🔒 Add Siteworx CA Certificates
run: |
apt update && apt install -yq ca-certificates curl
curl -Ls https://siteworxpro.com/hosted/Siteworx+Root+CA.pem -o /usr/local/share/ca-certificates/sw.crt
update-ca-certificates
@@ -27,15 +28,114 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Install Composer Libraries
- name: 🏎️ 🏁 Start Support Containers
run: |
echo "Starting Support Containers"
docker run --rm \
--network "${{ env.JOB_CONTAINER_NAME }}-${{ gitea.job }}-network" \
--name ${{ gitea.job }}-${{ gitea.run_id }}-postgres \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=postgres \
-p 5432 \
-d postgres:18
echo "Waiting for Postgres to start"
sleep 10
- name: 💽 ⬆️ Run Migrations
run: |
docker run \
--name ${{ gitea.run_id }}-migrate \
--rm \
--volumes-from ${{ env.JOB_CONTAINER_NAME }} \
--network "${{ env.JOB_CONTAINER_NAME }}-${{ gitea.job }}-network" \
-w ${{ github.workspace }} \
siteworxpro/migrate:v4.18.3 -database "postgres://postgres:postgres@${{ gitea.job }}-${{ gitea.run_id }}-postgres:5432/postgres?sslmode=disable" -path db/migrations up
- name: 💽 ⬇️ Rollback Migrations
run: |
docker run \
--name ${{ gitea.run_id }}-migrate \
--rm \
--volumes-from ${{ env.JOB_CONTAINER_NAME }} \
--network "${{ env.JOB_CONTAINER_NAME }}-${{ gitea.job }}-network" \
-w ${{ github.workspace }} \
siteworxpro/migrate:v4.18.3 -database "postgres://postgres:postgres@${{ gitea.job }}-${{ gitea.run_id }}-postgres:5432/postgres?sslmode=disable" -path db/migrations down --all
- name: 🧨 💥 Tear Down Support Containers
if: always()
run: |
docker stop ${{ gitea.job }}-${{ gitea.run_id }}-postgres
LibraryAudit:
name: 🛡️ 🔒 Library Audit
runs-on: ubuntu-latest
steps:
- name: 🛡️ 🔒 Add Siteworx CA Certificates
run: |
curl -Ls https://siteworxpro.com/hosted/Siteworx+Root+CA.pem -o /usr/local/share/ca-certificates/sw.crt
update-ca-certificates
- name: 📖 🔍 Checkout Repository Code
uses: actions/checkout@v2
with:
fetch-depth: 1
- name: 🔑 🔐 Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: 📖 ✨ Install Composer Libraries
run: |
docker run --rm \
-v composer-cache:/tmp/cache \
--volumes-from ${{ env.JOB_CONTAINER_NAME }} \
-w ${{ github.workspace }} \
siteworxpro/composer \
install --ignore-platform-reqs --no-interaction --prefer-dist --optimize-autoloader
- name: Run License Check
- name: Run Library Audit
run: |
docker run --rm \
--volumes-from ${{ env.JOB_CONTAINER_NAME }} \
-w ${{ github.workspace }} \
siteworxpro/composer \
audit
LicenseCheck:
name: 🛡️ 🔒 License Check
runs-on: ubuntu-latest
steps:
- name: 🛡️ 🔒 Add Siteworx CA Certificates
run: |
curl -Ls https://siteworxpro.com/hosted/Siteworx+Root+CA.pem -o /usr/local/share/ca-certificates/sw.crt
update-ca-certificates
- name: 📖 🔍 Checkout Repository Code
uses: actions/checkout@v2
with:
fetch-depth: 1
- name: 🔑 🔐 Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: 📖 ✨ Install Composer Libraries
run: |
docker run --rm \
-v composer-cache:/tmp/cache \
--volumes-from ${{ env.JOB_CONTAINER_NAME }} \
-w ${{ github.workspace }} \
siteworxpro/composer \
install --ignore-platform-reqs --no-interaction --prefer-dist --optimize-autoloader
- name: 🎟️ 🔬 Run License Check
run: |
docker run --rm \
--volumes-from ${{ env.JOB_CONTAINER_NAME }} \
@@ -44,12 +144,11 @@ jobs:
run tests:license
CodeLint:
name: Code Lint
name: 📝 ✨ Code Lint
runs-on: ubuntu-latest
steps:
- name: 🛡️ 🔒 Add Siteworx CA Certificates
run: |
apt update && apt install -yq ca-certificates curl
curl -Ls https://siteworxpro.com/hosted/Siteworx+Root+CA.pem -o /usr/local/share/ca-certificates/sw.crt
update-ca-certificates
@@ -64,9 +163,10 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Install Composer Libraries
- name: 📖 ✨ Install Composer Libraries
run: |
docker run --rm \
-v composer-cache:/tmp/cache \
--volumes-from ${{ env.JOB_CONTAINER_NAME }} \
-w ${{ github.workspace }} \
siteworxpro/composer \
@@ -81,12 +181,11 @@ jobs:
run tests:lint
CodeSniffer:
name: Code Sniffer
name: 🐙 🔍 Code Sniffer
runs-on: ubuntu-latest
steps:
- name: 🛡️ 🔒 Add Siteworx CA Certificates
run: |
apt update && apt install -yq ca-certificates curl
curl -Ls https://siteworxpro.com/hosted/Siteworx+Root+CA.pem -o /usr/local/share/ca-certificates/sw.crt
update-ca-certificates
@@ -101,9 +200,10 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Install Composer Libraries
- name: 📖 ✨ Install Composer Libraries
run: |
docker run --rm \
-v composer-cache:/tmp/cache \
--volumes-from ${{ env.JOB_CONTAINER_NAME }} \
-w ${{ github.workspace }} \
siteworxpro/composer \
@@ -118,12 +218,11 @@ jobs:
run tests:phpstan
UnitTests:
name: Unit Tests
name: 🧪 ✅ Unit Tests
runs-on: ubuntu-latest
steps:
- name: 🛡️ 🔒 Add Siteworx CA Certificates
run: |
apt update && apt install -yq ca-certificates curl
curl -Ls https://siteworxpro.com/hosted/Siteworx+Root+CA.pem -o /usr/local/share/ca-certificates/sw.crt
update-ca-certificates
@@ -138,18 +237,32 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Install Composer Libraries
- name: 📖 ✨ Install Composer Libraries
run: |
docker run --rm \
-v composer-cache:/tmp/cache \
--volumes-from ${{ env.JOB_CONTAINER_NAME }} \
-w ${{ github.workspace }} \
siteworxpro/composer \
install --ignore-platform-reqs --no-interaction --prefer-dist --optimize-autoloader
- name: Run Unit Tests
- name: 🧪 ✅ Run Unit Tests
uses: addnab/docker-run-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
image: siteworxpro/composer
options: --volumes-from ${{ env.JOB_CONTAINER_NAME }} -w ${{ gitea.workspace }}
run: |
docker run --rm \
--volumes-from ${{ env.JOB_CONTAINER_NAME }} \
-w ${{ github.workspace }} \
siteworxpro/composer \
run tests:unit
bin/pcov.sh
composer run tests:unit:coverage
# - name: 📦 Publish Build Artifacts
# env:
# NODE_TLS_REJECT_UNAUTHORIZED: 0
# uses: christopherhx/gitea-upload-artifact@v4
# with:
# options: --volumes-from ${{ env.JOB_CONTAINER_NAME }} -w ${{ gitea.workspace }}
# name: junit-coverage.xml
# path: tests/reports/junit.xml
# retention-days: 1

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
.idea/
.DS_Store
vendor/
.phpunit.cache/
tests/reports/

View File

@@ -1,5 +0,0 @@
include:
- local: .gitlab/ci/stages.yml
- local: .gitlab/ci/tests.yml
- local: .gitlab/ci/libraries.yml

View File

@@ -1,15 +0,0 @@
Install Composer Libraries:
stage: libraries
image: siteworxpro/composer:latest
rules:
- if: '$CI_COMMIT_TAG'
when: never
- if: '$CI_PIPELINE_SOURCE == "push"'
when: always
- when: never
script:
- composer install --ignore-platform-reqs
artifacts:
paths:
- vendor/
expire_in: 1 hour

View File

@@ -1,3 +0,0 @@
stages:
- libraries
- tests

View File

@@ -1,65 +0,0 @@
Unit Tests:
stage: tests
needs:
- Install Composer Libraries
rules:
- if: '$CI_COMMIT_TAG'
when: never
- if: '$CI_PIPELINE_SOURCE == "push"'
when: always
- when: never
image: siteworxpro/composer
before_script: |
bin/pcov.sh
script: |
echo "Running unit tests..."
composer run tests:unit:coverage
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
artifacts:
expire_in: 1 day
reports:
junit: tests/reports/junit.xml
paths:
- tests/reports/
Run License Check:
stage: tests
needs:
- Install Composer Libraries
rules:
- if: '$CI_COMMIT_TAG'
when: never
- if: '$CI_PIPELINE_SOURCE == "push"'
when: on_success
- when: never
image: siteworxpro/composer
script:
- composer run tests:license
Run Code Lint:
stage: tests
needs:
- Install Composer Libraries
rules:
- if: '$CI_COMMIT_TAG'
when: never
- if: '$CI_PIPELINE_SOURCE == "push"'
when: on_success
- when: never
image: siteworxpro/composer
script:
- composer run tests:lint
Run Code Sniffer:
stage: tests
needs:
- Install Composer Libraries
rules:
- if: '$CI_COMMIT_TAG'
when: never
- if: '$CI_PIPELINE_SOURCE == "push"'
when: on_success
- when: never
image: siteworxpro/composer
script:
- composer run tests:phpstan

View File

@@ -6,6 +6,17 @@ server:
rpc:
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:
pool:
allocate_timeout: 5s
@@ -21,4 +32,4 @@ http:
logs:
encoding: json
level: ${LOG_LEVEL:-info}
mode: production
mode: ${LOG_MODE:-production}

View File

@@ -0,0 +1,10 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name=".dev: Compose Deployment" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="sourceFilePath" value=".dev/docker-compose.yml" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

8
.run/All.run.xml Normal file
View File

@@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All" type="ComposerRunConfigurationType" factoryName="Composer Script">
<option name="commandLineParameters" value="" />
<option name="pathToComposerJson" value="$PROJECT_DIR$/composer.json" />
<option name="script" value="tests:all" />
<method v="2" />
</configuration>
</component>

8
.run/Lint_fix.run.xml Normal file
View File

@@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Lint:fix" type="ComposerRunConfigurationType" factoryName="Composer Script">
<option name="commandLineParameters" value="" />
<option name="pathToComposerJson" value="$PROJECT_DIR$/composer.json" />
<option name="script" value="tests:lint:fix" />
<method v="2" />
</configuration>
</component>

11
.run/Main.run.xml Normal file
View File

@@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Main" type="PHPUnitRunConfigurationType" factoryName="PHPUnit">
<CommandLine>
<PhpTestInterpreterSettings>
<option name="interpreterName" value="composer-runtime" />
</PhpTestInterpreterSettings>
</CommandLine>
<TestRunner configuration_file="$PROJECT_DIR$/phpunit.xml" coverage_engine="PCov" scope="XML" use_alternative_configuration_file="true" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,5 +1,5 @@
# Use the RoadRunner image as a base for the first stage
FROM ghcr.io/roadrunner-server/roadrunner:2025.1.1 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
FROM siteworxpro/composer AS library
@@ -12,14 +12,28 @@ RUN composer install --optimize-autoloader --ignore-platform-reqs --no-dev
# Use the official PHP CLI image with Alpine Linux for the second stage
FROM php:8.4.6-alpine AS php
FROM siteworxpro/php:8.5.1-cli-alpine AS php
ARG KAFKA_ENABLED=0
ARG USER=appuser
ARG UID=1000
# Move the production PHP configuration file to the default location
RUN mv /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini \
&& apk add libpq-dev linux-headers --no-cache \
&& docker-php-ext-install pdo_pgsql sockets \
&& docker-php-ext-install pdo_pgsql sockets pcntl \
&& rm -rf /var/cache/apk/*
RUN if [ "$KAFKA_ENABLED" -eq 1 ] ; then \
echo "Kafka support enabled" ; \
apk add autoconf g++ librdkafka-dev make --no-cache ; \
pecl install rdkafka && docker-php-ext-enable rdkafka ; \
apk del autoconf g++ make ; \
else \
echo "Kafka support disabled" ; \
exit 0 ; \
fi
# Set the working directory to /app
WORKDIR /app
@@ -31,10 +45,19 @@ COPY --from=library /app/vendor /app/vendor
# Copy the RoadRunner configuration file and source
ADD src src/
ADD server.php .
ADD .rr.yaml .
ADD generated generated/
ADD protos protos/
ADD server.php cli.php grpc-worker.php .rr.yaml 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 ["rr", "serve", "-c", ".rr.yaml", "-s"]
ENTRYPOINT ["rr", "serve"]
CMD ["-c", ".rr.yaml", "-s"]

148
README.md
View File

@@ -1,26 +1,150 @@
# Template
```shell
export PHP_IDE_CONFIG=serverName=localhost
![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
This project uses Docker Compose and Make to manage the development environment. The `makefile` provides convenient
commands for common development tasks.
## Prerequisites
- Docker and Docker Compose
- Make
- 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
```
## Available Commands
```shell
docker run --rm -v $(PWD):/app siteworxpro/composer run tests:all
### 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
```
### migrations
### Adding a New Package
create a new migration
```shell
docker run --rm -v $(PWD):/app siteworxpro/migrate:v4.18.3 create -ext sql -dir /app/db/migrations -seq create_users_table
```bash
make composer-require package=vendor/package-name
```
### Running Tests
```bash
make test
```
### Code Quality Check
```bash
make lint
make fmt
```
### Debugging
```bash
make enable-debug
make run
```
## Notes
- 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
```text
postgres://siteworxpro:password@localhost:5432/siteworxpro?sslmode=disable
```
Copyright (c)2025 Siteworx Professionals, LLC
```shell
docker run --rm -v $(PWD):/app siteworxpro/migrate:v4.18.3 -database "postgres://siteworxpro:password@localhost:5432/siteworxpro?sslmode=disable" -path /app/db/migrations up
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation
files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom
the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```

4
bin/migrate.sh Executable file
View File

@@ -0,0 +1,4 @@
eval #!/bin/sh
set -e
migrate -path /app/db/migrations -database "postgres://$DB_USERNAME:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_DATABASE?sslmode=disable" up

View File

@@ -4,9 +4,9 @@ echo "Installing xDebug"
apk add make gcc linux-headers autoconf alpine-sdk
curl -sL https://github.com/xdebug/xdebug/archive/3.4.0.tar.gz -o 3.4.0.tar.gz
tar -xvf 3.4.0.tar.gz
cd xdebug-3.4.0 || exit
curl -sL https://github.com/xdebug/xdebug/archive/3.5.0alpha3.tar.gz -o 3.5.0alpha3.tar.gz
tar -xvf 3.5.0alpha3.tar.gz
cd xdebug-3.5.0alpha3 || exit
phpize
./configure --enable-xdebug
make
@@ -20,5 +20,5 @@ xdebug.client_host = host.docker.internal
" > /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
cd ..
rm -rf xdebug-3.4.0
rm -rf 3.4.0.tar.gz
rm -rf xdebug-3.5.0alpha3
rm -rf 3.5.0alpha3.tar.gz

11
cli.php Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/local/bin/php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Siteworxpro\App\Cli\App;
$cliApp = new App();
exit($cliApp->run());

View File

@@ -4,26 +4,39 @@
"autoload": {
"psr-4": {
"Siteworxpro\\App\\": "src/",
"Siteworxpro\\Tests\\": "tests/"
"Siteworxpro\\Tests\\": "tests/",
"GRPC\\": "generated/GRPC"
}
},
"require": {
"php": "^8.4",
"league/route": "^6.2",
"illuminate/database": "^12.10",
"spiral/roadrunner-http": "^3.5",
"nyholm/psr7": "^1.8",
"php": "^8.5",
"league/route": "^6.2.0",
"illuminate/database": "^v12.34.0",
"spiral/roadrunner-http": "^v3.6.0",
"nyholm/psr7": "^1.8.2",
"illuminate/support": "^v12.10.2",
"roadrunner-php/app-logger": "^1.2",
"siteworxpro/config": "^1.1",
"predis/predis": "^3.0"
"roadrunner-php/app-logger": "^1.2.0",
"siteworxpro/config": "^1.1.1",
"predis/predis": "^v3.2.0",
"siteworxpro/http-status": "0.0.2",
"lcobucci/jwt": "^5.6",
"adhocore/cli": "^1.9",
"robinvdvleuten/ulid": "^5.0",
"monolog/monolog": "^3.9",
"react/promise": "^3",
"react/async": "^4",
"guzzlehttp/guzzle": "^7.10",
"zircote/swagger-php": "^5.7",
"spiral/roadrunner-grpc": "^3.5",
"league/tactician": "^1.1"
},
"require-dev": {
"phpunit/phpunit": "^12.1",
"phpunit/phpunit": "^12.4",
"mockery/mockery": "^1.6",
"squizlabs/php_codesniffer": "^3.12",
"squizlabs/php_codesniffer": "^4.0",
"lendable/composer-license-checker": "^1.2",
"phpstan/phpstan": "^2.1"
"phpstan/phpstan": "^2.1.31",
"kwn/php-rdkafka-stubs": "^2.2"
},
"scripts": {
"tests:all": [
@@ -53,16 +66,10 @@
"phpstan analyse --level 4 ./src/ -c phpstan.neon"
]
},
"repositories": {
"git.siteworxpro.com/24": {
"repositories": [
{
"type": "composer",
"url": "https://git.siteworxpro.com/api/v4/group/24/-/packages/composer/packages.json",
"options": {
"ssl": {
"verify_peer": false,
"allow_self_signed": true
}
}
}
"url": "https://gitea.siteworxpro.com/api/packages/php-packages/composer"
}
]
}

2749
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,16 @@ use Siteworxpro\App\Helpers\Env;
return [
'app' => [
'log_level' => Env::get('LOG_LEVEL', 'debug'),
'dev_mode' => Env::get('DEV_MODE', false, 'bool'),
],
/**
* The server configuration.
*/
'server' => [
'port' => Env::get('HTTP_PORT', 9501, 'int'),
'dev_mode' => Env::get('DEV_MODE', false, 'bool'),
],
/**
@@ -21,8 +25,14 @@ return [
'database' => Env::get('DB_DATABASE', 'siteworxpro'),
'username' => Env::get('DB_USERNAME', 'siteworxpro'),
'password' => Env::get('DB_PASSWORD', 'password'),
'port' => Env::get('DB_PORT', 5432, 'int'),
'charset' => Env::get('DB_CHARSET', 'utf8'),
'collation' => Env::get('DB_COLLATION', 'utf8_unicode_ci'),
'prefix' => Env::get('DB_PREFIX', ''),
'options' => [
// Add any additional PDO options here
],
],
'cors' => [
'allowed_origins' => Env::get('CORS_ALLOWED_ORIGINS', 'localhost:3000'),
@@ -34,5 +44,45 @@ return [
'host' => Env::get('REDIS_HOST', 'localhost'),
'port' => Env::get('REDIS_PORT', 6379, 'int'),
'database' => Env::get('REDIS_DATABASE', 0, 'int'),
'password' => Env::get('REDIS_PASSWORD'),
],
'jwt' => [
'signing_key' => Env::get('JWT_SIGNING_KEY', 'a_super_secret_key'),
'audience' => Env::get('JWT_AUDIENCE', 'my_audience'),
'issuer' => Env::get('JWT_ISSUER', 'my_issuer'),
'strict_validation' => Env::get('JWT_STRICT_VALIDATION', false, 'bool'),
],
'queue' => [
'broker' => Env::get('QUEUE_BROKER', 'redis'),
'broker_config' => [
'redis' => [
'consumerGroup' => Env::get('QUEUE_REDIS_CONSUMER_GROUP', ''),
],
'kafka' => [
'brokers' => Env::get('QUEUE_KAFKA_BROKERS', 'kafka:9092'),
'consumerGroup' => Env::get('QUEUE_KAFKA_CONSUMER_GROUP', 'default_group'),
],
'rabbitmq' => [
'host' => Env::get('QUEUE_RABBITMQ_HOST', 'localhost'),
'port' => Env::get('QUEUE_RABBITMQ_PORT', 5672, 'int'),
'username' => Env::get('QUEUE_RABBITMQ_USERNAME', 'guest'),
'password' => Env::get('QUEUE_RABBITMQ_PASSWORD', 'guest'),
'vhost' => Env::get('QUEUE_RABBITMQ_VHOST', '/'),
],
'sqs' => [
'key' => Env::get('QUEUE_SQS_KEY', ''),
'secret' => Env::get('QUEUE_SQS_SECRET', ''),
'region' => Env::get('QUEUE_SQS_REGION', 'us-east-1'),
'version' => Env::get('QUEUE_SQS_VERSION', 'latest'),
'queue_url' => Env::get('QUEUE_SQS_QUEUE_URL', ''),
]
]
]
];

View File

@@ -1,46 +0,0 @@
volumes:
redisdata: {}
pgdata: {}
services:
composer-runtime:
volumes:
- .:/app
image: siteworxpro/composer
entrypoint: "/bin/sh -c 'while true; do sleep 30; done;'"
environment:
PHP_IDE_CONFIG: serverName=localhost
dev-runtime:
ports:
- "9501:9501"
volumes:
- .:/app
build:
context: .
dockerfile: Dockerfile
entrypoint: "/bin/sh -c 'while true; do sleep 30; done;'"
environment:
PHP_IDE_CONFIG: serverName=localhost
WORKERS: 1
DEBUG: 1
REDIS_HOST: redis
redis:
image: redis:latest
ports:
- "6379:6379"
volumes:
- redisdata:/data
postgres:
image: postgres:latest
environment:
POSTGRES_USER: ${DB_USERNAME:-siteworxpro}
POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
POSTGRES_DB: ${DB_DATABASE:-siteworxpro}
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data

View 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;
}
}

View 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;
}

View 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;
}
}

View 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
View 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
View 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);
}

187
makefile Normal file
View File

@@ -0,0 +1,187 @@
# 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
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
@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-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)
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
.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

8
migrations.Dockerfile Normal file
View File

@@ -0,0 +1,8 @@
FROM siteworxpro/migrate:v4.18.3
ADD db/migrations /app/db/migrations
ADD bin/migrate.sh /app/bin/migrate.sh
WORKDIR /app
ENTRYPOINT ["/app/bin/migrate.sh"]

23
protos/example.proto Normal file
View 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;
}

View File

@@ -1,14 +1,11 @@
<?php
use Siteworxpro\App\Server;
use Siteworxpro\App\Api;
require __DIR__ . '/vendor/autoload.php';
// Instantiate the ExternalServer class
$server = new Server();
// Start the server
try {
$server = new Api();
$server->startServer();
} catch (JsonException $e) {
echo $e->getMessage();

View File

@@ -4,16 +4,22 @@ declare(strict_types=1);
namespace Siteworxpro\App;
use Illuminate\Container\Container;
use Illuminate\Support\Facades\Facade;
use League\Route\Http\Exception\MethodNotAllowedException;
use League\Route\Http\Exception\NotFoundException;
use League\Route\RouteGroup;
use League\Route\Router;
use Nyholm\Psr7\Factory\Psr17Factory;
use Siteworxpro\App\Facades\Config;
use Siteworxpro\App\Facades\Logger;
use Siteworxpro\App\Controllers\HealthcheckController;
use Siteworxpro\App\Controllers\IndexController;
use Siteworxpro\App\Controllers\OpenApiController;
use Siteworxpro\App\Http\JsonResponseFactory;
use Siteworxpro\App\Http\Middleware\CorsMiddleware;
use Siteworxpro\App\Http\Middleware\JwtMiddleware;
use Siteworxpro\App\Http\Middleware\ScopeMiddleware;
use Siteworxpro\App\Http\Responses\NotFoundResponse;
use Siteworxpro\App\Http\Responses\ServerErrorResponse;
use Siteworxpro\App\Services\Facades\Config;
use Siteworxpro\App\Services\Facades\Logger;
use Spiral\RoadRunner\Http\PSR7Worker;
use Spiral\RoadRunner\Worker;
@@ -26,7 +32,7 @@ use Spiral\RoadRunner\Worker;
*
* @package Siteworxpro\App
*/
class Server
class Api
{
/**
* @var Router The router instance for handling routes.
@@ -38,69 +44,13 @@ class Server
*/
protected PSR7Worker $worker;
/**
* Server constructor.
*
* Initializes the server by booting the PSR-7 worker and router.
* @throws \ReflectionException
*/
public function __construct()
{
$this->boot();
}
/**
* Bootstraps the server by initializing the PSR-7 worker and router.
*
* This method sets up the PSR-7 worker and router instances, and registers
* the routes for the server. It should be called in the constructor of
* subclasses to ensure proper initialization.
*
* @return void
*/
private function boot(): void
{
$container = new Container();
Facade::setFacadeApplication($container);
$this->worker = new PSR7Worker(
Worker::create(),
new Psr17Factory(),
new Psr17Factory(),
new Psr17Factory()
);
$this->router = new Router();
Kernel::boot();
$this->registerRoutes();
$this->bootModelCapsule();
}
/**
* Bootstraps the model capsule for database connections.
*
* This method sets up the database connection using the Eloquent ORM.
* It retrieves the database configuration from the Config facade and
* initializes the Eloquent capsule manager.
*
* @return void
*/
public function bootModelCapsule(): void
{
$capsule = new \Illuminate\Database\Capsule\Manager();
$capsule->addConnection([
'driver' => Config::get('db.driver'),
'host' => Config::get('db.host'),
'database' => Config::get('db.database'),
'username' => Config::get('db.username'),
'password' => Config::get('db.password'),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
]);
$capsule->setAsGlobal();
$capsule->bootEloquent();
}
/**
@@ -111,13 +61,28 @@ class Server
*
* @return void
*/
protected function registerRoutes(): void
public function registerRoutes(): void
{
$this->router->get('/', function () {
return JsonResponseFactory::createJsonResponse(['status_code' => 200, 'message' => 'Server is running']);
$this->worker = new PSR7Worker(
Worker::create(),
new Psr17Factory(),
new Psr17Factory(),
new Psr17Factory()
);
$this->router = new Router();
$this->router->get('/', IndexController::class . '::get');
$this->router->post('/', IndexController::class . '::post');
$this->router->get('/healthz', HealthcheckController::class . '::get');
$this->router->group('/.well-known', function (RouteGroup $router) {
$router->get('/swagger.yaml', OpenApiController::class . '::get');
$router->get('/swagger.json', OpenApiController::class . '::get');
});
$this->router->middleware(new CorsMiddleware());
$this->router->middleware(new JwtMiddleware());
$this->router->middleware(new ScopeMiddleware());
}
/**
@@ -148,27 +113,21 @@ class Server
$response = $this->router->handle($request);
$this->worker->respond($response);
} catch (MethodNotAllowedException | NotFoundException) {
$uri = '';
if (isset($request)) {
$uri = $request->getUri()->getPath();
}
$this->worker->respond(
JsonResponseFactory::createJsonResponse(
['status_code' => 404, 'reason_phrase' => 'Not Found'],
404
)
JsonResponseFactory::createJsonResponse(new NotFoundResponse($uri))
);
} catch (\Throwable $e) {
Logger::error($e->getMessage());
Logger::error($e->getTraceAsString());
$json = ['status_code' => 500, 'reason_phrase' => 'Server Error'];
if (Config::get("server.dev_mode")) {
$json = [
'status_code' => 500,
'reason_phrase' => 'Server Error',
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
];
}
$this->worker->respond(JsonResponseFactory::createJsonResponse($json, 500));
$this->worker->respond(
JsonResponseFactory::createJsonResponse(new ServerErrorResponse($e))
);
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Async\Brokers;
abstract class Broker implements BrokerInterface
{
public const array BROKER_TYPES = [
'redis' => Redis::class,
'rabbitmq' => RabbitMQ::class,
'kafka' => Kafka::class,
'sqs' => Sqs::class,
];
public function __construct(protected $config = [])
{
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Async\Brokers;
use Siteworxpro\App\Async\Queues\Queue;
use Siteworxpro\App\Async\Messages\Message;
interface BrokerInterface
{
public function publish(Queue $queue, Message $message, ?int $delay = null): void;
public function consume(Queue $queue): Message | null;
public function acknowledge(Queue $queue, Message $message): void;
public function reject(Queue $queue, Message $message, bool $requeue = false): void;
public function purge(Queue $queue): void;
}

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Async\Brokers;
use RdKafka\Conf;
use RdKafka\Exception;
use RdKafka\KafkaConsumer;
use RdKafka\Producer;
use Siteworxpro\App\Async\Queues\Queue;
use Siteworxpro\App\Async\Messages\Message;
class Kafka extends Broker
{
private Producer $producer;
private KafkaConsumer $consumer;
public function __construct($config = [])
{
parent::__construct($config);
$conf = new Conf();
$conf->set('bootstrap.servers', $config['brokers'] ?? 'localhost:9092');
$this->producer = new Producer($conf);
$this->producer->addBrokers($config['brokers'] ?? 'localhost:9092');
$conf->set('group.id', $config['consumerGroup'] ?? 'default');
$conf->set('auto.offset.reset', 'earliest');
$this->consumer = new KafkaConsumer($conf);
}
public function __destruct()
{
$this->producer->flush(1000);
}
/**
* @throws \Exception
*/
public function publish(Queue $queue, Message $message, ?int $delay = null): void
{
$topic = $this->producer->newTopic($queue->queueName());
$topic->produce(RD_KAFKA_PARTITION_UA, 0, $message->serialize(), $message->getId());
$this->producer->flush(1000);
}
/**
* @throws Exception
*/
public function consume(Queue $queue): Message|null
{
$this->consumer->subscribe([$queue->queueName()]);
$kafkaMessage = $this->consumer->consume(1000);
if ($kafkaMessage->err === RD_KAFKA_RESP_ERR__TIMED_OUT) {
return null;
}
if ($kafkaMessage->err === RD_KAFKA_RESP_ERR_UNKNOWN_TOPIC_OR_PART) {
throw new \RuntimeException(
"Topic '{$queue->queueName()}' or partition does not exist. Kafka does not auto-create topics" .
" unless configured to do so."
);
}
/** @var string | null $messageData */
$messageData = $kafkaMessage->payload;
if ($messageData !== null) {
/** @var Message $message */
$message = unserialize($messageData, ['allowed_classes' => true]);
$message->setId((string)$kafkaMessage->offset);
return $message;
}
return null;
}
public function acknowledge(Queue $queue, Message $message): void
{
}
public function reject(Queue $queue, Message $message, bool $requeue = false): void
{
}
public function purge(Queue $queue): void
{
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Async\Brokers;
use Siteworxpro\App\Async\Queues\Queue;
use Siteworxpro\App\Async\Messages\Message;
class RabbitMQ extends Broker
{
public function publish(Queue $queue, Message $message, ?int $delay = null): void
{
// TODO: Implement publish() method.
}
public function consume(Queue $queue): Message | null
{
return null;
}
public function acknowledge(Queue $queue, Message $message): void
{
// TODO: Implement acknowledge() method.
}
public function reject(Queue $queue, Message $message, bool $requeue = false): void
{
// TODO: Implement reject() method.
}
public function purge(Queue $queue): void
{
// TODO: Implement purge() method.
}
}

190
src/Async/Brokers/Redis.php Normal file
View File

@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Async\Brokers;
use Predis\Client;
use Predis\Command\RawCommand;
use Siteworxpro\App\Async\Messages\SayHelloMessage;
use Siteworxpro\App\Async\Queues\Queue;
use Siteworxpro\App\Async\Messages\Message;
use Siteworxpro\App\Helpers\Ulid;
class Redis extends Broker
{
private Client $client;
private string $consumerId;
private string $consumerGroup;
private const string CONSUMER_ID_PREFIX = 'consumer-group:';
private const string QUEUE_PREFIX = 'queue:';
private array $queueNames = [];
public function __construct($config = [])
{
parent::__construct($config);
$this->client = \Siteworxpro\App\Services\Facades\Redis::getFacadeRoot();
$this->consumerId = php_uname('n') . ':' . getmypid();
$this->consumerGroup = $config['consumerGroup'] ?? 'default';
}
private function ensureQueue(string $queueName): void
{
if (in_array($queueName, $this->queueNames, true)) {
return;
}
try {
$this->client->executeCommand(
new RawCommand(
'XGROUP',
[
'CREATE',
self::QUEUE_PREFIX . $queueName,
self::CONSUMER_ID_PREFIX . $this->consumerGroup,
'$',
'MKSTREAM'
]
)
);
} catch (\Exception) {
// If the group already exists, we catch the exception and ignore it
// This is because Redis will throw an error if the group already exists
// We can safely ignore this error as it means the group is already set up
}
$this->client->executeCommand(
new RawCommand(
'XGROUP',
[
'CREATECONSUMER',
self::QUEUE_PREFIX . $queueName,
self::CONSUMER_ID_PREFIX . $this->consumerGroup,
$this->consumerId
]
)
);
$this->queueNames[] = $queueName;
}
public function __destruct()
{
foreach ($this->queueNames as $queueName) {
try {
$this->client->executeCommand(
new RawCommand(
'XGROUP',
[
'DELCONSUMER',
self::QUEUE_PREFIX . $queueName,
self::CONSUMER_ID_PREFIX . $this->consumerGroup,
$this->consumerId
]
)
);
} catch (\Exception) {
// Ignore exceptions during cleanup
}
}
}
/**
* @throws \Exception
*/
public function publish(Queue $queue, Message $message, ?int $delay = null): void
{
$command = '%s * data %s';
$command = sprintf(
$command,
self::QUEUE_PREFIX .
$queue->queueName(),
base64_encode($message->serialize())
);
/** @var string $result */
$result = $this
->client
->executeCommand(
new RawCommand('XADD', explode(' ', $command)),
);
$message->setId($result);
}
public function consume(Queue $queue): Message|null
{
$this->ensureQueue($queue->queueName());
$command = 'GROUP %s %s COUNT 1 STREAMS %s >';
$command = sprintf(
$command,
self::CONSUMER_ID_PREFIX . $this->consumerGroup,
$this->consumerId,
self::QUEUE_PREFIX . $queue->queueName(),
);
/** @var array | null $response */
$response = $this
->client
->executeCommand(
new RawCommand(
'XREADGROUP',
explode(' ', $command)
)
);
if ($response === null || !isset($response[0][1][0][1][1])) {
return null;
}
$messageData = base64_decode($response[0][1][0][1][1]);
$messageId = $response[0][1][0][0];
if ($messageData === 'NOOP') {
// If the message is a NOOP, we return null to indicate no actual message
return null;
}
$value = unserialize($messageData, ['allowed_classes' => true]);
if (!$value instanceof Message) {
return null;
}
$value->setId($messageId);
return $value;
}
public function acknowledge(Queue $queue, Message $message): void
{
$response = $this
->client
->executeCommand(
new RawCommand(
'XACK',
[
self::QUEUE_PREFIX . $queue->queueName(),
self::CONSUMER_ID_PREFIX . $this->consumerGroup,
$message->getId()
]
)
);
}
public function reject(Queue $queue, Message $message, bool $requeue = false): void
{
// TODO: Implement reject() method.
}
public function purge(Queue $queue): void
{
// TODO: Implement purge() method.
}
}

36
src/Async/Brokers/Sqs.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Async\Brokers;
use Siteworxpro\App\Async\Queues\Queue;
use Siteworxpro\App\Async\Messages\Message;
class Sqs extends Broker
{
public function publish(Queue $queue, Message $message, ?int $delay = null): void
{
// TODO: Implement publish() method.
}
public function consume(Queue $queue): Message | null
{
return null;
}
public function acknowledge(Queue $queue, Message $message): void
{
// TODO: Implement acknowledge() method.
}
public function reject(Queue $queue, Message $message, bool $requeue = false): void
{
// TODO: Implement reject() method.
}
public function purge(Queue $queue): void
{
// TODO: Implement purge() method.
}
}

173
src/Async/Consumer.php Normal file
View File

@@ -0,0 +1,173 @@
<?php
declare(ticks=1);
namespace Siteworxpro\App\Async;
use Siteworxpro\App\Attributes\Async\HandlesMessage;
use Siteworxpro\App\Async\Messages\Message;
use Siteworxpro\App\Async\Queues\Queue;
use Siteworxpro\App\Services\Facades\Broker;
use Siteworxpro\App\Services\Facades\Logger;
/**
* Long-running process that listens to queues, pops messages, and dispatches them to handlers.
*/
class Consumer
{
private static bool $shutDown = false;
/** @var array<string,string> */
private const array QUEUES = [
'default' => Queues\DefaultQueue::class,
];
/** @var Queue[] */
private array $queues = [];
/** @var array<string, string[]> message FQCN => handler FQCNs */
private array $handlers = [];
private const string HANDLER_NAMESPACE = 'Siteworxpro\\App\\Async\\Handlers\\';
/**
* @param string[] $queues Optional list of queue names (keys from self::QUEUES)
*/
public function __construct(array $queues = [])
{
$queueClasses = $queues === []
? array_values(self::QUEUES)
: array_map(
static function (string $name): string {
if (!isset(self::QUEUES[$name])) {
throw new \InvalidArgumentException("Queue '$name' is not defined.");
}
return self::QUEUES[$name];
},
$queues
);
foreach ($queueClasses as $class) {
$this->queues[] = new $class();
}
$this->registerHandlers();
}
/**
* Discover handler classes under `Handlers` and register them via HandlesMessage attributes.
*/
private function registerHandlers(): void
{
$it = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(__DIR__ . '/Handlers/')
);
/** @var \SplFileInfo $file */
foreach ($it as $file) {
if (!$file->isFile() || $file->getExtension() !== 'php') {
continue;
}
$relative = str_replace(__DIR__ . '/Handlers/', '', $file->getPathname());
$class = self::HANDLER_NAMESPACE . str_replace('/', '\\', substr($relative, 0, -4));
if (!class_exists($class)) {
continue;
}
$ref = new \ReflectionClass($class);
foreach ($ref->getAttributes(HandlesMessage::class) as $attr) {
$messageClass = $attr->newInstance()->getMessageClass();
$this->handlers[$messageClass][] = $class;
}
}
}
/**
* Signal handler used to initiate graceful or immediate shutdown.
*/
public static function handleSignal(int $signal): void
{
switch ($signal) {
case SIGINT:
case SIGTERM:
case SIGHUP:
self::$shutDown = true;
return;
case SIGKILL:
exit(9);
}
}
private function shouldShutDown(): bool
{
return self::$shutDown;
}
/**
* Start the consumer main loop.
*/
public function start(): void
{
if (!\function_exists('pcntl_signal')) {
throw new \RuntimeException('The pcntl extension is required to handle signals.');
}
Logger::info('Starting queue consumer...');
Logger::info('Using Broker: ' . Broker::getFacadeRoot()::class);
foreach ([SIGINT, SIGTERM, SIGHUP] as $sig) {
\pcntl_signal($sig, [self::class, 'handleSignal']);
}
while (true) {
if ($this->shouldShutDown()) {
Logger::info('Shutting down queue consumer...');
break;
}
/** @var Queue $queue */
foreach ($this->queues as $queue) {
Logger::info('Listening to queue: ' . $queue->queueName());
$message = $queue->pop();
if (!$message) {
continue;
}
Logger::info('Processing message of type: ' . get_class($message));
foreach ($this->getHandlersForMessage($message) as $handler) {
$handler($message);
}
// Continue polling from the top of the loop after processing a message.
continue 2;
}
// Avoid busy-looping when no messages are available.
sleep(1);
}
}
/**
* @return callable[] Handler instances invokable with the message
*/
private function getHandlersForMessage(Message $message): array
{
$messageClass = get_class($message);
if (!isset($this->handlers[$messageClass])) {
throw new \RuntimeException("No handler found for message class: $messageClass");
}
$callables = [];
foreach ($this->handlers[$messageClass] as $handlerClass) {
if (class_exists($handlerClass)) {
$callables[] = new $handlerClass();
}
}
return $callables;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Async\Handlers;
use Siteworxpro\App\Async\Messages\Message;
interface HandlerInterface
{
public function __invoke(Message $message): void;
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Async\Handlers;
use Siteworxpro\App\Attributes\Async\HandlesMessage;
use Siteworxpro\App\Async\Messages\Message;
use Siteworxpro\App\Async\Messages\SayHelloMessage;
use Siteworxpro\App\Services\Facades\Logger;
#[HandlesMessage(SayHelloMessage::class)]
class SayHelloHandler implements HandlerInterface
{
public function __invoke(Message | SayHelloMessage $message): void
{
$name = $message->getPayload()['name'] ?? 'Guest';
Logger::info(sprintf("Hello, %s!", $name));
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Async\Messages;
use Siteworxpro\App\Async\Queues\DefaultQueue;
use Siteworxpro\App\Async\Queues\Queue;
use Siteworxpro\App\Helpers\Ulid;
abstract class Message implements \Serializable
{
protected string $id = '';
protected string $uniqueId;
protected array $payload;
protected int $timestamp;
protected string $queue = '';
protected const string DEFAULT_QUEUE = DefaultQueue::class;
abstract public static function dispatch(...$args): void;
abstract public static function dispatchLater(int $delay, ...$args): void;
public function __construct()
{
$this->uniqueId = Ulid::generate();
$this->timestamp = time();
}
protected function getQueue(): Queue
{
if ($this->queue === '') {
$this->queue = static::DEFAULT_QUEUE;
}
return new $this->queue();
}
public function getId(): string
{
return $this->id;
}
/**
* @param string $id
*/
public function setId(string $id): void
{
$this->id = $id;
}
public function getPayload(): array
{
return $this->payload;
}
public function getTimestamp(): int
{
return $this->timestamp;
}
public function __serialize(): array
{
return [
'id' => $this->id,
'payload' => $this->payload,
'timestamp' => $this->timestamp,
'queue' => $this->queue,
];
}
public function __unserialize(array $data): void
{
$this->id = $data['id'];
$this->payload = $data['payload'];
$this->timestamp = $data['timestamp'];
$this->queue = $data['queue'];
}
public function serialize(): string
{
return serialize($this);
}
public function unserialize(string $data): Message
{
$unserializedData = unserialize($data, ['allowed_classes' => [Message::class]]);
$this->id = $unserializedData['id'];
$this->uniqueId = $unserializedData['uniqueId'];
$this->payload = $unserializedData['payload'];
$this->timestamp = $unserializedData['timestamp'];
$this->queue = $unserializedData['queue'];
return $this;
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Async\Messages;
use Siteworxpro\App\Services\Facades\Broker;
class SayHelloMessage extends Message
{
public static function dispatch(...$args): void
{
$name = $args[0] ?? 'World';
$message = new self($name);
Broker::publish(
$message->getQueue(),
$message
);
}
public static function dispatchLater(int $delay, ...$args): void
{
$name = $args[0] ?? 'World';
$message = new self($name);
Broker::publishLater(
$message->getQueue(),
$message,
$delay
);
}
private function __construct(
private readonly string $name
) {
parent::__construct();
$this->payload = [
'name' => $this->name,
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Async\Queues;
readonly class DefaultQueue extends Queue
{
public function queueName(): string
{
return 'default';
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Async\Queues;
use Siteworxpro\App\Async\Messages\Message;
use Siteworxpro\App\Services\Facades\Broker;
readonly abstract class Queue
{
abstract public function queueName(): string;
public function push(Message $message): void
{
Broker::publish($this, $message);
}
public function later(int $delay, Message $message): void
{
Broker::publish($this, $message, $delay);
}
public function pop(): Message | null
{
return Broker::consume($this);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Attributes\Async;
use Attribute;
/**
* Attribute to mark a class as a handler for a specific message class in an async workflow.
*
* Repeatable: attach multiple times to handle multiple message classes.
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
readonly class HandlesMessage
{
/**
* Create a new HandlesMessage attribute.
*
* @param class-string $messageClass Fully-qualified class name of the message handled.
*/
public function __construct(
public string $messageClass,
) {
}
/**
* Get the fully-qualified message class this handler processes.
*
* @return class-string
*/
public function getMessageClass(): string
{
return $this->messageClass;
}
}

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

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Attributes\Events;
use Attribute;
/**
* Attribute to mark a class as an event listener for a specific event class.
*
* Apply this attribute to classes that subscribe to domain or application events.
* Repeatable: can be attached multiple times to the same class to listen for multiple events.
*
* Targets: class only.
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
readonly class ListensFor
{
/**
* Initialize the ListensFor attribute.
*
* @param class-string $eventClass Fully-qualified class name of the event to listen for.
*/
public function __construct(public string $eventClass)
{
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Attributes\Guards;
use Attribute;
use Siteworxpro\App\Services\Facades\Config;
/**
* Attribute to guard classes or methods with JWT claim requirements.
*
* Apply this attribute to a class or method to declare the expected JWT issuer and/or audience.
* If either the issuer or audience is an empty string, the value will be resolved from configuration:
* - `jwt.issuer`
* - `jwt.audience`
*
* Targets: class or method.
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
readonly class Jwt
{
/**
* Initialize the Jwt attribute with optional overrides for expected JWT claims.
*
* @param string $issuer Optional expected JWT issuer (`iss`). Empty string uses `Config::get('jwt.issuer')`.
* @param string $audience Optional expected JWT audience (`aud`). Empty string uses `Config::get('jwt.audience')`.
*/
public function __construct(
private string $issuer = '',
private string $audience = '',
) {
}
/**
* Get the expected audience for validation.
*
* Returns the constructor-provided audience when non-empty; otherwise falls back to `jwt.audience` config.
*
* @return string The audience value to enforce.
*/
public function getAudience(): string
{
if ($this->audience === '') {
return Config::get('jwt.audience') ?? '';
}
return $this->audience;
}
/**
* Get the expected issuer for validation.
*
* Returns the constructor-provided issuer when non-empty; otherwise falls back to `jwt.issuer` config.
*
* @return string The issuer value to enforce.
*/
public function getIssuer(): string
{
if ($this->issuer === '') {
return Config::get('jwt.issuer') ?? '';
}
return $this->issuer;
}
}

View 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
{
}

View 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;
}
}

46
src/Cli/App.php Normal file
View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Cli;
use Ahc\Cli\Application;
use Siteworxpro\App\Cli\Commands\DemoCommand;
use Siteworxpro\App\Cli\Commands\Queue\Start;
use Siteworxpro\App\Cli\Commands\Queue\TestJob;
use Siteworxpro\App\Helpers\Version;
use Siteworxpro\App\Kernel;
use Siteworxpro\App\Services\Facades\Config;
class App
{
private Application $app;
/**
* @throws \ReflectionException
*/
public function __construct()
{
Kernel::boot();
$this->app = new Application('Php-Template', Version::VERSION);
$this->app->add(new DemoCommand());
$this->app->add(new Start());
$this->app->add(new TestJob());
}
public function run(): int
{
$this->app->logo(
<<<EOF
▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄ ▀▀█ ▄
█ ▀█ █ █ █ ▀█ █ ▄▄▄ ▄▄▄▄▄ ▄▄▄▄ █ ▄▄▄ ▄▄█▄▄ ▄▄▄
█▄▄▄█▀ █▄▄▄▄█ █▄▄▄█▀ █ █▀ █ █ █ █ █▀ ▀█ █ ▀ █ █ █▀ █
█ █ █ █ ▀▀▀ █ █▀▀▀▀ █ █ █ █ █ █ ▄▀▀▀█ █ █▀▀▀▀
█ █ █ █ █ ▀█▄▄▀ █ █ █ ██▄█▀ ▀▄▄ ▀▄▄▀█ ▀▄▄ ▀█▄▄▀
EOF
);
return $this->app->handle($_SERVER['argv']);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Cli\Commands;
interface CommandInterface
{
/**
* Execute the command.
*
* @return int
*/
public function execute(): int;
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Cli\Commands;
use Ahc\Cli\Input\Command;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\Services\Facades\CommandBus;
class DemoCommand extends Command implements CommandInterface
{
public function __construct()
{
parent::__construct('api:demo', 'A demo command to showcase the CLI functionality.');
$this->argument('[name]', 'Your name')
->option('-g, --greet', 'Include a greeting message');
}
public function execute(): int
{
$pb = $this->progress(100);
for ($i = 0; $i < 100; $i += 10) {
usleep(100000); // Simulate work
$pb->advance(10);
}
$pb->finish();
$this->writer()->boldBlue("Demo Command Executed!\n");
$name = $this->values()['name'];
$greet = $this->values()['greet'] ?? false;
if ($greet) {
$this->writer()->green("Hello, $name! Welcome to the CLI demo.\n");
} else {
$exampleCommand = new ExampleCommand($name);
$this->writer()->yellow(CommandBus::handle($exampleCommand));
}
return 0;
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Cli\Commands\Queue;
use Ahc\Cli\Input\Command;
use Siteworxpro\App\Async\Consumer;
use Siteworxpro\App\Async\Messages\SayHelloMessage;
use Siteworxpro\App\Cli\Commands\CommandInterface;
class Start extends Command implements CommandInterface
{
public function __construct()
{
parent::__construct('queue:start', 'Start the queue consumer to process messages.');
$this->argument('[queues]', 'The name of the queue to consume from. ex. "first_queue,second_queue"');
}
public function execute(): int
{
$queues = [];
if ($this->values()['queues'] !== null) {
$queues = explode(',', $this->values()['queues']);
}
$consumer = new Consumer($queues);
$consumer->start();
return 0;
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Cli\Commands\Queue;
use Ahc\Cli\Input\Command;
use Siteworxpro\App\Async\Messages\SayHelloMessage;
use Siteworxpro\App\Cli\Commands\CommandInterface;
/**
* Class TestJob
*
* A CLI command to schedule a demo job that dispatches a SayHelloMessage.
*/
class TestJob extends Command implements CommandInterface
{
public function __construct()
{
parent::__construct('queue:demo', 'Schedule a demo job.');
}
/**
* Execute the command to dispatch a SayHelloMessage.
*
* @return int Exit code
*/
public function execute(): int
{
SayHelloMessage::dispatch('World from TestJob Command!');
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

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Controllers;
use League\Route\Http\Exception\NotFoundException;
use Nyholm\Psr7\ServerRequest;
use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface;
use Siteworxpro\App\Helpers\Version;
/**
* Class Controller
*
* An abstract base controller providing default implementations for HTTP methods.
*
* @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
{
/**
* @param ServerRequest $request
* @return ResponseInterface
* @throws NotFoundException
*/
public function get(ServerRequest $request): ResponseInterface
{
throw new NotFoundException("not found");
}
/**
* @throws NotFoundException
*/
public function post(ServerRequest $request): ResponseInterface
{
throw new NotFoundException("not found");
}
/**
* @throws NotFoundException
*/
public function put(ServerRequest $request): ResponseInterface
{
throw new NotFoundException("not found");
}
/**
* @throws NotFoundException
*/
public function delete(ServerRequest $request): ResponseInterface
{
throw new NotFoundException("not found");
}
/**
* @throws NotFoundException
*/
public function patch(ServerRequest $request): ResponseInterface
{
throw new NotFoundException("not found");
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Controllers;
use Nyholm\Psr7\ServerRequest;
use Psr\Http\Message\ResponseInterface;
/**
* Interface ControllerInterface
*
* Defines the contract for handling HTTP requests in a controller.
*/
interface ControllerInterface
{
/**
* Handle the request and return a response.
*
* @param ServerRequest $request The request data.
* @return ResponseInterface The response data.
*/
public function get(ServerRequest $request): ResponseInterface;
/**
* Handle the request and return a response.
*
* @param ServerRequest $request The request data.
* @return ResponseInterface The response data.
*/
public function post(ServerRequest $request): ResponseInterface;
/**
* Handle the request and return a response.
*
* @param ServerRequest $request The request data.
* @return ResponseInterface The response data.
*/
public function put(ServerRequest $request): ResponseInterface;
/**
* Handle the request and return a response.
*
* @param ServerRequest $request The request data.
* @return ResponseInterface The response data.
*/
public function delete(ServerRequest $request): ResponseInterface;
/**
* Handle the request and return a response.
*
* @param ServerRequest $request The request data.
* @return ResponseInterface The response data.
*/
public function patch(ServerRequest $request): ResponseInterface;
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Controllers;
use Illuminate\Database\PostgresConnection;
use Nyholm\Psr7\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Siteworxpro\App\Http\JsonResponseFactory;
use Siteworxpro\App\Http\Responses\GenericResponse;
use Siteworxpro\App\Http\Responses\ServerErrorResponse;
use Siteworxpro\App\Models\Model;
use Siteworxpro\App\Services\Facades\Logger;
use Siteworxpro\App\Services\Facades\Redis;
use Siteworxpro\HttpStatus\CodesEnum;
use OpenApi\Attributes as OA;
/**
* Class HealthcheckController
*
* Handles health check requests to verify database and cache connectivity.
*
* @package Siteworxpro\App\Controllers
*/
class HealthcheckController extends Controller
{
/**
* Handles the GET request for health check.
*
* @throws \JsonException
*/
#[OA\Get(path: '/healthz', tags: ['Healthcheck'])]
#[OA\Response(response: '200', description: 'Healthcheck OK')]
#[OA\Response(response: '503', description: 'Healthcheck Failed')]
public function get(ServerRequest $request): ResponseInterface
{
try {
/** @var PostgresConnection $conn */
$conn = Model::getConnectionResolver()->connection();
$conn->getPdo()->exec('SELECT 1');
$response = Redis::ping();
if ($response->getPayload() !== 'PONG') {
throw new \Exception('Redis ping failed');
}
} catch (\Exception $e) {
Logger::emergency(
'Healthcheck failed: ' . $e->getMessage(),
['exception' => $e]
);
return JsonResponseFactory::createJsonResponse(
new ServerErrorResponse($e),
CodesEnum::SERVICE_UNAVAILABLE
);
}
return JsonResponseFactory::createJsonResponse(
new GenericResponse('Healthcheck OK')
);
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Controllers;
use Nyholm\Psr7\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Siteworxpro\App\Attributes\Guards;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\Docs\TokenSecurity;
use Siteworxpro\App\Docs\UnauthorizedResponse;
use Siteworxpro\App\Http\JsonResponseFactory;
use OpenApi\Attributes as OA;
use Siteworxpro\App\Http\Responses\GenericResponse;
use Siteworxpro\App\Services\Facades\CommandBus;
/**
* Class IndexController
*
* This class handles the index route of the application.
*/
class IndexController extends Controller
{
/**
* Handles the GET request for the index route.
*
* @throws \JsonException
*/
#[Guards\Jwt]
#[Guards\Scope(['get.index', 'status.check'])]
#[Guards\RequireAllScopes]
#[OA\Get(path: '/', security: [new TokenSecurity()], tags: ['Examples'])]
#[OA\Response(
response: '200',
description: 'An Example Response',
content: new OA\JsonContent(ref: '#/components/schemas/GenericResponse')
)]
#[UnauthorizedResponse]
public function get(ServerRequest $request): ResponseInterface
{
$command = new ExampleCommand($request->getQueryParams()['name'] ?? 'Guest');
$greeting = CommandBus::handle($command);
return JsonResponseFactory::createJsonResponse(new GenericResponse('Server is running. ' . $greeting));
}
/**
* Handles the POST request for the index route.
*
* @throws \JsonException
*/
#[Guards\Jwt]
#[Guards\Scope(['post.index'])]
#[OA\Post(path: '/', security: [new TokenSecurity()], tags: ['Examples'])]
#[OA\Response(
response: '200',
description: 'An Example Response',
content: new OA\JsonContent(ref: '#/components/schemas/GenericResponse')
)]
#[UnauthorizedResponse]
public function post(ServerRequest $request): ResponseInterface
{
return JsonResponseFactory::createJsonResponse(new GenericResponse('POST request received'));
}
}

View 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');
}
}

View 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'
);
}
}

View File

@@ -0,0 +1,25 @@
<?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: 'message', type: 'string', example: 'Unauthorized'),
]
)
)
);
}
}

246
src/Events/Dispatcher.php Normal file
View File

@@ -0,0 +1,246 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Events;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection;
use Siteworxpro\App\Attributes\Events\ListensFor;
use function React\Async\await;
use function React\Async\coroutine;
/**
* Class Dispatcher
*
* A custom event dispatcher that automatically registers event listeners
* based on the ListensFor attribute.
*
* @package Siteworxpro\App\Events
*/
class Dispatcher implements DispatcherContract, Arrayable
{
/**
* @var array $listeners Registered event listeners
*/
private array $listeners = [];
/**
* @var Collection $pushed Pushed events collection
*/
private Collection $pushed;
private array $subscribers = [];
/**
* @var string LISTENERS_NAMESPACE The namespace where listeners are located
*/
private const string LISTENERS_NAMESPACE = 'Siteworxpro\\App\\Events\\Listeners\\';
public function __construct()
{
$this->pushed = new Collection();
$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.
*
* @return void
*/
private function registerListeners(): void
{
// traverse the Listeners directory and register all listeners
$listenersPath = __DIR__ . '/Listeners';
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($listenersPath));
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$relativePath = str_replace($listenersPath . '/', '', $file->getPathname());
$className = self::LISTENERS_NAMESPACE . str_replace(['/', '.php'], ['\\', ''], $relativePath);
if (class_exists($className)) {
$reflectionClass = new \ReflectionClass($className);
$attributes = $reflectionClass->getAttributes(ListensFor::class);
foreach ($attributes as $attribute) {
$instance = $attribute->newInstance();
$eventClass = $instance->eventClass;
$this->listen($eventClass, new $className());
}
}
}
}
}
/**
* Register a listener for the given events.
*
* @param $events
* @param $listener
* @return void
*/
public function listen($events, $listener = null): void
{
$this->listeners[$events][] = $listener;
}
/**
* Check if there are listeners for the given event.
*
* @param $eventName
* @return bool
*/
public function hasListeners($eventName): bool
{
return isset($this->listeners[$eventName]) && !empty($this->listeners[$eventName]);
}
/**
* Subscribe a subscriber to the dispatcher.
*
* @param Arrayable $subscriber
* @return void
*/
public function subscribe($subscriber): void
{
$this->subscribers[] = $subscriber;
}
/**
* Dispatch an event and halt on the first non-null response.
*
* @param $event
* @param array $payload
* @return array|null
* @throws \Throwable
*/
public function until($event, $payload = []): array|null
{
return $this->dispatch($event, $payload, true);
}
/**
* Dispatch an event to its listeners.
*
* @param $event
* @param array $payload
* @param bool $halt
* @return array|null
* @throws \Throwable
*/
public function dispatch($event, $payload = [], $halt = false): array|null
{
if (is_object($event)) {
$eventClass = get_class($event);
} else {
$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;
// If no listeners, just await the subscriber promise
if ($listeners === null) {
return await($promise);
}
$responses = [];
foreach ($listeners as $listener) {
$response = $listener($event, $payload);
$responses[$eventClass] = $response;
if ($halt && $response !== null) {
return $response;
}
}
// Await the subscriber promise and merge responses
$promiseResponses = await($promise);
if (is_array($promiseResponses)) {
$responses = array_merge($responses, $promiseResponses);
}
return $responses;
}
/**
* Push an event to be dispatched later.
*
* @param $event
* @param array $payload
* @return void
*/
public function push($event, $payload = []): void
{
$this->pushed->put($event, $payload);
}
/**
* Flush a pushed event, dispatching it if it exists.
*
* @param $event
* @return void
* @throws \Throwable
*/
public function flush($event): void
{
if ($this->pushed->has($event)) {
$payload = $this->pushed->get($event);
$this->dispatch($event, $payload);
$this->pushed->forget([$event]);
}
}
/**
* Forget a pushed event without dispatching it.
*
* @param $event
* @return void
*/
public function forget($event): void
{
$this->pushed->forget([$event]);
}
/**
* Forget all pushed events.
*
* @return void
*/
public function forgetPushed(): void
{
$this->pushed = new Collection();
}
public function toArray(): array
{
return $this->listeners;
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Events\Listeners\Database;
use Illuminate\Database\Events\ConnectionEstablished;
use Illuminate\Database\Events\ConnectionEvent;
use Siteworxpro\App\Attributes\Events\ListensFor;
use Siteworxpro\App\Events\Listeners\Listener;
use Siteworxpro\App\Services\Facades\Logger;
/**
* Class Connected
* @package Siteworxpro\App\Events\Listeners\Database
*/
#[ListensFor(ConnectionEstablished::class)]
class Connected extends Listener
{
/**
* @param mixed $event
* @param array $payload
* @return 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]);
return null;
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Events\Listeners;
/**
* Class Listener
*
* @package Siteworxpro\App\Events\Listeners
*/
abstract class Listener implements ListenerInterface
{
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Events\Listeners;
/**
* Interface ListenerInterface
* @package Siteworxpro\App\Events\Listeners
*/
interface ListenerInterface
{
/**
* @param mixed $event
* @param array $payload
* @return mixed
*/
public function __invoke(mixed $event, array $payload = []): mixed;
}

View 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);
}
}

View 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;
}

View File

@@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Facades;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Support\Facades\Facade;
use Siteworx\Config\Exception\EmptyDirectoryException;
use Siteworx\Config\Exception\FileNotFoundException;
use Siteworx\Config\Exception\UnsupportedFormatException;
/**
* Class Config
*
* This class serves as a facade for the configuration settings of the application.
* It extends the Facade class from the Illuminate\Support\Facades namespace.
*
* @method static bool | string | int get(string $key) Retrieve the configuration value for the given key.
*
* @package Siteworx\App\Facades
*/
class Config extends Facade
{
protected static $cached = false;
/**
* @throws UnsupportedFormatException
* @throws FileNotFoundException
* @throws EmptyDirectoryException
*/
public static function getFacadeRoot(): \Siteworx\Config\Config
{
if (self::$resolvedInstance !== null) {
try {
$config = self::resolveFacadeInstance(self::getFacadeAccessor());
if ($config instanceof \Siteworx\Config\Config) {
return $config;
}
} catch (BindingResolutionException) {
}
}
return \Siteworx\Config\Config::load(__DIR__ . '/../../config.php');
}
/**
* Get the registered name of the component.
*
* @return string The name of the component.
*/
protected static function getFacadeAccessor(): string
{
return \Siteworx\Config\Config::class;
}
}

View File

@@ -1,50 +0,0 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Facades;
use Illuminate\Support\Facades\Facade;
use RoadRunner\Logger\Logger as RRLogger;
use Spiral\Goridge\RPC\RPC;
/**
* Class Logger
*
* This class serves as a facade for the Monolog logger.
* It extends the Facade class from the Illuminate\Support\Facades namespace.
*
* @method static info(string $message, array $context = []) Log an informational message.
* @method static error(string $message, array $context = []) Log an error message.
* @method static warning(string $message, array $context = []) Log a warning message.
*
* @package Siteworxpro\App\Facades
*/
class Logger extends Facade
{
public static function getFacadeRoot(): RRLogger
{
if (self::$resolvedInstance !== null) {
$logger = self::resolveFacadeInstance(self::getFacadeAccessor());
if ($logger instanceof RRLogger) {
return $logger;
}
}
$rpc = RPC::create('tcp://127.0.0.1:6001');
return new RRLogger($rpc);
}
/**
* Get the registered name of the component.
*
* @return string The name of the component.
*/
protected static function getFacadeAccessor(): string
{
return RRLogger::class;
}
}

54
src/Grpc.php Normal file
View 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;
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\GrpcHandlers;
use GRPC\Greeter\GreeterInterface;
use GRPC\Greeter\HelloReply;
use GRPC\Greeter\HelloRequest;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\Services\Facades\CommandBus;
use Spiral\RoadRunner\GRPC;
class GreeterHandler implements GreeterInterface
{
public function SayHello(GRPC\ContextInterface $ctx, HelloRequest $in): HelloReply // phpcs:ignore
{
$command = new ExampleCommand($in->getName());
$reply = new HelloReply();
$reply->setMessage(CommandBus::handle($command));
return $reply;
}
}

View File

@@ -4,6 +4,10 @@ declare(strict_types=1);
namespace Siteworxpro\App\Helpers;
/**
* Class Env
* @package Siteworxpro\App\Helpers
*/
abstract class Env
{
/**

22
src/Helpers/Ulid.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Helpers;
/**
* Class Ulid
* @package Siteworxpro\App\Helpers
*/
class Ulid
{
/**
* Generate a ULID string
*
* @return string
*/
public static function generate(): string
{
return \Ulid\Ulid::generate()->getRandomness();
}
}

10
src/Helpers/Version.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Helpers;
class Version
{
public const string VERSION = 'dev-master';
}

View File

@@ -4,7 +4,9 @@ declare(strict_types=1);
namespace Siteworxpro\App\Http;
use Illuminate\Contracts\Support\Arrayable;
use Nyholm\Psr7\Response;
use Siteworxpro\HttpStatus\CodesEnum;
/**
* Class JsonResponseFactory
@@ -16,15 +18,21 @@ class JsonResponseFactory
/**
* Create a JSON response with the given data and status code.
*
* @param array $data The data to include in the response.
* @param int $statusCode The HTTP status code for the response.
* @param array|Arrayable $data The data to include in the response.
* @param CodesEnum $statusCode The HTTP status code for the response.
* @return Response The JSON response.
* @throws \JsonException
*/
public static function createJsonResponse(array $data, int $statusCode = 200): Response
{
public static function createJsonResponse(
array|Arrayable $data,
CodesEnum $statusCode = CodesEnum::OK
): Response {
if ($data instanceof Arrayable) {
$data = $data->toArray();
}
return new Response(
status: $statusCode,
status: $statusCode->value,
headers: [
'Content-Type' => 'application/json',
],

View File

@@ -9,7 +9,7 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Siteworxpro\App\Facades\Config;
use Siteworxpro\App\Services\Facades\Config;
/**
* Class CorsMiddleware

View File

@@ -0,0 +1,322 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Http\Middleware;
use Carbon\Carbon;
use Carbon\WrapperClock;
use GuzzleHttp\Exception\GuzzleException;
use Lcobucci\JWT\JwtFacade;
use Lcobucci\JWT\Signer\Hmac\Sha256 as Hmac256;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token\InvalidTokenStructure;
use Lcobucci\JWT\Validation\Constraint\IssuedBy;
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
use Lcobucci\JWT\Validation\Constraint\PermittedFor;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\StrictValidAt;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
use League\Route\Dispatcher;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Siteworxpro\App\Attributes\Guards\Jwt;
use Siteworxpro\App\Controllers\Controller;
use Siteworxpro\App\Http\JsonResponseFactory;
use Siteworxpro\App\Services\Facades\Config;
use Siteworxpro\App\Services\Facades\Guzzle;
use Siteworxpro\App\Services\Facades\Redis;
use Siteworxpro\HttpStatus\CodesEnum;
/**
* JWT authorization middleware.
*
* Applies JWT validation to controller actions annotated with `Jwt` attribute.
* Flow:
* - Resolve the targeted controller and method for the current route.
* - If the method has `Jwt`, read the `Authorization` header and parse the Bearer token.
* - Validate signature, time constraints, issuer\(\) and audience\(\) based on attribute and config.
* - On success, attach all token claims to the request as attributes.
* - On failure, return a 401 JSON response with validation errors.
*
* Configuration:
* - `jwt.signing_key`: key material or `file://` path to key.
* - `jwt.strict_validation`: bool toggling strict vs loose time validation.
*/
class JwtMiddleware extends Middleware
{
/**
* Process the incoming request.
*
* If the matched controller method is annotated with `Jwt`, validates the token and
* augments the request with claims on success. Otherwise, just delegates to the next handler.
*
* @param ServerRequestInterface $request PSR-7 request instance.
* @param RequestHandlerInterface|Dispatcher $handler Next middleware or route dispatcher.
*
* @return ResponseInterface Response produced by the next handler or a 401 JSON response.
*
* @throws \JsonException On JSON error response encoding issues.
* @throws \Exception On unexpected reflection or JWT parsing issues.
*/
public function process(
ServerRequestInterface $request,
RequestHandlerInterface|Dispatcher $handler
): ResponseInterface {
// Resolve the callable \[Controller, method] for the current route.
$callable = $this->extractRouteCallable($handler);
if ($callable === null) {
return $handler->handle($request);
}
/** @var Controller $class */
[$class, $method] = $callable;
if (class_exists($class::class)) {
$reflectionClass = new \ReflectionClass($class);
if ($reflectionClass->hasMethod($method)) {
$reflectionMethod = $reflectionClass->getMethod($method);
// Read `Jwt` attribute on the controller method.
$attributes = $reflectionMethod->getAttributes(Jwt::class);
// If no `Jwt` attribute, do not enforce auth here.
if (empty($attributes)) {
return $handler->handle($request);
}
// Extract Bearer token from Authorization header.
$token = str_replace('Bearer ', '', $request->getHeaderLine('Authorization'));
if (empty($token)) {
return JsonResponseFactory::createJsonResponse([
'status_code' => 401,
'message' => 'Unauthorized: Missing token',
], CodesEnum::UNAUTHORIZED);
}
// Aggregate required issuers and audience from attributes.
$requiredIssuers = [];
$requiredAudience = '';
foreach ($attributes as $attribute) {
/** @var Jwt $jwtInstance */
$jwtInstance = $attribute->newInstance();
if ($jwtInstance->getAudience() !== '') {
$requiredAudience = $jwtInstance->getAudience();
}
$requiredIssuers[] = $jwtInstance->getIssuer();
}
try {
// Parse and validate the token with signature, time, issuer and audience constraints.
$jwt = new JwtFacade()->parse(
$token,
$this->getSignedWith($token),
Config::get('jwt.strict_validation') ?
new StrictValidAt(new WrapperClock(Carbon::now())) :
new LooseValidAt(new WrapperClock(Carbon::now())),
new IssuedBy(...$requiredIssuers),
new PermittedFor($requiredAudience)
);
} catch (RequiredConstraintsViolated $exception) {
// Collect human-readable violations to return to the client.
$violations = [];
foreach ($exception->violations() as $violation) {
$violations[] = $violation->getMessage();
}
return JsonResponseFactory::createJsonResponse([
'status_code' => CodesEnum::UNAUTHORIZED->value,
'message' => 'Unauthorized: Invalid token',
'errors' => $violations
], CodesEnum::UNAUTHORIZED);
} catch (InvalidTokenStructure) {
// Token could not be parsed due to malformed structure.
return JsonResponseFactory::createJsonResponse([
'status_code' => CodesEnum::UNAUTHORIZED->value,
'message' => 'Unauthorized: Invalid token',
], 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.
foreach ($jwt->claims()->all() as $item => $value) {
$request = $request->withAttribute($item, $value);
}
}
}
return $handler->handle($request);
}
/**
* Build the signature validation constraint from configured key.
*
* - If the configured key content includes the string `PUBLIC KEY`, use RSA SHA-256.
* - Otherwise assume an HMAC SHA-256 shared secret.
* - Supports raw key strings or `file://` paths.
*
* @return SignedWith Signature constraint used during JWT parsing.
*
* @throws \RuntimeException When no signing key is configured.
* @throws \JsonException
*/
private function getSignedWith(string $token): SignedWith
{
$keyConfig = Config::get('jwt.signing_key');
if ($keyConfig === null) {
throw new \RuntimeException('JWT signing key is not configured.');
}
// file:// path to key
if (str_starts_with($keyConfig, 'file://')) {
$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 {
$key = InMemory::plainText($keyConfig);
}
// Heuristic: if PEM public key content is detected, use RSA; otherwise use HMAC.
if (str_contains($key->contents(), 'PUBLIC KEY')) {
return new SignedWith(new Sha256(), $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;
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Http\Middleware;
use League\Route\Dispatcher;
use League\Route\Route;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Base middleware helper for extracting route callables.
*
* This abstract middleware provides a utility method to inspect a League\Route
* dispatcher and obtain the underlying route callable as a [class, method] tuple.
*
* @package Siteworxpro\App\Http\Middleware
*/
abstract class Middleware implements MiddlewareInterface
{
/**
* Extract the route callable [class, method] from a League\Route dispatcher.
*
* When the provided handler is a League\Route\Dispatcher, this inspects its
* middleware stack, looks at the last segment (the resolved Route), and
* attempts to normalize its callable into a [class, method] pair.
*
* Supported callable forms:
* - array callable: [object|class-string, method-string]
* - string callable: "ClassName::methodName"
*
* Returns null when the handler is not a Dispatcher, the stack is empty,
* or the callable cannot be parsed.
*
* @param RequestHandlerInterface|Dispatcher $handler The downstream handler or dispatcher.
*
* @return array{0: class-string|object|null, 1: string|null}|null Tuple of [class|object, method] or null.
*/
protected function extractRouteCallable(
RequestHandlerInterface|Dispatcher $handler
): array|null {
// Only proceed if this is a League\Route dispatcher.
if (!$handler instanceof Dispatcher) {
return null;
}
/** @var Route | null $lastSegment */
// Retrieve the last middleware in the stack, which should be the Route.
$lastSegment = array_last($handler->getMiddlewareStack());
if ($lastSegment === null) {
return null;
}
// Obtain the callable associated with the route.
$callable = $lastSegment->getCallable();
$class = null;
$method = null;
// Handle array callable: [object|class-string, 'method']
if (is_array($callable) && count($callable) === 2) {
[$class, $method] = $callable;
} elseif (is_string($callable)) {
// Handle string callable: 'ClassName::methodName'
[$class, $method] = explode('::', $callable);
}
return [$class, $method];
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Http\Middleware;
use League\Route\Dispatcher;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Siteworxpro\App\Attributes\Guards\RequireAllScopes;
use Siteworxpro\App\Attributes\Guards\Scope;
use Siteworxpro\App\Controllers\Controller;
use Siteworxpro\App\Http\JsonResponseFactory;
use Siteworxpro\HttpStatus\CodesEnum;
/**
* Middleware that enforces scope-based access control on controller actions.
*
* It inspects PHP 8 attributes of type \`Scope\` applied to the resolved controller method,
* compares the required scopes with the user scopes provided on the request attribute \`scopes\`,
* and returns a 403 JSON response when any required scope is missing.
*
* If the route callable cannot be resolved, or no scope is required, the request is passed through.
*
* @see Scope
*/
class ScopeMiddleware extends Middleware
{
/**
* Resolve the route callable, read any \`Scope\` attributes, and enforce required scopes.
*
* Expected user scopes are provided on the request under the attribute name \`scopes\`
* as an array of strings.
*
* @param ServerRequestInterface $request Incoming PSR-7 request (expects \`scopes\` attribute).
* @param RequestHandlerInterface|Dispatcher $handler Next handler or League\Route dispatcher.
*
* @return ResponseInterface A 403 JSON response when scopes are insufficient; otherwise the handler response.
*
* @throws \JsonException If encoding the JSON error response fails.
* @throws \ReflectionException If reflection on the controller or method fails.
*/
public function process(
ServerRequestInterface $request,
RequestHandlerInterface|Dispatcher $handler
): ResponseInterface {
// Attempt to resolve the route's callable [Controller instance, method name].
$callable = $this->extractRouteCallable($handler);
if ($callable === null) {
// If no callable is available, delegate to the next handler.
return $handler->handle($request);
}
/** @var Controller $class Controller instance resolved from the route. */
[$class, $method] = $callable;
// Ensure the controller exists and the method is defined before reflecting.
if (class_exists($class::class)) {
$reflectionClass = new \ReflectionClass($class);
if ($reflectionClass->hasMethod($method)) {
$reflectionMethod = $reflectionClass->getMethod($method);
// Fetch all Scope attributes declared on the method.
$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) {
/** @var Scope $scopeInstance Concrete Scope attribute instance. */
$scopeInstance = $attribute->newInstance();
$requiredScopes = array_merge($requiredScopes, $scopeInstance->getScopes());
// If any attribute requires all scopes, set the flag.
$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.
if (
(!$requireAll && array_intersect($userScopes, $requiredScopes) === []) ||
($requireAll && array_diff($requiredScopes, $userScopes) !== [])
) {
return JsonResponseFactory::createJsonResponse([
'error' => 'insufficient_scope',
'error_description' =>
'The request requires higher privileges than provided by the access token.'
], CodesEnum::FORBIDDEN);
}
}
}
// All checks passed; continue down the middleware pipeline.
return $handler->handle($request);
}
}

View File

@@ -0,0 +1,29 @@
<?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.'),
]
)]
readonly class GenericResponse implements Arrayable
{
public function __construct(
private string $message = '',
) {
}
public function toArray(): array
{
return [
'message' => $this->message,
];
}
}

View File

@@ -0,0 +1,38 @@
<?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: '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 [
'message' => 'The requested resource ' . $this->uri . ' was not found.',
'context' => $this->context,
];
}
}

View 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: '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 [
'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 [
'code' => $this->e->getCode() != 0 ?
$this->e->getCode() :
CodesEnum::INTERNAL_SERVER_ERROR->value,
'message' => 'An internal server error occurred.',
];
}
}

97
src/Kernel.php Normal file
View File

@@ -0,0 +1,97 @@
<?php
namespace Siteworxpro\App;
use Illuminate\Container\Container;
use Illuminate\Database\Capsule\Manager;
use Illuminate\Support\ServiceProvider;
use Siteworx\Config\Config as SWConfig;
use Siteworxpro\App\Services\Facade;
use Siteworxpro\App\Services\Facades\Config;
use Siteworxpro\App\Services\Facades\Dispatcher;
use Siteworxpro\App\Services\ServiceProviders\BrokerServiceProvider;
use Siteworxpro\App\Services\ServiceProviders\CommandBusProvider;
use Siteworxpro\App\Services\ServiceProviders\DispatcherServiceProvider;
use Siteworxpro\App\Services\ServiceProviders\LoggerServiceProvider;
use Siteworxpro\App\Services\ServiceProviders\RedisServiceProvider;
/**
* Class Kernel
*
* The Kernel class is responsible for bootstrapping the application by
* initializing service providers and setting up the database connection.
*
* @package Siteworxpro\App
*/
class Kernel
{
/**
* List of service providers to be registered during bootstrapping.
*
* @var array
*/
private static array $serviceProviders = [
LoggerServiceProvider::class,
RedisServiceProvider::class,
DispatcherServiceProvider::class,
BrokerServiceProvider::class,
CommandBusProvider::class,
];
/**
* Bootstraps the server by initializing the PSR-7 worker and router.
*
* This method sets up the PSR-7 worker and router instances, and registers
* the routes for the server. It should be called in the constructor of
* subclasses to ensure proper initialization.
*
* @return void
* @throws \ReflectionException
*/
public static function boot(): void
{
$container = new Container();
Facade::setFacadeContainer($container);
// Bind the container to the Config facade first so that it can be used by service providers
$container->bind(SWConfig::class, function () {
return SWConfig::load(__DIR__ . '/../config.php');
});
foreach (self::$serviceProviders as $serviceProvider) {
if (class_exists($serviceProvider)) {
$provider = new $serviceProvider($container);
if ($provider instanceof ServiceProvider) {
$provider->register();
} else {
throw new \RuntimeException(sprintf(
'Service provider %s is not an instance of ServiceProvider.',
$serviceProvider
));
}
} else {
throw new \RuntimeException(sprintf('Service provider %s not found.', $serviceProvider));
}
}
self::bootModelCapsule();
}
/**
* Bootstraps the model capsule for database connections.
*
* This method sets up the database connection using the Eloquent ORM.
* It retrieves the database configuration from the Config facade and
* initializes the Eloquent capsule manager.
*
* @return void
*/
private static function bootModelCapsule(): void
{
$capsule = new Manager();
$capsule->setEventDispatcher(Dispatcher::getFacadeRoot());
$capsule->addConnection(Config::get('db'));
$capsule->setAsGlobal();
$capsule->bootEloquent();
}
}

236
src/Log/Logger.php Normal file
View File

@@ -0,0 +1,236 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Log;
use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\StreamHandler;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use RoadRunner\Logger\Logger as RRLogger;
use Siteworxpro\App\Services\Facades\RoadRunnerLogger;
/**
* Logger implementation that conforms to PSR-3 (`Psr\Log\LoggerInterface`).
*
* Behavior:
* - If environment indicates RoadRunner RPC (`$_SERVER['RR_RPC']`), logs are forwarded
* to a RoadRunner RPC logger (`RoadRunner\Logger\Logger`) created via Goridge RPC.
* - Otherwise, logs are written to `php://stdout` using Monolog with a JSON formatter.
* - Messages below the configured threshold are ignored (level filtering).
*
* Supported PSR-3 levels are mapped to an internal numeric ordering in `$levels`.
* When using the RPC logger, levels are translated to the respective RPC methods
* (debug, info, warning, error). When using Monolog, the numeric mapping is used
* as the numeric level passed to Monolog's `log` method.
*/
class Logger implements LoggerInterface
{
/**
* RoadRunner RPC logger instance when running under RoadRunner.
*
* @var RRLogger | LoggerInterface | null
*/
private RRLogger | LoggerInterface | null $rpcLogger = null;
/**
* Monolog logger used as a fallback to write JSON-formatted logs to stdout.
*
* @var \Monolog\Logger
*/
private \Monolog\Logger $monologLogger;
/**
* Numeric ordering for PSR-3 log levels.
*
* Lower numbers represent higher severity. This mapping is used for filtering
* messages according to the configured minimum level and for Monolog numeric level.
*
* @var array<string,int>
*/
private array $levels = [
LogLevel::EMERGENCY => 0,
LogLevel::ALERT => 1,
LogLevel::CRITICAL => 2,
LogLevel::ERROR => 3,
LogLevel::WARNING => 4,
LogLevel::NOTICE => 5,
LogLevel::INFO => 6,
LogLevel::DEBUG => 7,
];
/**
* Create a new Logger.
*
* @param string $level Minimum level to log (PSR-3 level string). Messages with
* a higher numeric value in `$levels` will be ignored.
*
* @param resource | null $streamOutput Optional stream handler for Monolog.
*
* The default is `LogLevel::DEBUG` (log everything).
*
* If `$_SERVER['RR_RPC']` is set, an RPC connection will be attempted at
* $_SERVER['RR_RPC'] and a RoadRunner RPC logger will be used.
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function __construct(
private readonly string $level = LogLevel::DEBUG,
$streamOutput = null,
) {
if (isset($_SERVER['RR_RPC'])) {
$this->rpcLogger = RoadRunnerLogger::getFacadeRoot();
}
$this->monologLogger = new \Monolog\Logger('app_logger');
$formatter = new JsonFormatter();
$stream = $streamOutput ?? 'php://stdout';
$this->monologLogger->pushHandler(new StreamHandler($stream)->setFormatter($formatter));
}
/**
* System is unusable.
*
* @param \Stringable|string $message
* @param array $context
*/
public function emergency(\Stringable|string $message, array $context = []): void
{
$this->log(LogLevel::EMERGENCY, $message, $context);
}
/**
* Action must be taken immediately.
*
* @param \Stringable|string $message
* @param array $context
*/
public function alert(\Stringable|string $message, array $context = []): void
{
$this->log(LogLevel::ALERT, $message, $context);
}
/**
* Critical conditions.
*
* @param \Stringable|string $message
* @param array $context
*/
public function critical(\Stringable|string $message, array $context = []): void
{
$this->log(LogLevel::CRITICAL, $message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically be logged and monitored.
*
* @param \Stringable|string $message
* @param array $context
*/
public function error(\Stringable|string $message, array $context = []): void
{
$this->log(LogLevel::ERROR, $message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* @param \Stringable|string $message
* @param array $context
*/
public function warning(\Stringable|string $message, array $context = []): void
{
$this->log(LogLevel::WARNING, $message, $context);
}
/**
* Normal but significant events.
*
* @param \Stringable|string $message
* @param array $context
*/
public function notice(\Stringable|string $message, array $context = []): void
{
$this->log(LogLevel::NOTICE, $message, $context);
}
/**
* Interesting events.
*
* @param \Stringable|string $message
* @param array $context
*/
public function info(\Stringable|string $message, array $context = []): void
{
$this->log(LogLevel::INFO, $message, $context);
}
/**
* Detailed debug information.
*
* @param \Stringable|string $message
* @param array $context
*/
public function debug(\Stringable|string $message, array $context = []): void
{
$this->log(LogLevel::DEBUG, $message, $context);
}
/**
* Logs with an arbitrary level.
*
* Behavior details:
* - If the provided `$level` maps to a numeric value greater than the configured
* minimum level, the message is discarded (filtered).
* - If an RPC logger is available, the message is forwarded to the RPC logger
* using a method chosen by level (debug, info, warning, error).
* - Otherwise, the message is written to Monolog using the numeric mapping.
*
* Notes:
* - `$level` should be a PSR-3 level string (values defined in `Psr\Log\LogLevel`).
* - If an unknown level string is passed, accessing `$this->levels[$level]` may
* trigger a PHP notice or undefined index. Ensure callers use valid PSR-3 levels.
*
* @param mixed $level PSR-3 log level (string)
* @param \Stringable|string $message
* @param array $context
*/
public function log($level, \Stringable|string $message, array $context = []): void
{
if (isset($this->levels[$level]) && $this->levels[$level] > $this->levels[$this->level]) {
return;
}
if ($this->rpcLogger) {
switch ($level) {
case LogLevel::DEBUG:
$this->rpcLogger->debug((string)$message, $context);
break;
case LogLevel::NOTICE:
case LogLevel::INFO:
$this->rpcLogger->info((string)$message, $context);
break;
case LogLevel::WARNING:
$this->rpcLogger->warning((string)$message, $context);
break;
case LogLevel::CRITICAL:
case LogLevel::ERROR:
case LogLevel::ALERT:
case LogLevel::EMERGENCY:
$this->rpcLogger->error((string)$message, $context);
break;
default:
$this->rpcLogger->log($level, (string)$message, $context);
break;
}
return;
}
$this->monologLogger->log($this->levels[$level], (string)$message, $context);
}
}

View File

@@ -6,6 +6,12 @@ namespace Siteworxpro\App\Models;
use Illuminate\Database\Eloquent\Model as ORM;
/**
* Class Model
*
* @package Siteworxpro\App\Models
*/
abstract class Model extends ORM
{
protected $dateFormat = 'Y-m-d H:i:s';
}

77
src/Models/User.php Normal file
View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Models;
use Carbon\Carbon;
use OpenApi\Attributes as OA;
use Siteworxpro\App\Helpers\Ulid;
/**
* Class User
*
* @property-read string $id
* @property string $first_name
* @property string $last_name
* @property string $email
* @property string $password
* @property Carbon $created_at
*
* @property-read string $full_name
* @property-read string $formatted_email
*/
#[OA\Schema(
schema: "User",
properties: [
new OA\Property(
property: "id",
description: "Unique identifier for the user",
type: "string",
format: "ulid",
readOnly: true,
example: '01KBD5WPZKYD77BYM2QD9NKG99'
),
new OA\Property(property: "first_name", type: "string"),
new OA\Property(property: "last_name", type: "string"),
new OA\Property(property: "email", type: "string", format: "email"),
new OA\Property(property: "created_at", type: "string", format: "date-time"),
]
)]
class User extends Model
{
protected $casts = [
'created_at' => 'datetime',
];
protected $hidden = [
'password',
];
protected $fillable = [
'first_name',
'last_name',
'email',
'password',
];
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->attributes['id'] = $this->attributes['id'] ?? Ulid::generate();
}
public function getFullNameAttribute(): string
{
return "$this->first_name $this->last_name";
}
public function getFormattedEmailAttribute(): string
{
return sprintf(
'%s <%s>',
$this->getFullNameAttribute(),
strtolower($this->email)
);
}
}

278
src/Services/Facade.php Normal file
View File

@@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Services;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\HigherOrderTapProxy;
use Illuminate\Support\Testing\Fakes\Fake;
use Mockery;
use Mockery\Expectation;
use Mockery\ExpectationInterface;
use Mockery\LegacyMockInterface;
use Mockery\MockInterface;
class Facade
{
/**
* The application instance being facaded.
*
* @var Container | null
*/
protected static ?Container $container = null;
/**
* The resolved object instances.
*
* @var array
*/
protected static array $resolvedInstance;
/**
* Indicates if the resolved instance should be cached.
*
* @var bool
*/
protected static bool $cached = true;
/**
* Run a Closure when the facade has been resolved.
*
* @param \Closure $callback
* @return void
*/
public static function resolved(\Closure $callback): void
{
$accessor = static::getFacadeAccessor();
if (static::$container->resolved($accessor) === true) {
$callback(static::getFacadeRoot(), static::$container);
}
static::$container->afterResolving($accessor, function ($service, $app) use ($callback) {
$callback($service, $app);
});
}
/**
* Initiate a partial mock on the facade.
*
* @return MockInterface
*/
public static function partialMock(): MockInterface
{
$name = static::getFacadeAccessor();
$mock = static::isMock()
? static::$resolvedInstance[$name]
: static::createFreshMockInstance();
return $mock->makePartial();
}
/**
* Initiate a mock expectation on the facade.
*
* @return Expectation|ExpectationInterface
*/
public static function shouldReceive(): Mockery\Expectation | Mockery\ExpectationInterface
{
$name = static::getFacadeAccessor();
$mock = static::isMock()
? static::$resolvedInstance[$name]
: static::createFreshMockInstance();
return $mock->shouldReceive(...func_get_args());
}
/**
* Initiate a mock expectation on the facade.
*
* @return Expectation|ExpectationInterface
*/
public static function expects(): Mockery\Expectation | Mockery\ExpectationInterface
{
$name = static::getFacadeAccessor();
$mock = static::isMock()
? static::$resolvedInstance[$name]
: static::createFreshMockInstance();
return $mock->expects(...func_get_args());
}
/**
* Create a fresh mock instance for the given class.
*
* @return MockInterface|LegacyMockInterface
*/
protected static function createFreshMockInstance(): MockInterface | LegacyMockInterface
{
return tap(static::createMock(), function ($mock) {
static::swap($mock);
$mock->shouldAllowMockingProtectedMethods();
});
}
/**
* Create a fresh mock instance for the given class.
*
* @return MockInterface
*/
protected static function createMock(): MockInterface
{
$class = static::getMockableClass();
return $class ? Mockery::mock($class) : Mockery::mock();
}
/**
* Determines whether a mock is set as the instance of the facade.
*
* @return bool
*/
protected static function isMock(): bool
{
$name = static::getFacadeAccessor();
return isset(static::$resolvedInstance[$name]) &&
static::$resolvedInstance[$name] instanceof LegacyMockInterface;
}
/**
* Get the mockable class for the bound instance.
*
* @return string|null
*/
protected static function getMockableClass(): ?string
{
if ($root = static::getFacadeRoot()) {
return get_class($root);
}
return null;
}
/**
* Hotswap the underlying instance behind the facade.
*
* @param mixed $instance
* @return void
*/
public static function swap(mixed $instance): void
{
static::$resolvedInstance[static::getFacadeAccessor()] = $instance;
if (isset(static::$container)) {
static::$container->instance(static::getFacadeAccessor(), $instance);
}
}
/**
* Get the root object behind the facade.
*
* @return mixed
*/
public static function getFacadeRoot(): mixed
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
/**
* Get the registered name of the component.
*
* @return string
*
* @throws \RuntimeException
*/
protected static function getFacadeAccessor(): string
{
throw new \RuntimeException('Facade does not implement getFacadeAccessor method.');
}
/**
* Resolve the facade root instance from the container.
*
* @param string $name
* @return mixed
*/
protected static function resolveFacadeInstance(string $name): mixed
{
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
if (static::$container) {
if (static::$cached) {
return static::$resolvedInstance[$name] = static::$container[$name];
}
return static::$container[$name];
}
return null;
}
/**
* Clear a resolved facade instance.
*
* @param string $name
* @return void
*/
public static function clearResolvedInstance(string $name): void
{
unset(static::$resolvedInstance[$name]);
}
/**
* Clear all of the resolved instances.
*
* @return void
*/
public static function clearResolvedInstances(): void
{
static::$resolvedInstance = [];
}
/**
* Get the application instance behind the facade.
*/
public static function getFacadeContainer(): ?Container
{
return static::$container;
}
/**
* Set the application instance.
*
* @param Container | null $container
* @return void
*/
public static function setFacadeContainer(Container | null $container): void
{
static::$container = $container;
}
/**
* Handle dynamic, static calls to the object.
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws \RuntimeException
*/
public static function __callStatic(string $method, array $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new \RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
}

Some files were not shown because too many files have changed in this diff Show More