diff --git a/.gitignore b/.gitignore
index 604d834..a75f195 100644
--- a/.gitignore
+++ b/.gitignore
@@ -240,3 +240,4 @@ gradle-app.setting
hs_err_pid*
replay_pid*
+reference-*
\ No newline at end of file
diff --git a/README.md b/README.md
index 7031ed9..4b73c0d 100644
--- a/README.md
+++ b/README.md
@@ -128,4 +128,4 @@ Here's a summary of example API endpoints:
| `/items/{id}` | PUT | Update item details |
| `/items/{id}` | DELETE | Delete item |
-Suggested base URL is `http://localhost:8080/api/v1/`.
\ No newline at end of file
+Suggested base URL is `http://localhost:50080/api/v1/`.
\ No newline at end of file
diff --git a/cpp17/app/src/App.cpp b/cpp17/app/src/App.cpp
index 9ec7ffb..1d68f7d 100644
--- a/cpp17/app/src/App.cpp
+++ b/cpp17/app/src/App.cpp
@@ -26,7 +26,7 @@ App::App(int argc, char** argv)
AutoStore::Config{
.dataPath = os::getApplicationDirectory() + "/data",
.host = "0.0.0.0",
- .port = 8080,
+ .port = 50080,
},
logger);
diff --git a/cpp17/docker/docker-compose.yml b/cpp17/docker/docker-compose.yml
index c1d34f2..422387c 100644
--- a/cpp17/docker/docker-compose.yml
+++ b/cpp17/docker/docker-compose.yml
@@ -7,4 +7,4 @@ services:
image: autostore-build-cpp-vcpkg-img
container_name: autostore-build-cpp-vcpkg
ports:
- - 8080:8080
+ - 50080:50080
diff --git a/cpp17/lib/include/autostore/AutoStore.h b/cpp17/lib/include/autostore/AutoStore.h
index b5ab753..80162b9 100644
--- a/cpp17/lib/include/autostore/AutoStore.h
+++ b/cpp17/lib/include/autostore/AutoStore.h
@@ -38,7 +38,7 @@ public:
{
std::string dataPath;
std::string host{"0.0.0.0"};
- uint16_t port{8080};
+ uint16_t port{50080};
};
AutoStore(Config config, ILoggerPtr logger);
diff --git a/cpp17/lib/src/infrastructure/http/HttpServer.h b/cpp17/lib/src/infrastructure/http/HttpServer.h
index f47d76e..cd323df 100644
--- a/cpp17/lib/src/infrastructure/http/HttpServer.h
+++ b/cpp17/lib/src/infrastructure/http/HttpServer.h
@@ -15,7 +15,7 @@ public:
HttpServer(ILoggerPtr logger, application::IAuthService& authService);
~HttpServer();
- bool start(int port = 8080, std::string_view host = "0.0.0.0");
+ bool start(int port = 50080, std::string_view host = "0.0.0.0");
void stop();
bool isRunning() const;
diff --git a/openapi.yaml b/openapi.yaml
index a93f4a1..dfa5e70 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -153,54 +153,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
- put:
- summary: Update an item
- description: Updates an existing item
- security:
- - bearerAuth: []
- parameters:
- - name: id
- in: path
- required: true
- description: Item ID
- schema:
- type: string
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ItemInput'
- responses:
- '200':
- description: Item updated successfully
- content:
- application/json:
- schema:
- allOf:
- - $ref: '#/components/schemas/JsendSuccess'
- - type: object
- properties:
- data:
- $ref: '#/components/schemas/Item'
- '400':
- description: Invalid input
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/JsendError'
- '401':
- description: Unauthorized
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/JsendError'
- '404':
- description: Item not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/JsendError'
delete:
summary: Delete an item
description: Deletes an existing item
diff --git a/php8/.devcontainer/Dockerfile b/php8/.devcontainer/Dockerfile
new file mode 100755
index 0000000..5e33ca9
--- /dev/null
+++ b/php8/.devcontainer/Dockerfile
@@ -0,0 +1,70 @@
+FROM php:8.2-fpm-alpine
+
+# Install system dependencies and development tools
+RUN apk add --no-cache \
+ $PHPIZE_DEPS \
+ icu-dev \
+ libzip-dev \
+ libpng-dev \
+ jpeg-dev \
+ freetype-dev \
+ linux-headers \
+ git \
+ vim \
+ curl \
+ shadow \
+ sudo
+
+RUN pecl install xdebug && docker-php-ext-enable xdebug
+
+RUN apk add icu-dev
+RUN docker-php-ext-install \
+ intl \
+ pdo_mysql \
+ zip \
+ gd
+
+# Configure PHP for development, xdebug.client_host=127.0.0.1 for in-container xdebug server
+RUN echo "memory_limit = 512M" > /usr/local/etc/php/conf.d/custom.ini \
+ && echo "upload_max_filesize = 100M" >> /usr/local/etc/php/conf.d/custom.ini \
+ && echo "post_max_size = 100M" >> /usr/local/etc/php/conf.d/custom.ini \
+ && echo "max_execution_time = 300" >> /usr/local/etc/php/conf.d/custom.ini \
+ && echo "display_errors = On" >> /usr/local/etc/php/conf.d/custom.ini \
+ && echo "display_startup_errors = On" >> /usr/local/etc/php/conf.d/custom.ini \
+ && echo "error_reporting = E_ALL" >> /usr/local/etc/php/conf.d/custom.ini \
+ && echo "xdebug.mode=debug,develop" >> /usr/local/etc/php/conf.d/custom.ini \
+ && echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/custom.ini \
+ && echo "xdebug.client_host=127.0.0.1" >> /usr/local/etc/php/conf.d/custom.ini \
+ && echo "xdebug.client_port=9003" >> /usr/local/etc/php/conf.d/custom.ini \
+ && echo "xdebug.idekey=VSCODE" >> /usr/local/etc/php/conf.d/custom.ini
+
+# Set working directory
+WORKDIR /var/www/html
+
+# Install Composer
+COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
+
+# Configure user permissions
+ARG USER_ID=1000
+ARG GROUP_ID=1000
+
+# Create a user with matching UID/GID
+RUN if getent passwd $USER_ID > /dev/null 2>&1; then \
+ usermod -u $USER_ID -g $GROUP_ID www-data; \
+ else \
+ addgroup -g $GROUP_ID developer; \
+ adduser -D -u $USER_ID -G developer -s /bin/sh developer; \
+ echo '%developer ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers.d/developer; \
+ chmod 0440 /etc/sudoers.d/developer; \
+ usermod -a -G developer www-data; \
+ fi
+
+RUN chown -R $USER_ID:$GROUP_ID /var/www/html
+
+USER $USER_ID:$GROUP_ID
+
+# Expose port 9000 for PHP-FPM
+EXPOSE 9000
+
+# Start PHP-FPM
+CMD ["php-fpm"]
\ No newline at end of file
diff --git a/php8/.devcontainer/default.dev.conf b/php8/.devcontainer/default.dev.conf
new file mode 100755
index 0000000..0b8f8a4
--- /dev/null
+++ b/php8/.devcontainer/default.dev.conf
@@ -0,0 +1,28 @@
+server {
+ listen 80;
+ server_name localhost;
+ root /var/www/html;
+ index index.php index.html;
+
+ location / {
+ try_files $uri $uri/ /index.php?$query_string;
+ }
+
+ location ~ \.php$ {
+ try_files $uri =404;
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
+ fastcgi_pass php:9000;
+ fastcgi_index index.php;
+ include fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+ fastcgi_param PATH_INFO $fastcgi_path_info;
+ }
+
+ location ~ /\.(?!well-known).* {
+ deny all;
+ }
+
+ # Enable access log for debugging in development
+ access_log /var/log/nginx/access.log;
+ error_log /var/log/nginx/error.log warn;
+}
\ No newline at end of file
diff --git a/php8/.devcontainer/devcontainer.json b/php8/.devcontainer/devcontainer.json
new file mode 100755
index 0000000..1e00e99
--- /dev/null
+++ b/php8/.devcontainer/devcontainer.json
@@ -0,0 +1,29 @@
+{
+ "name": "PHP 8.2 dev container",
+ "dockerComposeFile": "./docker-compose.yml",
+ "service": "php",
+ "workspaceFolder": "/var/www/html",
+ "customizations": {
+ "vscode": {
+ "settings": {
+ "terminal.integrated.defaultProfile.linux": "bash",
+ "php.validate.executablePath": "/usr/local/bin/php",
+ "php.debug.executablePath": "/usr/local/bin/php"
+ },
+ "extensions": [
+ "xdebug.php-debug",
+ "bmewburn.vscode-intelephense-client",
+ "ms-vscode.vscode-json",
+ "mrmlnc.vscode-json5",
+ "mrmlnc.vscode-json2"
+ ]
+ }
+ },
+ "forwardPorts": [50080],
+ "remoteUser": "developer",
+ "containerEnv": {
+ "USER_ID": "${localEnv:USER_ID:-1000}",
+ "GROUP_ID": "${localEnv:GROUP_ID:-1000}"
+ },
+ "postCreateCommand": "sudo chown -R developer:developer /var/www/html"
+}
\ No newline at end of file
diff --git a/php8/.devcontainer/docker-compose.yml b/php8/.devcontainer/docker-compose.yml
new file mode 100755
index 0000000..135cb49
--- /dev/null
+++ b/php8/.devcontainer/docker-compose.yml
@@ -0,0 +1,42 @@
+version: "3.9"
+services:
+ php:
+ build:
+ context: ..
+ dockerfile: .devcontainer/Dockerfile
+ args:
+ USER_ID: ${USER_ID:-1000}
+ GROUP_ID: ${GROUP_ID:-1000}
+ image: dev-php82-img
+ container_name: dev-php82
+ user: "developer"
+ volumes:
+ - ../:/var/www/html:cached
+ - composer-cache:/home/www-data/.composer/cache
+ environment:
+ PHP_IDE_CONFIG: serverName=localhost
+ XDEBUG_MODE: debug,develop
+ networks:
+ - dev-network
+
+ nginx:
+ build:
+ context: .
+ dockerfile: nginx.Dockerfile
+ image: dev-php82-nginx-img
+ container_name: dev-php82-nginx
+ ports:
+ - "50080:80"
+ volumes:
+ - ../:/var/www/html:cached
+ depends_on:
+ - php
+ networks:
+ - dev-network
+
+volumes:
+ composer-cache:
+
+networks:
+ dev-network:
+ driver: bridge
\ No newline at end of file
diff --git a/php8/.devcontainer/nginx.Dockerfile b/php8/.devcontainer/nginx.Dockerfile
new file mode 100755
index 0000000..092bf5b
--- /dev/null
+++ b/php8/.devcontainer/nginx.Dockerfile
@@ -0,0 +1,7 @@
+FROM nginx:alpine
+
+COPY default.dev.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/php8/.dockerignore b/php8/.dockerignore
new file mode 100755
index 0000000..81ed2fd
--- /dev/null
+++ b/php8/.dockerignore
@@ -0,0 +1,19 @@
+.git
+.github
+.vscode
+.phpunit.result.cache
+.php_cs.cache
+node_modules
+npm-debug.log
+yarn-error.log
+.env
+.env.backup
+.env.*
+!.env.example
+.DS_Store
+Thumbs.db
+*.log
+*.zip
+*.tar.gz
+.docker
+.devcontainer
\ No newline at end of file
diff --git a/php8/cli/scheduler-tasks/cleanup-logs.3600.php b/php8/cli/scheduler-tasks/cleanup-logs.3600.php
new file mode 100755
index 0000000..985448e
--- /dev/null
+++ b/php8/cli/scheduler-tasks/cleanup-logs.3600.php
@@ -0,0 +1,29 @@
+get(LoggerInterface::class);
+
+try {
+ // Example log cleanup task
+ $logFile = $storagePath . '/app.log';
+
+ if (file_exists($logFile) && filesize($logFile) > 10 * 1024 * 1024) { // 10MB
+ // Rotate log file
+ $backupFile = $storagePath . '/app.log.' . date('Y-m-d_H-i-s');
+ rename($logFile, $backupFile);
+ $logger->info("Log file rotated to: {$backupFile}");
+ }
+
+ $logger->info('Log cleanup check completed');
+ exit(0);
+} catch (\Exception $e) {
+ $logger->error('Error during log cleanup: ' . $e->getMessage());
+ exit(1);
+}
\ No newline at end of file
diff --git a/php8/cli/scheduler-tasks/handle-expired-items.60.php b/php8/cli/scheduler-tasks/handle-expired-items.60.php
new file mode 100755
index 0000000..2e74dcf
--- /dev/null
+++ b/php8/cli/scheduler-tasks/handle-expired-items.60.php
@@ -0,0 +1,23 @@
+get(LoggerInterface::class);
+
+try {
+ $handleExpiredItems = $diContainer->get(HandleExpiredItems::class);
+ $handleExpiredItems->execute();
+
+ $logger->info('Expired items check completed successfully');
+ exit(0);
+} catch (\Exception $e) {
+ $logger->error('Error handling expired items: ' . $e->getMessage());
+ exit(1);
+}
\ No newline at end of file
diff --git a/php8/cli/scheduler.php b/php8/cli/scheduler.php
new file mode 100755
index 0000000..283d92a
--- /dev/null
+++ b/php8/cli/scheduler.php
@@ -0,0 +1,131 @@
+tasksDirectory = $tasksDirectory;
+ $this->logger = $logger;
+ $this->loadTasks();
+ }
+
+ private function loadTasks(): void
+ {
+ if (!is_dir($this->tasksDirectory)) {
+ $this->logger->error("Tasks directory not found: {$this->tasksDirectory}");
+ return;
+ }
+
+ $files = scandir($this->tasksDirectory);
+ foreach ($files as $file) {
+ if ($file === '.' || $file === '..') {
+ continue;
+ }
+
+ $filePath = $this->tasksDirectory . '/' . $file;
+ if (!is_file($filePath)) {
+ continue;
+ }
+
+ // Parse interval from filename (e.g., "task.60.php" -> 60 seconds)
+ if (preg_match('/^(.+)\.(\d+)\.php$/', $file, $matches)) {
+ $taskName = $matches[1];
+ $interval = (int) $matches[2];
+
+ $this->tasks[] = [
+ 'name' => $taskName,
+ 'file' => $filePath,
+ 'interval' => $interval,
+ 'last_run' => null,
+ ];
+
+ $this->logger->info("Loaded task: {$taskName} with interval {$interval} seconds");
+ }
+ }
+ }
+
+ public function run(): void
+ {
+ $this->logger->info('Scheduler started');
+
+ // Install signal handlers for graceful shutdown
+ pcntl_async_signals(true);
+ pcntl_signal(SIGTERM, [$this, 'handleSignal']);
+ pcntl_signal(SIGINT, [$this, 'handleSignal']);
+
+ while ($this->running) {
+ $this->executeDueTasks();
+ sleep(1); // Check every second
+ }
+
+ $this->logger->info('Scheduler stopped');
+ }
+
+ private function executeDueTasks(): void
+ {
+ $currentTime = time();
+
+ foreach ($this->tasks as &$task) {
+ if ($task['last_run'] === null || ($currentTime - $task['last_run']) >= $task['interval']) {
+ $this->executeTask($task);
+ $task['last_run'] = $currentTime;
+ }
+ }
+ }
+
+ private function executeTask(array $task): void
+ {
+ $this->logger->info("Executing task: {$task['name']}");
+
+ $command = sprintf('php %s > /dev/null 2>&1 &', escapeshellarg($task['file']));
+ exec($command, $output, $exitCode);
+
+ if ($exitCode === 0) {
+ $this->logger->info("Task {$task['name']} executed successfully");
+ } else {
+ $this->logger->error("Task {$task['name']} failed with exit code: {$exitCode}");
+ }
+ }
+
+ public function handleSignal(int $signal): void
+ {
+ $this->logger->info("Received signal: {$signal}. Shutting down gracefully...");
+ $this->running = false;
+ }
+}
+
+// Main execution
+try {
+ $diContainer = new DiContainer();
+ $logger = $diContainer->get(LoggerInterface::class);
+ $tasksDirectory = __DIR__ . '/scheduler-tasks';
+
+ $scheduler = new Scheduler($tasksDirectory, $logger);
+ $scheduler->run();
+
+ exit(0);
+} catch (\Exception $e) {
+ $logger = $logger ?? new \Monolog\Logger('scheduler');
+ $logger->error('Scheduler failed: ' . $e->getMessage());
+ exit(1);
+}
diff --git a/php8/composer.json b/php8/composer.json
new file mode 100755
index 0000000..b92493f
--- /dev/null
+++ b/php8/composer.json
@@ -0,0 +1,41 @@
+{
+ "name": "autostore/php8-implementation",
+ "description": "PHP 8.2 implementation of AutoStore application following Clean Architecture",
+ "type": "project",
+ "license": "MIT",
+ "require": {
+ "php": ">=8.1",
+ "firebase/php-jwt": "^6.10",
+ "guzzlehttp/guzzle": "^7.8",
+ "psr/container": "^2.0",
+ "psr/http-message": "^2.0",
+ "psr/http-server-handler": "^1.0",
+ "psr/http-server-middleware": "^1.0",
+ "monolog/monolog": "^3.5",
+ "vlucas/phpdotenv": "^5.6",
+ "slim/slim": "^4.12",
+ "slim/psr7": "^1.6",
+ "league/container": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.5",
+ "phpstan/phpstan": "^1.10",
+ "squizlabs/php_codesniffer": "^3.8"
+ },
+ "autoload": {
+ "psr-4": {
+ "AutoStore\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "AutoStore\\Tests\\": "tests/"
+ }
+ },
+ "scripts": {
+ "test": "phpunit",
+ "phpstan": "phpstan analyse src tests",
+ "cs-check": "phpcs src tests --standard=PSR12",
+ "cs-fix": "phpcbf src tests --standard=PSR12"
+ }
+}
\ No newline at end of file
diff --git a/php8/composer.lock b/php8/composer.lock
new file mode 100755
index 0000000..91cef88
--- /dev/null
+++ b/php8/composer.lock
@@ -0,0 +1,3646 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "37b5717a836e31bfd6a117b74c9dd949",
+ "packages": [
+ {
+ "name": "fig/http-message-util",
+ "version": "1.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message-util.git",
+ "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765",
+ "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3 || ^7.0 || ^8.0"
+ },
+ "suggest": {
+ "psr/http-message": "The package containing the PSR-7 interfaces"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Fig\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Utility classes and constants for use with PSR-7 (psr/http-message)",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/http-message-util/issues",
+ "source": "https://github.com/php-fig/http-message-util/tree/1.1.5"
+ },
+ "time": "2020-11-24T22:02:12+00:00"
+ },
+ {
+ "name": "firebase/php-jwt",
+ "version": "v6.11.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/firebase/php-jwt.git",
+ "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
+ "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0"
+ },
+ "require-dev": {
+ "guzzlehttp/guzzle": "^7.4",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.5",
+ "psr/cache": "^2.0||^3.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0"
+ },
+ "suggest": {
+ "ext-sodium": "Support EdDSA (Ed25519) signatures",
+ "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Firebase\\JWT\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Neuman Vong",
+ "email": "neuman+pear@twilio.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Anant Narayanan",
+ "email": "anant@php.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+ "homepage": "https://github.com/firebase/php-jwt",
+ "keywords": [
+ "jwt",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/firebase/php-jwt/issues",
+ "source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
+ },
+ "time": "2025-04-09T20:32:01+00:00"
+ },
+ {
+ "name": "graham-campbell/result-type",
+ "version": "v1.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/GrahamCampbell/Result-Type.git",
+ "reference": "3ba905c11371512af9d9bdd27d99b782216b6945"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945",
+ "reference": "3ba905c11371512af9d9bdd27d99b782216b6945",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "phpoption/phpoption": "^1.9.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "GrahamCampbell\\ResultType\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ }
+ ],
+ "description": "An Implementation Of The Result Type",
+ "keywords": [
+ "Graham Campbell",
+ "GrahamCampbell",
+ "Result Type",
+ "Result-Type",
+ "result"
+ ],
+ "support": {
+ "issues": "https://github.com/GrahamCampbell/Result-Type/issues",
+ "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-07-20T21:45:45+00:00"
+ },
+ {
+ "name": "guzzlehttp/guzzle",
+ "version": "7.10.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
+ "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "guzzlehttp/promises": "^2.3",
+ "guzzlehttp/psr7": "^2.8",
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-client": "^1.0",
+ "symfony/deprecation-contracts": "^2.2 || ^3.0"
+ },
+ "provide": {
+ "psr/http-client-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "ext-curl": "*",
+ "guzzle/client-integration-tests": "3.0.2",
+ "php-http/message-factory": "^1.1",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20",
+ "psr/log": "^1.1 || ^2.0 || ^3.0"
+ },
+ "suggest": {
+ "ext-curl": "Required for CURL handler support",
+ "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+ "psr/log": "Required for using the Log middleware"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Jeremy Lindblom",
+ "email": "jeremeamia@gmail.com",
+ "homepage": "https://github.com/jeremeamia"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "psr-18",
+ "psr-7",
+ "rest",
+ "web service"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/guzzle/issues",
+ "source": "https://github.com/guzzle/guzzle/tree/7.10.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-23T22:36:01+00:00"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "2.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "481557b130ef3790cf82b713667b43030dc9c957"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957",
+ "reference": "481557b130ef3790cf82b713667b43030dc9c957",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/promises/issues",
+ "source": "https://github.com/guzzle/promises/tree/2.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-22T14:34:08+00:00"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "2.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "21dc724a0583619cd1652f673303492272778051"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
+ "reference": "21dc724a0583619cd1652f673303492272778051",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "ralouphie/getallheaders": "^3.0"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "http-interop/http-factory-tests": "0.9.0",
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25"
+ },
+ "suggest": {
+ "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://sagikazarmark.hu"
+ }
+ ],
+ "description": "PSR-7 message implementation that also provides common utility methods",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/psr7/issues",
+ "source": "https://github.com/guzzle/psr7/tree/2.8.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-23T21:21:41+00:00"
+ },
+ {
+ "name": "league/container",
+ "version": "4.2.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/container.git",
+ "reference": "d3cebb0ff4685ff61c749e54b27db49319e2ec00"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/container/zipball/d3cebb0ff4685ff61c749e54b27db49319e2ec00",
+ "reference": "d3cebb0ff4685ff61c749e54b27db49319e2ec00",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0",
+ "psr/container": "^1.1 || ^2.0"
+ },
+ "provide": {
+ "psr/container-implementation": "^1.0"
+ },
+ "replace": {
+ "orno/di": "~2.0"
+ },
+ "require-dev": {
+ "nette/php-generator": "^3.4",
+ "nikic/php-parser": "^4.10",
+ "phpstan/phpstan": "^0.12.47",
+ "phpunit/phpunit": "^8.5.17",
+ "roave/security-advisories": "dev-latest",
+ "scrutinizer/ocular": "^1.8",
+ "squizlabs/php_codesniffer": "^3.6"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-1.x": "1.x-dev",
+ "dev-2.x": "2.x-dev",
+ "dev-3.x": "3.x-dev",
+ "dev-4.x": "4.x-dev",
+ "dev-master": "4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\Container\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Phil Bennett",
+ "email": "mail@philbennett.co.uk",
+ "role": "Developer"
+ }
+ ],
+ "description": "A fast and intuitive dependency injection container.",
+ "homepage": "https://github.com/thephpleague/container",
+ "keywords": [
+ "container",
+ "dependency",
+ "di",
+ "injection",
+ "league",
+ "provider",
+ "service"
+ ],
+ "support": {
+ "issues": "https://github.com/thephpleague/container/issues",
+ "source": "https://github.com/thephpleague/container/tree/4.2.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/philipobenito",
+ "type": "github"
+ }
+ ],
+ "time": "2025-05-20T12:55:37+00:00"
+ },
+ {
+ "name": "monolog/monolog",
+ "version": "3.9.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/monolog.git",
+ "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6",
+ "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/log": "^2.0 || ^3.0"
+ },
+ "provide": {
+ "psr/log-implementation": "3.0.0"
+ },
+ "require-dev": {
+ "aws/aws-sdk-php": "^3.0",
+ "doctrine/couchdb": "~1.0@dev",
+ "elasticsearch/elasticsearch": "^7 || ^8",
+ "ext-json": "*",
+ "graylog2/gelf-php": "^1.4.2 || ^2.0",
+ "guzzlehttp/guzzle": "^7.4.5",
+ "guzzlehttp/psr7": "^2.2",
+ "mongodb/mongodb": "^1.8",
+ "php-amqplib/php-amqplib": "~2.4 || ^3",
+ "php-console/php-console": "^3.1.8",
+ "phpstan/phpstan": "^2",
+ "phpstan/phpstan-deprecation-rules": "^2",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpunit/phpunit": "^10.5.17 || ^11.0.7",
+ "predis/predis": "^1.1 || ^2",
+ "rollbar/rollbar": "^4.0",
+ "ruflin/elastica": "^7 || ^8",
+ "symfony/mailer": "^5.4 || ^6",
+ "symfony/mime": "^5.4 || ^6"
+ },
+ "suggest": {
+ "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+ "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+ "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
+ "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+ "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
+ "ext-mbstring": "Allow to work properly with unicode symbols",
+ "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
+ "ext-openssl": "Required to send log messages using SSL",
+ "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
+ "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+ "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
+ "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+ "rollbar/rollbar": "Allow sending log messages to Rollbar",
+ "ruflin/elastica": "Allow sending log messages to an Elastic Search server"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Monolog\\": "src/Monolog"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "https://seld.be"
+ }
+ ],
+ "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+ "homepage": "https://github.com/Seldaek/monolog",
+ "keywords": [
+ "log",
+ "logging",
+ "psr-3"
+ ],
+ "support": {
+ "issues": "https://github.com/Seldaek/monolog/issues",
+ "source": "https://github.com/Seldaek/monolog/tree/3.9.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Seldaek",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-03-24T10:02:05+00:00"
+ },
+ {
+ "name": "nikic/fast-route",
+ "version": "v1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/FastRoute.git",
+ "reference": "181d480e08d9476e61381e04a71b34dc0432e812"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812",
+ "reference": "181d480e08d9476e61381e04a71b34dc0432e812",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35|~5.7"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "FastRoute\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov",
+ "email": "nikic@php.net"
+ }
+ ],
+ "description": "Fast request router for PHP",
+ "keywords": [
+ "router",
+ "routing"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/FastRoute/issues",
+ "source": "https://github.com/nikic/FastRoute/tree/master"
+ },
+ "time": "2018-02-13T20:26:39+00:00"
+ },
+ {
+ "name": "phpoption/phpoption",
+ "version": "1.9.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/schmittjoh/php-option.git",
+ "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
+ "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ },
+ "branch-alias": {
+ "dev-master": "1.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpOption\\": "src/PhpOption/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Johannes M. Schmitt",
+ "email": "schmittjoh@gmail.com",
+ "homepage": "https://github.com/schmittjoh"
+ },
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ }
+ ],
+ "description": "Option Type for PHP",
+ "keywords": [
+ "language",
+ "option",
+ "php",
+ "type"
+ ],
+ "support": {
+ "issues": "https://github.com/schmittjoh/php-option/issues",
+ "source": "https://github.com/schmittjoh/php-option/tree/1.9.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-21T11:53:16+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
+ },
+ "time": "2021-11-05T16:47:00+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory"
+ },
+ "time": "2024-04-15T12:06:14+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "psr/http-server-handler",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-server-handler.git",
+ "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4",
+ "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Server\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP server-side request handler",
+ "keywords": [
+ "handler",
+ "http",
+ "http-interop",
+ "psr",
+ "psr-15",
+ "psr-7",
+ "request",
+ "response",
+ "server"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2"
+ },
+ "time": "2023-04-10T20:06:20+00:00"
+ },
+ {
+ "name": "psr/http-server-middleware",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-server-middleware.git",
+ "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
+ "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0",
+ "psr/http-message": "^1.0 || ^2.0",
+ "psr/http-server-handler": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Server\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP server-side middleware",
+ "keywords": [
+ "http",
+ "http-interop",
+ "middleware",
+ "psr",
+ "psr-15",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/http-server-middleware/issues",
+ "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2"
+ },
+ "time": "2023-04-11T06:14:47+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/3.0.2"
+ },
+ "time": "2024-09-11T13:17:53+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
+ "time": "2019-03-08T08:55:37+00:00"
+ },
+ {
+ "name": "slim/psr7",
+ "version": "1.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/slimphp/Slim-Psr7.git",
+ "reference": "fe98653e7983010aa85c1d137c9b9ad5a1cd187d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/slimphp/Slim-Psr7/zipball/fe98653e7983010aa85c1d137c9b9ad5a1cd187d",
+ "reference": "fe98653e7983010aa85c1d137c9b9ad5a1cd187d",
+ "shasum": ""
+ },
+ "require": {
+ "fig/http-message-util": "^1.1.5",
+ "php": "^8.0",
+ "psr/http-factory": "^1.1",
+ "psr/http-message": "^1.0 || ^2.0",
+ "ralouphie/getallheaders": "^3.0",
+ "symfony/polyfill-php80": "^1.29"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "^1.0",
+ "psr/http-message-implementation": "^1.0 || ^2.0"
+ },
+ "require-dev": {
+ "adriansuter/php-autoload-override": "^1.4",
+ "ext-json": "*",
+ "http-interop/http-factory-tests": "^1.0 || ^2.0",
+ "php-http/psr7-integration-tests": "^1.4",
+ "phpspec/prophecy": "^1.19",
+ "phpspec/prophecy-phpunit": "^2.2",
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^9.6 || ^10",
+ "squizlabs/php_codesniffer": "^3.10"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Slim\\Psr7\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Josh Lockhart",
+ "email": "hello@joshlockhart.com",
+ "homepage": "https://joshlockhart.com"
+ },
+ {
+ "name": "Andrew Smith",
+ "email": "a.smith@silentworks.co.uk",
+ "homepage": "https://silentworks.co.uk"
+ },
+ {
+ "name": "Rob Allen",
+ "email": "rob@akrabat.com",
+ "homepage": "https://akrabat.com"
+ },
+ {
+ "name": "Pierre Berube",
+ "email": "pierre@lgse.com",
+ "homepage": "https://www.lgse.com"
+ }
+ ],
+ "description": "Strict PSR-7 implementation",
+ "homepage": "https://www.slimframework.com",
+ "keywords": [
+ "http",
+ "psr-7",
+ "psr7"
+ ],
+ "support": {
+ "issues": "https://github.com/slimphp/Slim-Psr7/issues",
+ "source": "https://github.com/slimphp/Slim-Psr7/tree/1.7.1"
+ },
+ "time": "2025-05-13T14:24:12+00:00"
+ },
+ {
+ "name": "slim/slim",
+ "version": "4.15.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/slimphp/Slim.git",
+ "reference": "17eba5182975878a0ab9b27982cd2e2cfcb67ea2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/slimphp/Slim/zipball/17eba5182975878a0ab9b27982cd2e2cfcb67ea2",
+ "reference": "17eba5182975878a0ab9b27982cd2e2cfcb67ea2",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "nikic/fast-route": "^1.3",
+ "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
+ "psr/container": "^1.0 || ^2.0",
+ "psr/http-factory": "^1.1",
+ "psr/http-message": "^1.1 || ^2.0",
+ "psr/http-server-handler": "^1.0",
+ "psr/http-server-middleware": "^1.0",
+ "psr/log": "^1.1 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "adriansuter/php-autoload-override": "^1.4 || ^2",
+ "ext-simplexml": "*",
+ "guzzlehttp/psr7": "^2.6",
+ "httpsoft/http-message": "^1.1",
+ "httpsoft/http-server-request": "^1.1",
+ "laminas/laminas-diactoros": "^2.17 || ^3",
+ "nyholm/psr7": "^1.8",
+ "nyholm/psr7-server": "^1.1",
+ "phpspec/prophecy": "^1.19",
+ "phpspec/prophecy-phpunit": "^2.1",
+ "phpstan/phpstan": "^1 || ^2",
+ "phpunit/phpunit": "^9.6",
+ "slim/http": "^1.3",
+ "slim/psr7": "^1.6",
+ "squizlabs/php_codesniffer": "^3.10",
+ "vimeo/psalm": "^5 || ^6"
+ },
+ "suggest": {
+ "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware",
+ "ext-xml": "Needed to support XML format in BodyParsingMiddleware",
+ "php-di/php-di": "PHP-DI is the recommended container library to be used with Slim",
+ "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information."
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Slim\\": "Slim"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Josh Lockhart",
+ "email": "hello@joshlockhart.com",
+ "homepage": "https://joshlockhart.com"
+ },
+ {
+ "name": "Andrew Smith",
+ "email": "a.smith@silentworks.co.uk",
+ "homepage": "https://silentworks.co.uk"
+ },
+ {
+ "name": "Rob Allen",
+ "email": "rob@akrabat.com",
+ "homepage": "https://akrabat.com"
+ },
+ {
+ "name": "Pierre Berube",
+ "email": "pierre@lgse.com",
+ "homepage": "https://www.lgse.com"
+ },
+ {
+ "name": "Gabriel Manricks",
+ "email": "gmanricks@me.com",
+ "homepage": "http://gabrielmanricks.com"
+ }
+ ],
+ "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs",
+ "homepage": "https://www.slimframework.com",
+ "keywords": [
+ "api",
+ "framework",
+ "micro",
+ "router"
+ ],
+ "support": {
+ "docs": "https://www.slimframework.com/docs/v4/",
+ "forum": "https://discourse.slimframework.com/",
+ "irc": "irc://irc.freenode.net:6667/slimphp",
+ "issues": "https://github.com/slimphp/Slim/issues",
+ "rss": "https://www.slimframework.com/blog/feed.rss",
+ "slack": "https://slimphp.slack.com/",
+ "source": "https://github.com/slimphp/Slim",
+ "wiki": "https://github.com/slimphp/Slim/wiki"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/slimphp",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/slim/slim",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-20T18:16:16+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:21:43+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
+ "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
+ "shasum": ""
+ },
+ "require": {
+ "ext-iconv": "*",
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-12-23T08:48:59+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php80",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
+ "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-01-02T08:10:11+00:00"
+ },
+ {
+ "name": "vlucas/phpdotenv",
+ "version": "v5.6.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/vlucas/phpdotenv.git",
+ "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
+ "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
+ "shasum": ""
+ },
+ "require": {
+ "ext-pcre": "*",
+ "graham-campbell/result-type": "^1.1.3",
+ "php": "^7.2.5 || ^8.0",
+ "phpoption/phpoption": "^1.9.3",
+ "symfony/polyfill-ctype": "^1.24",
+ "symfony/polyfill-mbstring": "^1.24",
+ "symfony/polyfill-php80": "^1.24"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "ext-filter": "*",
+ "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
+ },
+ "suggest": {
+ "ext-filter": "Required to use the boolean validator."
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ },
+ "branch-alias": {
+ "dev-master": "5.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Dotenv\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Vance Lucas",
+ "email": "vance@vancelucas.com",
+ "homepage": "https://github.com/vlucas"
+ }
+ ],
+ "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
+ "keywords": [
+ "dotenv",
+ "env",
+ "environment"
+ ],
+ "support": {
+ "issues": "https://github.com/vlucas/phpdotenv/issues",
+ "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-04-30T23:37:27+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.13.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-01T08:46:24+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v5.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
+ "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1"
+ },
+ "time": "2025-08-13T20:13:15+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
+ {
+ "name": "phpstan/phpstan",
+ "version": "1.12.28",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan.git",
+ "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9",
+ "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "docs": "https://phpstan.org/user-guide/getting-started",
+ "forum": "https://github.com/phpstan/phpstan/discussions",
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "security": "https://github.com/phpstan/phpstan/security/policy",
+ "source": "https://github.com/phpstan/phpstan-src"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ }
+ ],
+ "time": "2025-07-17T17:15:39+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "10.1.16",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "7e308268858ed6baedc8704a304727d20bc07c77"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77",
+ "reference": "7e308268858ed6baedc8704a304727d20bc07c77",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.19.1 || ^5.1.0",
+ "php": ">=8.1",
+ "phpunit/php-file-iterator": "^4.1.0",
+ "phpunit/php-text-template": "^3.0.1",
+ "sebastian/code-unit-reverse-lookup": "^3.0.0",
+ "sebastian/complexity": "^3.2.0",
+ "sebastian/environment": "^6.1.0",
+ "sebastian/lines-of-code": "^2.0.2",
+ "sebastian/version": "^4.0.1",
+ "theseer/tokenizer": "^1.2.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.1"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "10.1.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-08-22T04:31:57+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "4.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c",
+ "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-08-31T06:24:48+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "4.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7",
+ "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^10.0"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:56:09+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748",
+ "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "security": "https://github.com/sebastianbergmann/php-text-template/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-08-31T14:07:24+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "6.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d",
+ "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:57:52+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "10.5.53",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "32768472ebfb6969e6c7399f1c7b09009723f653"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/32768472ebfb6969e6c7399f1c7b09009723f653",
+ "reference": "32768472ebfb6969e6c7399f1c7b09009723f653",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.13.4",
+ "phar-io/manifest": "^2.0.4",
+ "phar-io/version": "^3.2.1",
+ "php": ">=8.1",
+ "phpunit/php-code-coverage": "^10.1.16",
+ "phpunit/php-file-iterator": "^4.1.0",
+ "phpunit/php-invoker": "^4.0.0",
+ "phpunit/php-text-template": "^3.0.1",
+ "phpunit/php-timer": "^6.0.0",
+ "sebastian/cli-parser": "^2.0.1",
+ "sebastian/code-unit": "^2.0.0",
+ "sebastian/comparator": "^5.0.3",
+ "sebastian/diff": "^5.1.1",
+ "sebastian/environment": "^6.1.0",
+ "sebastian/exporter": "^5.1.2",
+ "sebastian/global-state": "^6.0.2",
+ "sebastian/object-enumerator": "^5.0.0",
+ "sebastian/recursion-context": "^5.0.1",
+ "sebastian/type": "^4.0.0",
+ "sebastian/version": "^4.0.1"
+ },
+ "suggest": {
+ "ext-soap": "To be able to generate mocks based on WSDL files"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "10.5-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.53"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsors.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-20T14:40:06+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084",
+ "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T07:12:49+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "a81fee9eef0b7a76af11d121767abc44c104e503"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503",
+ "reference": "a81fee9eef0b7a76af11d121767abc44c104e503",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:58:43+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d",
+ "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:59:15+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e",
+ "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-mbstring": "*",
+ "php": ">=8.1",
+ "sebastian/diff": "^5.0",
+ "sebastian/exporter": "^5.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "security": "https://github.com/sebastianbergmann/comparator/security/policy",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-18T14:56:07+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "3.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "68ff824baeae169ec9f2137158ee529584553799"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799",
+ "reference": "68ff824baeae169ec9f2137158ee529584553799",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "security": "https://github.com/sebastianbergmann/complexity/security/policy",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-21T08:37:17+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "5.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e",
+ "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0",
+ "symfony/process": "^6.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "security": "https://github.com/sebastianbergmann/diff/security/policy",
+ "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T07:15:17+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "6.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "8074dbcd93529b357029f5cc5058fd3e43666984"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984",
+ "reference": "8074dbcd93529b357029f5cc5058fd3e43666984",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "https://github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "security": "https://github.com/sebastianbergmann/environment/security/policy",
+ "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-23T08:47:14+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "5.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "955288482d97c19a372d3f31006ab3f37da47adf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf",
+ "reference": "955288482d97c19a372d3f31006ab3f37da47adf",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=8.1",
+ "sebastian/recursion-context": "^5.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "security": "https://github.com/sebastianbergmann/exporter/security/policy",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T07:17:12+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "6.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9",
+ "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "sebastian/object-reflector": "^3.0",
+ "sebastian/recursion-context": "^5.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "https://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "security": "https://github.com/sebastianbergmann/global-state/security/policy",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T07:19:19+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0",
+ "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-21T08:38:20+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "5.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906",
+ "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "sebastian/object-reflector": "^3.0",
+ "sebastian/recursion-context": "^5.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T07:08:32+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "24ed13d98130f0e7122df55d06c5c4942a577957"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957",
+ "reference": "24ed13d98130f0e7122df55d06c5c4942a577957",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T07:06:18+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "5.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a",
+ "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-10T07:50:56+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "4.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "462699a16464c3944eefc02ebdd77882bd3925bf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf",
+ "reference": "462699a16464c3944eefc02ebdd77882bd3925bf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/4.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T07:10:45+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17",
+ "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-07T11:34:05+00:00"
+ },
+ {
+ "name": "squizlabs/php_codesniffer",
+ "version": "3.13.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
+ "reference": "5b5e3821314f947dd040c70f7992a64eac89025c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c",
+ "reference": "5b5e3821314f947dd040c70f7992a64eac89025c",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
+ },
+ "bin": [
+ "bin/phpcbf",
+ "bin/phpcs"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Greg Sherwood",
+ "role": "Former lead"
+ },
+ {
+ "name": "Juliette Reinders Folmer",
+ "role": "Current lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "keywords": [
+ "phpcs",
+ "standards",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
+ "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy",
+ "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2025-06-17T22:17:01+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.2.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:36:25+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=8.1"
+ },
+ "platform-dev": {},
+ "plugin-api-version": "2.6.0"
+}
diff --git a/php8/configuration.json b/php8/configuration.json
new file mode 100755
index 0000000..517c422
--- /dev/null
+++ b/php8/configuration.json
@@ -0,0 +1,4 @@
+{
+ "storage_directory": "./storage",
+ "jwt_secret": "secret-key"
+}
diff --git a/php8/docker/Dockerfile b/php8/docker/Dockerfile
new file mode 100755
index 0000000..5c69ac6
--- /dev/null
+++ b/php8/docker/Dockerfile
@@ -0,0 +1,35 @@
+FROM php:8.2-fpm-alpine
+
+# Install system dependencies
+RUN apk add --no-cache \
+ icu-dev \
+ libzip-dev \
+ libpng-dev \
+ jpeg-dev \
+ freetype-dev \
+ && docker-php-ext-install \
+ intl \
+ pdo_mysql \
+ zip \
+ gd
+RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
+ && docker-php-ext-configure pcntl --enable-pcntl \
+ && docker-php-ext-install pcntl
+
+# Create PHP configuration file
+RUN echo "memory_limit = 256M" > /usr/local/etc/php/conf.d/custom.ini \
+ && echo "upload_max_filesize = 100M" >> /usr/local/etc/php/conf.d/custom.ini \
+ && echo "post_max_size = 100M" >> /usr/local/etc/php/conf.d/custom.ini \
+ && echo "max_execution_time = 300" >> /usr/local/etc/php/conf.d/custom.ini
+
+# Set working directory
+WORKDIR /var/www/html
+
+# Copy application code
+COPY --chown=www-data:www-data . .
+
+# Expose port 9000 for PHP-FPM
+EXPOSE 9000
+
+# Start PHP-FPM
+CMD ["php-fpm"]
\ No newline at end of file
diff --git a/php8/docker/default.conf b/php8/docker/default.conf
new file mode 100755
index 0000000..88e89d4
--- /dev/null
+++ b/php8/docker/default.conf
@@ -0,0 +1,29 @@
+server {
+ listen 80;
+ server_name localhost;
+ root /var/www/html;
+ index index.php index.html;
+
+ location / {
+ try_files $uri $uri/ /index.php?$query_string;
+ }
+
+ location ~ \.php$ {
+ try_files $uri =404;
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
+ fastcgi_pass php:9000;
+ fastcgi_index index.php;
+ include fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+ fastcgi_param PATH_INFO $fastcgi_path_info;
+ }
+
+ location ~ /\.(?!well-known).* {
+ deny all;
+ }
+
+ # Security headers
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+}
\ No newline at end of file
diff --git a/php8/docker/docker-compose.yml b/php8/docker/docker-compose.yml
new file mode 100755
index 0000000..46d4fe7
--- /dev/null
+++ b/php8/docker/docker-compose.yml
@@ -0,0 +1,46 @@
+version: "3.9"
+services:
+ php:
+ build:
+ context: ..
+ dockerfile: docker/Dockerfile
+ image: php82-app-img
+ container_name: php82-app
+ volumes:
+ - ..:/var/www/html
+ networks:
+ - app-network
+
+ nginx:
+ build:
+ context: ..
+ dockerfile: docker/nginx.Dockerfile
+ image: php82-nginx-img
+ container_name: php82-nginx
+ ports:
+ - "8081:80"
+ volumes:
+ - ..:/var/www/html:ro
+ depends_on:
+ - php
+ networks:
+ - app-network
+
+ scheduler:
+ build:
+ context: ..
+ dockerfile: docker/Dockerfile
+ image: php82-app-img
+ container_name: php82-scheduler
+ volumes:
+ - ..:/var/www/html
+ command: ["php", "/var/www/html/cli/scheduler.php"]
+ depends_on:
+ - php
+ networks:
+ - app-network
+ restart: unless-stopped
+
+networks:
+ app-network:
+ driver: bridge
\ No newline at end of file
diff --git a/php8/docker/nginx.Dockerfile b/php8/docker/nginx.Dockerfile
new file mode 100755
index 0000000..7d69fed
--- /dev/null
+++ b/php8/docker/nginx.Dockerfile
@@ -0,0 +1,7 @@
+FROM nginx:alpine
+
+COPY docker/default.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/php8/index.php b/php8/index.php
new file mode 100755
index 0000000..f8a7f83
--- /dev/null
+++ b/php8/index.php
@@ -0,0 +1,9 @@
+run();
\ No newline at end of file
diff --git a/php8/phpunit.xml b/php8/phpunit.xml
new file mode 100755
index 0000000..8e0fcfb
--- /dev/null
+++ b/php8/phpunit.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ tests/Unit
+
+
+ tests/Integration
+
+
+
\ No newline at end of file
diff --git a/php8/src/Application.php b/php8/src/Application.php
new file mode 100755
index 0000000..17d1d10
--- /dev/null
+++ b/php8/src/Application.php
@@ -0,0 +1,61 @@
+di = new DiContainer();
+ $this->app = AppFactory::create();
+
+ $this->setupMiddleware();
+ $this->setupRoutes();
+ }
+
+ private function setupMiddleware(): void
+ {
+ $this->app->addBodyParsingMiddleware();
+ $this->app->addRoutingMiddleware();
+ $this->app->addErrorMiddleware(true, true, true);
+ }
+
+ private function setupRoutes(): void
+ {
+ $jwtMiddleware = $this->di->get(JwtMiddleware::class);
+ $authController = $this->di->get(AuthController::class);
+ $storeController = $this->di->get(StoreController::class);
+
+ // Public routes
+ $this->app->group('/api/v1', function (RouteCollectorProxy $group) use ($authController) {
+ $group->post('/login', [$authController, 'login']);
+ });
+
+ // Protected routes
+ $this->app->group('/api/v1', function (RouteCollectorProxy $group) use ($storeController, $jwtMiddleware) {
+ $group->group('/items', function (RouteCollectorProxy $itemGroup) use ($storeController) {
+ $itemGroup->get('', [$storeController, 'listItems']);
+ $itemGroup->post('', [$storeController, 'addItem']);
+ $itemGroup->get('/{id}', [$storeController, 'getItem']);
+ $itemGroup->delete('/{id}', [$storeController, 'deleteItem']);
+ })->add($jwtMiddleware);
+ });
+ }
+
+ public function run(): void
+ {
+ $this->app->run();
+ }
+
+}
\ No newline at end of file
diff --git a/php8/src/Application/Commands/AddItem.php b/php8/src/Application/Commands/AddItem.php
new file mode 100755
index 0000000..6db631f
--- /dev/null
+++ b/php8/src/Application/Commands/AddItem.php
@@ -0,0 +1,67 @@
+itemRepository = $itemRepository;
+ $this->orderService = $orderService;
+ $this->timeProvider = $timeProvider;
+ $this->expirationPolicy = new ItemExpirationPolicy();
+ $this->logger = $logger;
+ }
+
+ public function execute(string $name, string $expirationDate, string $orderUrl, string $userId): string
+ {
+ try {
+ $item = new Item(
+ uniqid('item_', true),
+ $name,
+ new \DateTimeImmutable($expirationDate),
+ $orderUrl,
+ $userId
+ );
+
+ $currentTime = $this->timeProvider->now();
+ $this->expirationPolicy->checkExpiration($item, $currentTime);
+
+ if ($item->isExpired()) {
+ try {
+ $this->orderService->orderItem($item);
+ $item->markAsOrdered();
+ } catch (\Exception $e) {
+ $this->logger->error('Failed to place order for expired item: ' . $e->getMessage());
+ }
+ }
+
+ $this->itemRepository->save($item);
+
+ return $item->getId();
+ } catch (DomainException $e) {
+ throw new ApplicationException('Failed to add item: ' . $e->getMessage(), 0, $e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/php8/src/Application/Commands/DeleteItem.php b/php8/src/Application/Commands/DeleteItem.php
new file mode 100755
index 0000000..b5cc738
--- /dev/null
+++ b/php8/src/Application/Commands/DeleteItem.php
@@ -0,0 +1,39 @@
+itemRepository = $itemRepository;
+ }
+
+ public function execute(string $itemId, string $userId): void
+ {
+ try {
+ $item = $this->itemRepository->findById($itemId);
+
+ if (!$item) {
+ throw new ItemNotFoundException("Item with ID {$itemId} not found");
+ }
+
+ if ($item->getUserId() !== $userId) {
+ throw new ApplicationException("User {$userId} is not authorized to delete item {$itemId}");
+ }
+
+ $this->itemRepository->delete($itemId);
+ } catch (DomainException $e) {
+ throw new ApplicationException('Failed to delete item: ' . $e->getMessage(), 0, $e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/php8/src/Application/Commands/HandleExpiredItems.php b/php8/src/Application/Commands/HandleExpiredItems.php
new file mode 100755
index 0000000..110eb5e
--- /dev/null
+++ b/php8/src/Application/Commands/HandleExpiredItems.php
@@ -0,0 +1,59 @@
+itemRepository = $itemRepository;
+ $this->orderService = $orderService;
+ $this->timeProvider = $timeProvider;
+ $this->expirationPolicy = new ItemExpirationPolicy();
+ $this->logger = $logger;
+ }
+
+ public function execute(): void
+ {
+ try {
+ $currentTime = $this->timeProvider->now();
+ $items = $this->itemRepository->findExpired();
+
+ foreach ($items as $item) {
+ $this->expirationPolicy->checkExpiration($item, $currentTime);
+
+ if ($item->isExpired() && !$item->isOrdered()) {
+ try {
+ $this->orderService->orderItem($item);
+ $item->markAsOrdered();
+ $this->itemRepository->save($item);
+ } catch (\Exception $e) {
+ $this->logger->error('Failed to place order for expired item ' . $item->getId() . ': ' . $e->getMessage());
+ }
+ }
+ }
+ } catch (DomainException $e) {
+ throw new ApplicationException('Failed to handle expired items: ' . $e->getMessage(), 0, $e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/php8/src/Application/Commands/LoginUser.php b/php8/src/Application/Commands/LoginUser.php
new file mode 100755
index 0000000..8e0276a
--- /dev/null
+++ b/php8/src/Application/Commands/LoginUser.php
@@ -0,0 +1,33 @@
+authService = $authService;
+ }
+
+ public function execute(string $username, string $password): string
+ {
+ try {
+ $token = $this->authService->authenticate($username, $password);
+
+ if (!$token) {
+ throw new ApplicationException('Invalid credentials');
+ }
+
+ return $token;
+ } catch (\Exception $e) {
+ throw new ApplicationException('Failed to login: ' . $e->getMessage(), 0, $e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/php8/src/Application/Exceptions/ApplicationException.php b/php8/src/Application/Exceptions/ApplicationException.php
new file mode 100755
index 0000000..1c96d31
--- /dev/null
+++ b/php8/src/Application/Exceptions/ApplicationException.php
@@ -0,0 +1,11 @@
+itemRepository = $itemRepository;
+ }
+
+ public function execute(string $itemId, string $userId): array
+ {
+ try {
+ $item = $this->itemRepository->findById($itemId);
+
+ if (!$item) {
+ throw new ItemNotFoundException("Item with ID {$itemId} not found");
+ }
+
+ if ($item->getUserId() !== $userId) {
+ throw new ApplicationException("User {$userId} is not authorized to access item {$itemId}");
+ }
+
+ return $item->toArray();
+ } catch (DomainException $e) {
+ throw new ApplicationException('Failed to get item: ' . $e->getMessage(), 0, $e);
+ }
+ }
+}
diff --git a/php8/src/Application/Queries/ListItems.php b/php8/src/Application/Queries/ListItems.php
new file mode 100755
index 0000000..ed2e76f
--- /dev/null
+++ b/php8/src/Application/Queries/ListItems.php
@@ -0,0 +1,32 @@
+itemRepository = $itemRepository;
+ }
+
+ public function execute(string $userId): array
+ {
+ try {
+ $items = $this->itemRepository->findByUserId($userId);
+
+ return array_map(static function ($item) {
+ return $item->toArray();
+ }, $items);
+ } catch (DomainException $e) {
+ throw new ApplicationException('Failed to list items: ' . $e->getMessage(), 0, $e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/php8/src/DiContainer.php b/php8/src/DiContainer.php
new file mode 100755
index 0000000..c5e1120
--- /dev/null
+++ b/php8/src/DiContainer.php
@@ -0,0 +1,180 @@
+storagePath = $config['storage_directory'] ?? self::ROOT_DIR. '/storage';
+ $this->jwtSecret = $config['jwt_secret'] ?? 'secret-key';
+
+ $this->diContainer = new Container();
+ $this->configure();
+ }
+ public function add(string $id, $concrete = null): void
+ {
+ $this->diContainer->add($id, $concrete);
+ }
+
+ public function get($id)
+ {
+ return $this->diContainer->get($id);
+ }
+
+ public function getNew($id)
+ {
+ return $this->diContainer->getNew($id);
+ }
+
+ public function has($id): bool
+ {
+ return $this->diContainer->has($id);
+ }
+
+ private function configure(): void
+ {
+ $di = $this->diContainer;
+
+ // Configure shared parameters
+ $di->add('storagePath', $this->storagePath);
+ $di->add('jwtSecret', $this->jwtSecret);
+
+ // --- Logger ---
+ $di->add(LoggerInterface::class, function () {
+ $logger = new Logger('autostore');
+ $logger->pushHandler(new StreamHandler($this->storagePath . '/app.log', \Monolog\Level::Debug));
+ return $logger;
+ })->setShared(true);
+
+ // --- Time Provider ---
+ $di->add(ITimeProvider::class, SystemTimeProvider::class)->setShared(true);
+
+ // --- HTTP Client ---
+ $di->add(Client::class, function () {
+ return new Client([
+ 'timeout' => 30,
+ 'headers' => [
+ 'User-Agent' => 'AutoStore/1.0'
+ ]
+ ]);
+ })->setShared(true);
+
+ // --- Repositories ---
+ $di->add(IUserRepository::class, FileUserRepository::class)
+ ->addArgument('storagePath')
+ ->addArgument(LoggerInterface::class)
+ ->setShared(true);
+
+ $di->add(IItemRepository::class, FileItemRepository::class)
+ ->addArgument('storagePath')
+ ->addArgument(LoggerInterface::class)
+ ->setShared(true);
+
+ // --- Auth Service ---
+ $di->add(IAuthService::class, JwtAuthService::class)
+ ->addArgument(IUserRepository::class)
+ ->addArgument('jwtSecret')
+ ->addArgument(LoggerInterface::class)
+ ->setShared(true);
+
+ // --- Order Service ---
+ $di->add(IOrderService::class, HttpOrderService::class)
+ ->addArgument(Client::class)
+ ->addArgument(LoggerInterface::class)
+ ->setShared(true);
+
+ // --- JWT Middleware ---
+ $di->add(JwtMiddleware::class)
+ ->addArgument(IAuthService::class)
+ ->addArgument(LoggerInterface::class)
+ ->setShared(true);
+
+ // --- Use Cases ---
+ $di->add(LoginUser::class)
+ ->addArgument(IAuthService::class)
+ ->setShared(true);
+
+ $di->add(AddItem::class)
+ ->addArgument(IItemRepository::class)
+ ->addArgument(IOrderService::class)
+ ->addArgument(ITimeProvider::class)
+ ->addArgument(LoggerInterface::class)
+ ->setShared(true);
+
+ $di->add(GetItem::class)
+ ->addArgument(IItemRepository::class)
+ ->setShared(true);
+
+ $di->add(ListItems::class)
+ ->addArgument(IItemRepository::class)
+ ->setShared(true);
+
+ $di->add(UpdateItem::class)
+ ->addArgument(IItemRepository::class)
+ ->addArgument(IOrderService::class)
+ ->addArgument(ITimeProvider::class)
+ ->addArgument(LoggerInterface::class)
+ ->setShared(true);
+
+ $di->add(DeleteItem::class)
+ ->addArgument(IItemRepository::class)
+ ->setShared(true);
+
+ $di->add(HandleExpiredItems::class)
+ ->addArgument(IItemRepository::class)
+ ->addArgument(IOrderService::class)
+ ->addArgument(ITimeProvider::class)
+ ->addArgument(LoggerInterface::class)
+ ->setShared(true);
+
+ // --- Controllers ---
+ $di->add(AuthController::class)
+ ->addArgument(LoginUser::class)
+ ->setShared(true);
+
+ $di->add(StoreController::class)
+ ->addArgument(AddItem::class)
+ ->addArgument(GetItem::class)
+ ->addArgument(ListItems::class)
+ ->addArgument(DeleteItem::class)
+ ->setShared(true);
+ }
+}
\ No newline at end of file
diff --git a/php8/src/Domain/Entities/Item.php b/php8/src/Domain/Entities/Item.php
new file mode 100755
index 0000000..5669b58
--- /dev/null
+++ b/php8/src/Domain/Entities/Item.php
@@ -0,0 +1,194 @@
+id = $id;
+ $this->name = $name;
+ $this->expirationDate = $expirationDate;
+ $this->orderUrl = $orderUrl;
+ $this->userId = $userId;
+ $this->expired = false;
+ $this->ordered = false;
+ $this->createdAt = new DateTimeImmutable();
+ $this->updatedAt = null;
+}
+
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getExpirationDate(): DateTimeImmutable
+ {
+ return $this->expirationDate;
+ }
+
+ public function getOrderUrl(): string
+ {
+ return $this->orderUrl;
+ }
+
+ public function getUserId(): string
+ {
+ return $this->userId;
+ }
+
+ public function isExpired(): bool
+ {
+ return $this->expired;
+ }
+
+ public function getCreatedAt(): DateTimeImmutable
+ {
+ return $this->createdAt;
+ }
+
+ public function getUpdatedAt(): ?DateTimeImmutable
+ {
+ return $this->updatedAt;
+ }
+
+ public function markAsExpired(): void
+ {
+ if ($this->expired) {
+ return;
+ }
+
+ $this->expired = true;
+ $this->updatedAt = new DateTimeImmutable();
+ }
+
+ public function isOrdered(): bool
+ {
+ return $this->ordered;
+ }
+
+ public function markAsOrdered(): void
+ {
+ if ($this->ordered) {
+ return;
+ }
+
+ $this->ordered = true;
+ $this->updatedAt = new DateTimeImmutable();
+ }
+
+ public function updateName(string $name): void
+ {
+ if ($this->name === $name) {
+ return;
+ }
+
+ $this->name = $name;
+ $this->updatedAt = new DateTimeImmutable();
+ }
+
+ public function updateExpirationDate(DateTimeImmutable $expirationDate): void
+ {
+ if ($this->expirationDate == $expirationDate) {
+ return;
+ }
+
+ $this->expirationDate = $expirationDate;
+ $this->updatedAt = new DateTimeImmutable();
+ }
+
+ public function updateOrderUrl(string $orderUrl): void
+ {
+ if ($this->orderUrl === $orderUrl) {
+ return;
+ }
+
+ $this->orderUrl = $orderUrl;
+ $this->updatedAt = new DateTimeImmutable();
+ }
+
+ public function toArray(): array
+ {
+ return [
+ 'id' => $this->id,
+ 'name' => $this->name,
+ 'expirationDate' => $this->expirationDate->format('Y-m-d\TH:i:s.uP'),
+ 'orderUrl' => $this->orderUrl,
+ 'userId' => $this->userId,
+ 'expired' => $this->expired,
+ 'is_ordered' => $this->ordered,
+ 'createdAt' => $this->createdAt->format('Y-m-d\TH:i:s.uP'),
+ 'updatedAt' => $this->updatedAt?->format('Y-m-d\TH:i:s.uP'),
+ ];
+ }
+
+ public static function fromArray(array $data): self
+ {
+ if (!isset($data['id'], $data['name'], $data['expirationDate'], $data['orderUrl'], $data['userId'])) {
+ throw new DomainException('Invalid item data');
+ }
+
+ $item = new self(
+ $data['id'],
+ $data['name'],
+ new DateTimeImmutable($data['expirationDate']),
+ $data['orderUrl'],
+ $data['userId']
+ );
+
+ if (isset($data['expired']) && is_bool($data['expired'])) {
+ if ($data['expired']) {
+ $item->markAsExpired();
+ }
+ }
+
+ if (isset($data['is_ordered']) && is_bool($data['is_ordered'])) {
+ if ($data['is_ordered']) {
+ $item->markAsOrdered();
+ }
+ }
+
+ return $item;
+ }
+}
\ No newline at end of file
diff --git a/php8/src/Domain/Entities/User.php b/php8/src/Domain/Entities/User.php
new file mode 100755
index 0000000..4b09b13
--- /dev/null
+++ b/php8/src/Domain/Entities/User.php
@@ -0,0 +1,97 @@
+id = $id;
+ $this->username = $username;
+ $this->passwordHash = $passwordHash;
+ $this->createdAt = new DateTimeImmutable();
+ $this->updatedAt = null;
+ }
+
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ public function getUsername(): string
+ {
+ return $this->username;
+ }
+
+ public function getPasswordHash(): string
+ {
+ return $this->passwordHash;
+ }
+
+ public function getCreatedAt(): DateTimeImmutable
+ {
+ return $this->createdAt;
+ }
+
+ public function getUpdatedAt(): ?DateTimeImmutable
+ {
+ return $this->updatedAt;
+ }
+
+ public function updateUsername(string $username): void
+ {
+ if ($this->username === $username) {
+ return;
+ }
+
+ $this->username = $username;
+ $this->updatedAt = new DateTimeImmutable();
+ }
+
+ public function toArray(): array
+ {
+ return [
+ 'id' => $this->id,
+ 'username' => $this->username,
+ 'passwordHash' => $this->passwordHash,
+ 'createdAt' => $this->createdAt->format('Y-m-d\TH:i:s.uP'),
+ 'updatedAt' => $this->updatedAt?->format('Y-m-d\TH:i:s.uP'),
+ ];
+ }
+
+ public static function fromArray(array $data): self
+ {
+ if (!isset($data['id'], $data['username'], $data['passwordHash'])) {
+ throw new DomainException('Invalid user data');
+ }
+
+ return new self(
+ $data['id'],
+ $data['username'],
+ $data['passwordHash']
+ );
+ }
+}
diff --git a/php8/src/Domain/Exceptions/DomainException.php b/php8/src/Domain/Exceptions/DomainException.php
new file mode 100755
index 0000000..c400f9e
--- /dev/null
+++ b/php8/src/Domain/Exceptions/DomainException.php
@@ -0,0 +1,11 @@
+getExpirationDate() <= $currentTime;
+ }
+
+ public function checkExpiration(Item $item, DateTimeImmutable $currentTime): void
+ {
+ if ($this->isExpired($item, $currentTime) && !$item->isExpired()) {
+ $item->markAsExpired();
+ }
+ }
+}
\ No newline at end of file
diff --git a/php8/src/Infrastructure/Adapters/SystemTimeProvider.php b/php8/src/Infrastructure/Adapters/SystemTimeProvider.php
new file mode 100755
index 0000000..4455197
--- /dev/null
+++ b/php8/src/Infrastructure/Adapters/SystemTimeProvider.php
@@ -0,0 +1,16 @@
+userRepository = $userRepository;
+ $this->secretKey = $secretKey;
+ $this->logger = $logger;
+ }
+
+ /**
+ * @note Showcase only, use a proper service
+ */
+ public function authenticate(string $username, string $password): ?string
+ {
+ $user = $this->userRepository->findByUsername($username);
+
+ if ($user === null) {
+ $this->logger->warning("User not found: {$username}");
+ return null;
+ }
+
+ // Verify the password against the stored hash
+ if (password_verify($password, $user->getPasswordHash())) {
+ $payload = [
+ 'iss' => 'autostore',
+ 'sub' => $user->getId(),
+ 'iat' => time(),
+ 'exp' => time() + 3600, // 1 hour expiration
+ 'username' => $user->getUsername()
+ ];
+
+ return JWT::encode($payload, $this->secretKey, 'HS256');
+ }
+
+ $this->logger->warning("Invalid password for user: {$username}");
+ return null;
+ }
+
+ public function validateToken(string $token): bool
+ {
+ try {
+ JWT::decode($token, new Key($this->secretKey, 'HS256'));
+ return true;
+ } catch (ExpiredException $e) {
+ $this->logger->warning('Token expired');
+ return false;
+ } catch (SignatureInvalidException $e) {
+ $this->logger->warning('Invalid token signature');
+ return false;
+ } catch (\Exception $e) {
+ $this->logger->error('Token validation error: ' . $e->getMessage());
+ return false;
+ }
+ }
+
+ public function getUserIdFromToken(string $token): ?string
+ {
+ try {
+ $decoded = JWT::decode($token, new Key($this->secretKey, 'HS256'));
+ return $decoded->sub;
+ } catch (\Exception $e) {
+ $this->logger->error('Error extracting user ID from token: ' . $e->getMessage());
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/php8/src/Infrastructure/Http/HttpOrderService.php b/php8/src/Infrastructure/Http/HttpOrderService.php
new file mode 100755
index 0000000..0feae95
--- /dev/null
+++ b/php8/src/Infrastructure/Http/HttpOrderService.php
@@ -0,0 +1,63 @@
+httpClient = $httpClient;
+ $this->logger = $logger;
+ }
+
+ /**
+ * @throws OrderException
+ */
+ public function orderItem(Item $item): void
+ {
+ try {
+ $orderData = [
+ 'itemId' => $item->getId(),
+ 'itemName' => $item->getName(),
+ 'userId' => $item->getUserId(),
+ 'expirationDate' => $item->getExpirationDate()->format('Y-m-d\TH:i:s.uP'),
+ 'orderTimestamp' => (new \DateTimeImmutable())->format('Y-m-d\TH:i:s.uP')
+ ];
+
+ $response = $this->httpClient->post($item->getOrderUrl(), [
+ 'json' => $orderData,
+ 'timeout' => 30,
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'User-Agent' => 'AutoStore/1.0'
+ ]
+ ]);
+
+ $statusCode = $response->getStatusCode();
+
+ if ($statusCode < 200 || $statusCode >= 300) {
+ throw OrderException::create($item->getOrderUrl(), "HTTP status code: {$statusCode}");
+ }
+
+ $this->logger->info("Order placed successfully for item {$item->getId()}");
+ } catch (RequestException $e) {
+ $this->logger->error("Failed to place order for item {$item->getId()}: " . $e->getMessage());
+ throw OrderException::create($item->getOrderUrl(), $e->getMessage());
+ } catch (\Exception $e) {
+ $this->logger->error("Unexpected error placing order for item {$item->getId()}: " . $e->getMessage());
+ throw OrderException::create($item->getOrderUrl(), $e->getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/php8/src/Infrastructure/Http/JwtMiddleware.php b/php8/src/Infrastructure/Http/JwtMiddleware.php
new file mode 100755
index 0000000..b0856bc
--- /dev/null
+++ b/php8/src/Infrastructure/Http/JwtMiddleware.php
@@ -0,0 +1,65 @@
+authService = $authService;
+ $this->logger = $logger;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $authHeader = $request->getHeaderLine('Authorization');
+
+ if (empty($authHeader)) {
+ $this->logger->warning('Missing Authorization header');
+ return $this->createUnauthorizedResponse('Missing Authorization header');
+ }
+
+ if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
+ $this->logger->warning('Invalid Authorization header format');
+ return $this->createUnauthorizedResponse('Invalid Authorization header format');
+ }
+
+ $token = $matches[1];
+
+ if (!$this->authService->validateToken($token)) {
+ $this->logger->warning('Invalid or expired token');
+ return $this->createUnauthorizedResponse('Invalid or expired token');
+ }
+
+ $userId = $this->authService->getUserIdFromToken($token);
+
+ if ($userId === null) {
+ $this->logger->warning('Could not extract user ID from token');
+ return $this->createUnauthorizedResponse('Invalid token');
+ }
+
+ $request = $request->withAttribute('userId', $userId);
+
+ return $handler->handle($request);
+ }
+
+ private function createUnauthorizedResponse(string $message): ResponseInterface
+ {
+ return new \GuzzleHttp\Psr7\Response(401, ['Content-Type' => 'application/json'], json_encode([
+ 'status' => 'error',
+ 'message' => $message
+ ]));
+ }
+}
\ No newline at end of file
diff --git a/php8/src/Infrastructure/Repositories/FileItemRepository.php b/php8/src/Infrastructure/Repositories/FileItemRepository.php
new file mode 100755
index 0000000..b5079e5
--- /dev/null
+++ b/php8/src/Infrastructure/Repositories/FileItemRepository.php
@@ -0,0 +1,172 @@
+storagePath = $storagePath;
+ $this->logger = $logger;
+
+ $this->ensureStorageDirectoryExists();
+ $this->loadItems();
+ }
+
+ public function save(Item $item): void
+ {
+ $this->items[$item->getId()] = $item->toArray();
+ $this->persistItems();
+
+ $this->logger->info("Item saved: {$item->getId()}");
+ }
+
+ public function findById(string $id): ?Item
+ {
+ if (!isset($this->items[$id])) {
+ return null;
+ }
+
+ try {
+ return Item::fromArray($this->items[$id]);
+ } catch (DomainException $e) {
+ $this->logger->error("Failed to create item from data: " . $e->getMessage());
+ return null;
+ }
+ }
+
+ public function findByUserId(string $userId): array
+ {
+ $result = [];
+
+ foreach ($this->items as $itemData) {
+ if ($itemData['userId'] === $userId) {
+ try {
+ $result[] = Item::fromArray($itemData);
+ } catch (DomainException $e) {
+ $this->logger->error("Failed to create item from data: " . $e->getMessage());
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ public function findAll(): array
+ {
+ $result = [];
+
+ foreach ($this->items as $itemData) {
+ try {
+ $result[] = Item::fromArray($itemData);
+ } catch (DomainException $e) {
+ $this->logger->error("Failed to create item from data: " . $e->getMessage());
+ }
+ }
+
+ return $result;
+ }
+
+ public function findExpiredItems(): array
+ {
+ return $this->findExpired();
+ }
+
+ public function delete(string $id): void
+ {
+ if (!isset($this->items[$id])) {
+ throw new ApplicationException("Item '{$id}' not found");
+ }
+
+ unset($this->items[$id]);
+ $this->persistItems();
+
+ $this->logger->info("Item deleted: {$id}");
+ }
+
+ public function findExpired(): array
+ {
+ $result = [];
+ $now = new \DateTimeImmutable();
+
+ foreach ($this->items as $itemData) {
+ $expirationDate = new \DateTimeImmutable($itemData['expirationDate']);
+
+ if ($expirationDate <= $now) {
+ try {
+ $result[] = Item::fromArray($itemData);
+ } catch (DomainException $e) {
+ $this->logger->error("Failed to create item from data: " . $e->getMessage());
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ public function exists(string $id): bool
+ {
+ return isset($this->items[$id]);
+ }
+
+ private function ensureStorageDirectoryExists(): void
+ {
+ if (!is_dir($this->storagePath)) {
+ if (!mkdir($this->storagePath, 0755, true)) {
+ throw new ApplicationException("Failed to create storage directory: {$this->storagePath}");
+ }
+ }
+ }
+
+ private function loadItems(): void
+ {
+ $filename = $this->storagePath . '/items.json';
+
+ if (!file_exists($filename)) {
+ return;
+ }
+
+ $content = file_get_contents($filename);
+
+ if ($content === false) {
+ throw new ApplicationException("Failed to read items file: {$filename}");
+ }
+
+ $data = json_decode($content, true);
+
+ if ($data === null) {
+ throw new ApplicationException("Failed to decode items JSON: " . json_last_error_msg());
+ }
+
+ $this->items = $data;
+ $this->logger->info("Loaded " . count($this->items) . " items from storage");
+ }
+
+ private function persistItems(): void
+ {
+ $filename = $this->storagePath . '/items.json';
+ $content = json_encode($this->items, JSON_PRETTY_PRINT);
+
+ if ($content === false) {
+ throw new ApplicationException("Failed to encode items to JSON");
+ }
+
+ $result = file_put_contents($filename, $content);
+
+ if ($result === false) {
+ throw new ApplicationException("Failed to write items file: {$filename}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/php8/src/Infrastructure/Repositories/FileUserRepository.php b/php8/src/Infrastructure/Repositories/FileUserRepository.php
new file mode 100755
index 0000000..533a144
--- /dev/null
+++ b/php8/src/Infrastructure/Repositories/FileUserRepository.php
@@ -0,0 +1,145 @@
+storagePath = $storagePath;
+ $this->logger = $logger;
+
+ $this->ensureStorageDirectoryExists();
+ $this->loadUsers();
+ }
+
+ public function save(User $user): void
+ {
+ $this->users[$user->getId()] = $user->toArray();
+ $this->persistUsers();
+
+ $this->logger->info("User saved: {$user->getId()}");
+ }
+
+ public function findById(string $id): ?User
+ {
+ if (!isset($this->users[$id])) {
+ return null;
+ }
+
+ try {
+ return User::fromArray($this->users[$id]);
+ } catch (DomainException $e) {
+ $this->logger->error("Failed to create user from data: " . $e->getMessage());
+ return null;
+ }
+ }
+
+ public function findByUsername(string $username): ?User
+ {
+ foreach ($this->users as $userData) {
+ if ($userData['username'] === $username) {
+ try {
+ return User::fromArray($userData);
+ } catch (DomainException $e) {
+ $this->logger->error("Failed to create user from data: " . $e->getMessage());
+ return null;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public function exists(string $id): bool
+ {
+ return isset($this->users[$id]);
+ }
+
+ private function ensureStorageDirectoryExists(): void
+ {
+ if (!is_dir($this->storagePath)) {
+ if (!mkdir($this->storagePath, 0755, true)) {
+ throw new ApplicationException("Failed to create storage directory: {$this->storagePath}");
+ }
+ }
+ }
+
+ private function loadUsers(): void
+ {
+ $filename = $this->storagePath . '/users.json';
+
+ if (!file_exists($filename)) {
+ $this->createDefaultUsers();
+ return;
+ }
+
+ $content = file_get_contents($filename);
+
+ if ($content === false) {
+ throw new ApplicationException("Failed to read users file: {$filename}");
+ }
+
+ $data = json_decode($content, true);
+
+ if ($data === null) {
+ throw new ApplicationException("Failed to decode users JSON: " . json_last_error_msg());
+ }
+
+ $this->users = $data;
+ $this->logger->info("Loaded " . count($this->users) . " users from storage");
+ }
+
+ private function persistUsers(): void
+ {
+ $filename = $this->storagePath . '/users.json';
+ $content = json_encode($this->users, JSON_PRETTY_PRINT);
+
+ if ($content === false) {
+ throw new ApplicationException("Failed to encode users to JSON");
+ }
+
+ $result = file_put_contents($filename, $content);
+
+ if ($result === false) {
+ throw new ApplicationException("Failed to write users file: {$filename}");
+ }
+ }
+
+ private function createDefaultUsers(): void
+ {
+ $defaultUsers = [
+ [
+ 'id' => '1000',
+ 'username' => 'admin',
+ 'passwordHash' => password_hash('admin', PASSWORD_DEFAULT),
+ 'createdAt' => (new \DateTimeImmutable())->format('Y-m-d\TH:i:s.uP'),
+ 'updatedAt' => null
+ ],
+ [
+ 'id' => '1001',
+ 'username' => 'user',
+ 'passwordHash' => password_hash('user', PASSWORD_DEFAULT),
+ 'createdAt' => (new \DateTimeImmutable())->format('Y-m-d\TH:i:s.uP'),
+ 'updatedAt' => null
+ ]
+ ];
+
+ $this->users = $defaultUsers;
+ $this->persistUsers();
+
+ $this->logger->info("Created default users");
+ }
+}
\ No newline at end of file
diff --git a/php8/src/WebApi/Controllers/AuthController.php b/php8/src/WebApi/Controllers/AuthController.php
new file mode 100755
index 0000000..c1f4219
--- /dev/null
+++ b/php8/src/WebApi/Controllers/AuthController.php
@@ -0,0 +1,43 @@
+loginUser = $loginUser;
+ }
+
+ public function login(Request $request, Response $response): Response
+ {
+ try {
+ $data = $this->getParsedBody($request);
+
+ if (!isset($data['username'], $data['password'])) {
+ return $this->createErrorResponse($response, 'Username and password are required', 400);
+ }
+
+ $token = $this->loginUser->execute($data['username'], $data['password']);
+
+ return $this->createSuccessResponse($response, [
+ 'token' => $token,
+ 'tokenType' => 'Bearer',
+ 'expiresIn' => 3600
+ ]);
+ } catch (ApplicationException $e) {
+ return $this->createErrorResponse($response, $e->getMessage(), 401);
+ } catch (\Exception $e) {
+ return $this->createErrorResponse($response, 'Internal server error', 500);
+ }
+ }
+}
\ No newline at end of file
diff --git a/php8/src/WebApi/Controllers/BaseController.php b/php8/src/WebApi/Controllers/BaseController.php
new file mode 100755
index 0000000..c8b95ac
--- /dev/null
+++ b/php8/src/WebApi/Controllers/BaseController.php
@@ -0,0 +1,59 @@
+ 'success',
+ 'data' => $data
+ ];
+
+ $response->getBody()->write(json_encode($payload));
+ return $response
+ ->withHeader('Content-Type', 'application/json')
+ ->withStatus($statusCode);
+ }
+
+ protected function createErrorResponse(Response $response, string $message, int $statusCode): Response
+ {
+ $payload = [
+ 'status' => 'error',
+ 'message' => $message
+ ];
+
+ $response->getBody()->write(json_encode($payload));
+ return $response
+ ->withHeader('Content-Type', 'application/json')
+ ->withStatus($statusCode);
+ }
+
+ protected function getParsedBody(Request $request): ?array
+ {
+ $body = $request->getParsedBody();
+
+ if (is_array($body)) {
+ return $body;
+ }
+
+ $contentType = $request->getHeaderLine('Content-Type');
+
+ if (strpos($contentType, 'application/json') !== false) {
+ $rawBody = $request->getBody()->getContents();
+ $parsedBody = json_decode($rawBody, true);
+
+ if (json_last_error() === JSON_ERROR_NONE) {
+ return $parsedBody;
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/php8/src/WebApi/Controllers/StoreController.php b/php8/src/WebApi/Controllers/StoreController.php
new file mode 100755
index 0000000..7ae6987
--- /dev/null
+++ b/php8/src/WebApi/Controllers/StoreController.php
@@ -0,0 +1,112 @@
+addItem = $addItem;
+ $this->getItem = $getItem;
+ $this->listItems = $listItems;
+ $this->deleteItem = $deleteItem;
+ }
+
+ public function addItem(Request $request, Response $response): Response
+ {
+ try {
+ $userId = $request->getAttribute('userId');
+ $data = $request->getParsedBody();
+
+ if (!isset($data['name'], $data['expirationDate'], $data['orderUrl'])) {
+ return $this->createErrorResponse($response, 'Missing required fields', 400);
+ }
+
+ $itemId = $this->addItem->execute(
+ $data['name'],
+ $data['expirationDate'],
+ $data['orderUrl'],
+ $userId
+ );
+
+ return $this->createSuccessResponse($response, ['id' => $itemId], 201);
+ } catch (ApplicationException $e) {
+ return $this->createErrorResponse($response, $e->getMessage(), 400);
+ } catch (\Exception $e) {
+ return $this->createErrorResponse($response, 'Internal server error', 500);
+ }
+ }
+
+ public function getItem(Request $request, Response $response, array $args): Response
+ {
+ try {
+ $userId = $request->getAttribute('userId');
+ $itemId = $args['id'] ?? '';
+
+ if (empty($itemId)) {
+ return $this->createErrorResponse($response, 'Item ID is required', 400);
+ }
+
+ $itemData = $this->getItem->execute($itemId, $userId);
+
+ return $this->createSuccessResponse($response, $itemData);
+ } catch (ApplicationException $e) {
+ return $this->createErrorResponse($response, $e->getMessage(), 404);
+ } catch (\Exception $e) {
+ return $this->createErrorResponse($response, 'Internal server error', 500);
+ }
+ }
+
+ public function listItems(Request $request, Response $response): Response
+ {
+ try {
+ $userId = $request->getAttribute('userId');
+ $items = $this->listItems->execute($userId);
+
+ return $this->createSuccessResponse($response, $items);
+ } catch (ApplicationException $e) {
+ return $this->createErrorResponse($response, $e->getMessage(), 400);
+ } catch (\Exception $e) {
+ return $this->createErrorResponse($response, 'Internal server error', 500);
+ }
+ }
+
+ public function deleteItem(Request $request, Response $response, array $args): Response
+ {
+ try {
+ $userId = $request->getAttribute('userId');
+ $itemId = $args['id'] ?? '';
+
+ if (empty($itemId)) {
+ return $this->createErrorResponse($response, 'Item ID is required', 400);
+ }
+
+ $this->deleteItem->execute($itemId, $userId);
+
+ return $this->createSuccessResponse($response, null, 204);
+ } catch (ApplicationException $e) {
+ return $this->createErrorResponse($response, $e->getMessage(), 404);
+ } catch (\Exception $e) {
+ return $this->createErrorResponse($response, 'Internal server error', 500);
+ }
+ }
+}
\ No newline at end of file
diff --git a/php8/tests/Integration/FileItemRepositoryTest.php b/php8/tests/Integration/FileItemRepositoryTest.php
new file mode 100755
index 0000000..3849891
--- /dev/null
+++ b/php8/tests/Integration/FileItemRepositoryTest.php
@@ -0,0 +1,207 @@
+testStoragePath = $GLOBALS['test_storage_path'];
+
+ // Clean up any existing test files
+ array_map('unlink', glob("$this->testStoragePath/*.json"));
+
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->repository = new FileItemRepository($this->testStoragePath, $this->logger);
+ }
+
+ protected function tearDown(): void
+ {
+ // Clean up test files
+ array_map('unlink', glob("$this->testStoragePath/*.json"));
+ }
+
+ public function testSaveShouldCreateFileForNewItem(): void
+ {
+ $item = new Item(
+ 'test-id',
+ 'Test Item',
+ new DateTimeImmutable('+1 day'),
+ 'http://example.com/order',
+ 'user-id'
+ );
+
+ $this->repository->save($item);
+
+ $filePath = $this->testStoragePath . '/items.json';
+ $this->assertFileExists($filePath);
+
+ $fileContent = file_get_contents($filePath);
+ $data = json_decode($fileContent, true);
+
+ $this->assertIsArray($data);
+ $this->assertCount(1, $data);
+
+ // Compare the essential fields
+ $this->assertEquals($item->getId(), $data[$item->getId()]['id']);
+ $this->assertEquals($item->getName(), $data[$item->getId()]['name']);
+ $this->assertEquals($item->getOrderUrl(), $data[$item->getId()]['orderUrl']);
+ $this->assertEquals($item->getUserId(), $data[$item->getId()]['userId']);
+ }
+
+ public function testFindByIdShouldReturnItemIfExists(): void
+ {
+ $item = new Item(
+ 'test-id',
+ 'Test Item',
+ new DateTimeImmutable('+1 day'),
+ 'http://example.com/order',
+ 'user-id'
+ );
+
+ $this->repository->save($item);
+
+ $foundItem = $this->repository->findById('test-id');
+
+ $this->assertNotNull($foundItem);
+ $this->assertSame($item->getId(), $foundItem->getId());
+ $this->assertSame($item->getName(), $foundItem->getName());
+ }
+
+ public function testFindByIdShouldReturnNullIfNotExists(): void
+ {
+ $foundItem = $this->repository->findById('non-existent-id');
+ $this->assertNull($foundItem);
+ }
+
+ public function testFindByUserIdShouldReturnItemsForUser(): void
+ {
+ $item1 = new Item(
+ 'test-id-1',
+ 'Test Item 1',
+ new DateTimeImmutable('+1 day'),
+ 'http://example.com/order1',
+ 'user-id-1'
+ );
+
+ $item2 = new Item(
+ 'test-id-2',
+ 'Test Item 2',
+ new DateTimeImmutable('+2 days'),
+ 'http://example.com/order2',
+ 'user-id-2'
+ );
+
+ $item3 = new Item(
+ 'test-id-3',
+ 'Test Item 3',
+ new DateTimeImmutable('+3 days'),
+ 'http://example.com/order3',
+ 'user-id-1'
+ );
+
+ $this->repository->save($item1);
+ $this->repository->save($item2);
+ $this->repository->save($item3);
+
+ $userItems = $this->repository->findByUserId('user-id-1');
+
+ $this->assertCount(2, $userItems);
+ $this->assertContainsEquals($item1->getId(), array_map(fn($i) => $i->getId(), $userItems));
+ $this->assertContainsEquals($item3->getId(), array_map(fn($i) => $i->getId(), $userItems));
+ }
+
+ public function testFindAllShouldReturnAllItems(): void
+ {
+ $item1 = new Item(
+ 'test-id-1',
+ 'Test Item 1',
+ new DateTimeImmutable('+1 day'),
+ 'http://example.com/order1',
+ 'user-id-1'
+ );
+
+ $item2 = new Item(
+ 'test-id-2',
+ 'Test Item 2',
+ new DateTimeImmutable('+2 days'),
+ 'http://example.com/order2',
+ 'user-id-2'
+ );
+
+ $this->repository->save($item1);
+ $this->repository->save($item2);
+
+ $allItems = $this->repository->findAll();
+
+ $this->assertCount(2, $allItems);
+ $this->assertContainsEquals($item1->getId(), array_map(fn($i) => $i->getId(), $allItems));
+ $this->assertContainsEquals($item2->getId(), array_map(fn($i) => $i->getId(), $allItems));
+ }
+
+ public function testDeleteShouldRemoveItem(): void
+ {
+ $item = new Item(
+ 'test-id',
+ 'Test Item',
+ new DateTimeImmutable('+1 day'),
+ 'http://example.com/order',
+ 'user-id'
+ );
+
+ $this->repository->save($item);
+
+ $this->repository->delete('test-id');
+
+ $foundItem = $this->repository->findById('test-id');
+ $this->assertNull($foundItem);
+ }
+
+ public function testDeleteShouldThrowExceptionForNonExistentItem(): void
+ {
+ $this->expectException(ApplicationException::class);
+ $this->expectExceptionMessage("Item 'non-existent-id' not found");
+
+ $this->repository->delete('non-existent-id');
+ }
+
+ public function testFindExpiredItemsShouldReturnOnlyExpiredItems(): void
+ {
+ $expiredItem = new Item(
+ 'expired-id',
+ 'Expired Item',
+ new DateTimeImmutable('-1 day'),
+ 'http://example.com/expired-order',
+ 'user-id'
+ );
+
+ $validItem = new Item(
+ 'valid-id',
+ 'Valid Item',
+ new DateTimeImmutable('+1 day'),
+ 'http://example.com/valid-order',
+ 'user-id'
+ );
+
+ $this->repository->save($expiredItem);
+ $this->repository->save($validItem);
+
+ $expiredItems = $this->repository->findExpiredItems();
+
+ $this->assertCount(1, $expiredItems);
+ $this->assertSame($expiredItem->getId(), $expiredItems[0]->getId());
+ }
+}
\ No newline at end of file
diff --git a/php8/tests/Unit/AddItemTest.php b/php8/tests/Unit/AddItemTest.php
new file mode 100755
index 0000000..d6fef51
--- /dev/null
+++ b/php8/tests/Unit/AddItemTest.php
@@ -0,0 +1,212 @@
+itemRepository = $this->createMock(IItemRepository::class);
+ $this->orderService = $this->createMock(IOrderService::class);
+ $this->timeProvider = $this->createMock(ITimeProvider::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->addItem = new AddItem(
+ $this->itemRepository,
+ $this->orderService,
+ $this->timeProvider,
+ $this->logger
+ );
+ }
+
+ public function testExecuteShouldSaveItemWhenNotExpired(): void
+ {
+ $userId = 'test-user-id';
+ $itemName = 'Test Item';
+ $expirationDate = new DateTimeImmutable('+1 day');
+ $orderUrl = 'http://example.com/order';
+
+ $this->timeProvider->method('now')
+ ->willReturn(new DateTimeImmutable());
+
+ // Capture the saved item
+ $savedItem = null;
+ $this->itemRepository->expects($this->once())
+ ->method('save')
+ ->with($this->callback(function (Item $item) use ($itemName, $orderUrl, $userId, &$savedItem) {
+ $savedItem = $item;
+ return $item->getName() === $itemName &&
+ $item->getOrderUrl() === $orderUrl &&
+ $item->getUserId() === $userId &&
+ !$item->isOrdered();
+ }));
+
+ $this->orderService->expects($this->never())
+ ->method('orderItem');
+
+ // Mock findById to return the saved item
+ $this->itemRepository->expects($this->once())
+ ->method('findById')
+ ->willReturnCallback(function ($id) use (&$savedItem) {
+ return $savedItem;
+ });
+
+ $resultId = $this->addItem->execute($itemName, $expirationDate->format('Y-m-d H:i:s'), $orderUrl, $userId);
+
+ // Retrieve the saved item to verify its properties
+ $result = $this->itemRepository->findById($resultId);
+
+ $this->assertSame($itemName, $result->getName());
+ // Compare DateTime objects without microseconds
+ $this->assertEquals($expirationDate->format('Y-m-d H:i:s'), $result->getExpirationDate()->format('Y-m-d H:i:s'));
+ $this->assertSame($orderUrl, $result->getOrderUrl());
+ $this->assertSame($userId, $result->getUserId());
+ $this->assertFalse($result->isOrdered());
+ }
+
+ public function testExecuteShouldPlaceOrderWhenItemIsExpired(): void
+ {
+ $userId = 'test-user-id';
+ $itemName = 'Test Item';
+ $expirationDate = new DateTimeImmutable('-1 day');
+ $orderUrl = 'http://example.com/order';
+
+ $this->timeProvider->method('now')
+ ->willReturn(new DateTimeImmutable());
+
+ $savedItem = null;
+ $orderedItem = null;
+ $this->itemRepository->expects($this->once())
+ ->method('save')
+ ->with($this->callback(function (Item $item) use (&$savedItem) {
+ $savedItem = $item;
+ return true;
+ }));
+
+ $this->orderService->expects($this->once())
+ ->method('orderItem')
+ ->with($this->callback(function (Item $item) use (&$orderedItem) {
+ $orderedItem = $item;
+ return true;
+ }));
+
+ // Mock findById to return the ordered item
+ $this->itemRepository->expects($this->once())
+ ->method('findById')
+ ->willReturnCallback(function ($id) use (&$orderedItem) {
+ // Mark the item as ordered before returning it
+ if ($orderedItem) {
+ $orderedItem->markAsOrdered();
+ }
+ return $orderedItem;
+ });
+
+ $resultId = $this->addItem->execute($itemName, $expirationDate->format('Y-m-d H:i:s'), $orderUrl, $userId);
+
+ // Retrieve the saved item to verify its properties
+ $result = $this->itemRepository->findById($resultId);
+
+ $this->assertTrue($result->isOrdered());
+ }
+
+ public function testExecuteShouldThrowExceptionWhenItemNameIsEmpty(): void
+ {
+ $userId = 'test-user-id';
+ $itemName = '';
+ $expirationDate = new DateTimeImmutable('+1 day');
+ $orderUrl = 'http://example.com/order';
+
+ $this->expectException(\AutoStore\Application\Exceptions\ApplicationException::class);
+ $this->expectExceptionMessage('Failed to add item: Item name cannot be empty');
+
+ $this->addItem->execute($itemName, $expirationDate->format('Y-m-d H:i:s'), $orderUrl, $userId);
+ }
+
+ public function testExecuteShouldThrowExceptionWhenOrderUrlIsEmpty(): void
+ {
+ $userId = 'test-user-id';
+ $itemName = 'Test Item';
+ $expirationDate = new DateTimeImmutable('+1 day');
+ $orderUrl = '';
+
+ $this->expectException(\AutoStore\Application\Exceptions\ApplicationException::class);
+ $this->expectExceptionMessage('Failed to add item: Order URL cannot be empty');
+
+ $this->addItem->execute($itemName, $expirationDate->format('Y-m-d H:i:s'), $orderUrl, $userId);
+ }
+
+ public function testExecuteShouldThrowExceptionWhenUserIdIsEmpty(): void
+ {
+ $userId = '';
+ $itemName = 'Test Item';
+ $expirationDate = new DateTimeImmutable('+1 day');
+ $orderUrl = 'http://example.com/order';
+
+ $this->expectException(\AutoStore\Application\Exceptions\ApplicationException::class);
+ $this->expectExceptionMessage('Failed to add item: User ID cannot be empty');
+
+ $this->addItem->execute($itemName, $expirationDate->format('Y-m-d H:i:s'), $orderUrl, $userId);
+ }
+
+ public function testExecuteShouldLogErrorWhenOrderServiceFails(): void
+ {
+ $userId = 'test-user-id';
+ $itemName = 'Test Item';
+ $expirationDate = new DateTimeImmutable('-1 day');
+ $orderUrl = 'http://example.com/order';
+
+ $this->timeProvider->method('now')
+ ->willReturn(new DateTimeImmutable());
+
+ // Mock the repository to return a saved item
+ $savedItem = null;
+ $this->itemRepository->expects($this->once())
+ ->method('save')
+ ->with($this->callback(function (Item $item) use (&$savedItem) {
+ $savedItem = $item;
+ return true;
+ }));
+
+ // Mock the order service to throw an exception
+ $this->orderService->expects($this->once())
+ ->method('orderItem')
+ ->willThrowException(new \RuntimeException('Order service failed'));
+
+ $this->logger->expects($this->once())
+ ->method('error')
+ ->with($this->stringContains('Failed to place order for expired item'));
+
+ // Mock findById to return the saved item
+ $this->itemRepository->expects($this->once())
+ ->method('findById')
+ ->willReturnCallback(function ($id) use (&$savedItem) {
+ return $savedItem;
+ });
+
+ // The handler should not throw an exception when the order service fails
+ // It should log the error and continue
+ $resultId = $this->addItem->execute($itemName, $expirationDate->format('Y-m-d H:i:s'), $orderUrl, $userId);
+
+ // Retrieve the saved item to verify its properties
+ $result = $this->itemRepository->findById($resultId);
+
+ $this->assertFalse($result->isOrdered());
+ }
+}
\ No newline at end of file
diff --git a/php8/tests/bootstrap.php b/php8/tests/bootstrap.php
new file mode 100755
index 0000000..1790fbf
--- /dev/null
+++ b/php8/tests/bootstrap.php
@@ -0,0 +1,15 @@
+ /dev/null && pwd )
+cd "$SCRIPT_DIR"
+
if [ -z "$TEST_SERVER_ADDRESS" ]; then
source export.sh
fi
diff --git a/testing/tavern/tavern-run-single.sh b/testing/tavern/tavern-run-single.sh
index 51c1a00..3dcc682 100755
--- a/testing/tavern/tavern-run-single.sh
+++ b/testing/tavern/tavern-run-single.sh
@@ -1,5 +1,8 @@
#!/usr/bin/env bash
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+cd "$SCRIPT_DIR"
+
if [ -z "$1" ]; then
echo "Usage: $0 "
exit 1