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