commit b516ca07dca4668d07dddd6c07bb950840097476 Author: Egor Isaev Date: Thu Jun 4 10:04:19 2026 +0300 Initial commit: Bicycle PHP MVC micro-framework Core MVC, HTTP client, Session, Cookie, Config, HTTPException — 111 PHPUnit tests. Co-Authored-By: Claude Sonnet 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5936158 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.idea/ +/.git +*.bat + +config.php +config.json +App/config/config.local.php + +.htaccess + +/App/media/img/no_img/* +!/App/media/img/no_img/no_image.png + + +**/files/**/* +!**/files/**/ +!**/files/**/.gitkeep +/tools/* diff --git a/App/Controller/IndexController.php b/App/Controller/IndexController.php new file mode 100644 index 0000000..7c254a9 --- /dev/null +++ b/App/Controller/IndexController.php @@ -0,0 +1,42 @@ +execute( + HttpRequest::factory('https://jsonplaceholder.typicode.com/todos/1') + ); + + $todo = $response->isSuccess() ? $response->json() : []; + + return $this->render('index', ['todo' => $todo]); + } + + public function postAction(): string + { + // POST с JSON-телом + $response = (new Curl())->execute( + HttpRequest::factory('https://jsonplaceholder.typicode.com/posts') + ->method('POST') + ->json(['title' => 'Bicycle', 'body' => 'test', 'userId' => 1]) + ); + + $created = $response->isSuccess() ? $response->json() : []; + + return $this->render('index', ['created' => $created]); + } +} diff --git a/App/config/config.php b/App/config/config.php new file mode 100644 index 0000000..fe95126 --- /dev/null +++ b/App/config/config.php @@ -0,0 +1,30 @@ + [ + 'default' => [ + 'driver' => 'pdo', + 'connection' => [ + 'host' => 'db', + 'dbname' => 'bicycle', + 'user' => 'root', + 'password' => '', + ], + ], + ], + + 'session' => [ + 'cookie_params' => [ + 'lifetime' => 0, + 'secure' => false, + 'httponly' => true, + 'samesite' => 'Lax', + ], + ], + +]; diff --git a/App/view/Index/index.html b/App/view/Index/index.html new file mode 100644 index 0000000..ad16d76 --- /dev/null +++ b/App/view/Index/index.html @@ -0,0 +1,11 @@ + + + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7073fea --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,247 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project overview + +**Bicycle** — самописный PHP MVC micro-framework (PHP >= 8.2). Название отражает идиому «изобрести велосипед». + +## Commands + +```bash +# Установка зависимостей +composer install + +# Обновление автозагрузчика после добавления классов +composer dump-autoload + +# Запуск тестов через Docker +docker exec bicycle vendor/bin/phpunit +``` + +Проект запускается как веб-приложение через Apache + PHP 8.2 в контейнере `bicycle`. + +## Architecture + +### Directory layout + +``` +index.php — точка входа; константы EXT, DOCROOT, APPPATH, SYSPATH +System/Classes/ — ядро фреймворка (namespace System\Classes\) +System/Classes/HTTP/ — HTTP-инфраструктура: интерфейсы, заголовки, исключения +System/Classes/HTTP/Client/ — исходящие HTTP-запросы (Curl, Request, Response) +System/Classes/HTTP/Exception/ — HTTPException_302/403/404 +App/Controller/ — контроллеры приложения (namespace App\Controller\) +App/Classes/ — вспомогательные классы приложения (namespace App\Classes\) +App/config/ — конфиги (config.php; config.local.php — в .gitignore) +App/view/ — шаблоны приложения (.html файлы с PHP-кодом) +App/media/ — статические ресурсы (js, css, img) +Services/DataBase/ — сервис базы данных (PDO) +Services/Mail/ — сервис почты (PHPMailer) +Services/PDF/ — генерация PDF (dompdf) +System/view/ — системные шаблоны (ошибки 404/500/403, exception) +tests/ — PHPUnit тесты +``` + +### Autoload (composer.json) + +PSR-4 с относительными путями: +- `System\` → `System/` +- `App\` → `App/` +- `Services\` → `Services/` +- `Tests\` → `tests/` (dev) + +PhpStorm может показывать предупреждение «Namespace doesn't match PSR-4» — это ложное срабатывание. Отключается в `Settings → Editor → Inspections → PHP → General`. + +### Namespaces + +- `System/Classes/*.php` → `namespace System\Classes;` +- `System/Classes/HTTP/*.php` → `namespace System\Classes\HTTP;` +- `System/Classes/HTTP/Client/*.php` → `namespace System\Classes\HTTP\Client;` +- `System/Classes/HTTP/Exception/*.php` → `namespace System\Classes\HTTP\Exception;` +- `App/Controller/*.php` → `namespace App\Controller;` +- `App/Classes/*.php` → `namespace App\Classes;` +- `Services/**/*.php` → `namespace Services\...;` + +### Core classes + +| Класс | Роль | +|---|---| +| `System\Classes\Core` | Bootstrap, обработка ошибок, поиск файлов (`findFile`), константы среды | +| `System\Classes\Controller` | Базовый контроллер; `render()` строит двухуровневый вывод: layout + content | +| `System\Classes\View` | Рендеринг `.html`-шаблонов через `ob_start` + `extract` + `include` | +| `System\Classes\Route` | Автоматический роутинг: парсит URI, ищет контроллер в `App/Controller/` | +| `System\Classes\Request` | Входящий HTTP-запрос; реализует `HTTP\Request`; `post()`, `query()`, `body()`, `is_ajax()` | +| `System\Classes\Response` | HTTP-ответ; `body()` — геттер/сеттер содержимого | +| `System\Classes\MyException` | Кастомное исключение; зарегистрировано через `set_exception_handler` | +| `System\Classes\Cookie` | Статический хелпер для работы с куками: `get()`, `set()`, `delete()` | +| `System\Classes\Session` | Синглтон сессии: `instance(string $name)`, `get/set/delete/destroy/regenerate` | +| `System\Classes\Config` | Статический конфиг: lazy-load `App/config/config.php` + merge `config.local.php` | +| `System\Classes\HTTP` | `redirect()`, `request_headers()` | +| `System\Classes\HTTP\Header` | Extends `ArrayObject`; `send()`, `__toString()` → RFC-формат | +| `System\Classes\HTTP\Message` | Интерфейс: `protocol()`, `headers()`, `body()`, `render()` | +| `System\Classes\HTTP\Request` | Интерфейс входящего запроса; константы методов GET/POST/PUT/... | +| `System\Classes\HTTP\HTTPException` | HTTP-исключение; `factory(int $code)`, `get_response(): Response` | +| `System\Classes\HTTP\Exception\HTTPException_302` | Редирект через `HTTP::redirect()` | +| `System\Classes\HTTP\Exception\HTTPException_403` | 403 Forbidden | +| `System\Classes\HTTP\Exception\HTTPException_404` | 404 Not Found | +| `System\Classes\HTTP\Client\Curl` | Исполнитель исходящих HTTP-запросов через cURL | +| `System\Classes\HTTP\Client\Request` | Билдер исходящего запроса: `method()`, `header()`, `json()`, `timeout()` | +| `System\Classes\HTTP\Client\Response` | Ответ внешнего запроса: `status()`, `body()`, `json()`, `isSuccess()` | + +### Request lifecycle + +``` +index.php + → Core::init() + → Request::factory() # detect_uri(), Route::resolve(), заполняет post/query/body/method + → Request::execute() # require_once контроллера, вызывает {action}Action() + → Response->body() # строка с HTML + → echo +``` + +### Internal Request (System\Classes\Request) + +Входящий запрос от браузера. Реализует `System\Classes\HTTP\Request`. + +- `factory()` — создаёт экземпляр, заполняет `$_GET`, `$_POST`, метод, тело, `X-Requested-With` +- `$initial` — первый запрос; `$current` — текущий +- `post($key?, $value?)` — getter/setter POST-данных +- `query($key?, $value?)` — getter/setter GET-параметров +- `body($content?)` — getter/setter тела запроса +- `method($method?)` — getter/setter HTTP-метода +- `is_ajax()` — проверяет `X-Requested-With: XMLHttpRequest` +- `detect_uri()` — определяет URI через `PATH_INFO` → `REQUEST_URI` → `PHP_SELF` + +### External HTTP Client (System\Classes\HTTP\Client\*) + +Исходящие запросы к внешним API. + +```php +use System\Classes\HTTP\Client\{Request as HttpRequest, Curl}; + +$response = (new Curl())->execute( + HttpRequest::factory('https://api.example.com/data') + ->method('POST') + ->json(['key' => 'value']) + ->timeout(10) +); + +if ($response->isSuccess()) { + $data = $response->json(); +} +``` + +### Session (System\Classes\Session) + +Синглтон, поддерживает именованные сессии. + +```php +use System\Classes\Session; + +$session = Session::instance(); // сессия 'main' +$session->set('user_id', 42); +$id = $session->get('user_id'); +$session->delete('user_id'); +$session->regenerate(); // новый session_id, старые данные сохраняются +$session->destroy(); // уничтожить сессию +``` + +### Cookie (System\Classes\Cookie) + +Статический хелпер. + +```php +use System\Classes\Cookie; + +Cookie::set('token', 'abc123', time() + 3600); +$val = Cookie::get('token'); +Cookie::delete('token'); +``` + +Свойства по умолчанию: `$expiration=0`, `$path='/'`, `$domain=null`, `$secure=false`, `$httponly=true`, `$samesite='Lax'`. + +### Config (System\Classes\Config) + +Lazy-load конфига из `App/config/config.php`. Если существует `App/config/config.local.php` — глубоко мержится поверх базового. + +```php +use System\Classes\Config; + +$db = Config::get('db'); // весь раздел +$params = Config::get('session', 'cookie_params'); // вложенный ключ +Config::set('foo', ['bar' => 'baz']); // переопределить в runtime +``` + +`config.local.php` исключён из git (`.gitignore`) — используется для переопределений на конкретном хосте. + +### HTTPException (System\Classes\HTTP\HTTPException) + +```php +use System\Classes\HTTP\HTTPException; + +throw HTTPException::factory(404, 'Страница :url не найдена', [':url' => $uri]); +// или через get_response() без throw: +$response = HTTPException::factory(403)->get_response(); +``` + +`factory(int $code)` создаёт `HTTPException_302/403/404` или базовый класс для неизвестных кодов. + +### Routing (Route::resolve) + +Автоматический роутинг без конфигурации, поиск контроллера по файловой системе в `App/Controller/`: + +| URL | Контроллер | Метод | +|-----|-----------|-------| +| `/` | `IndexController` | `indexAction()` | +| `/users` | `UsersController` | `indexAction()` | +| `/users/list` | `UsersController` | `listAction()` | +| `/users/list/42` | `UsersController` | `listAction()`, `param(0)=42` | +| `/admin/users/edit` | `Admin/UsersController` | `editAction()` (subdir) | + +Поиск файлов и имён контроллеров — без учёта регистра. Если контроллер не найден, Route возвращает flat-роутинг (не теряет action). + +### View path convention + +- Шаблон контента: `App/view/{Controller}/{template}.html` +- Layout: `System/view/views/{layout}.html` +- `Core::findFile($dir, $file, 'html')` ищет в `APPPATH/{dir}/{file}.html`, затем в `SYSPATH/{dir}/{file}.html` +- `$_paths` инициализируется лениво при первом вызове `findFile()` +- `Controller::render()` автоматически определяет `$dir` из имени класса (`App\Controller\FooController` → `view/Foo`) + +### Environment + +Среда задаётся через `SetEnv MMH_ENV` в `.htaccess` (PRODUCTION / STAGING / TESTING / DEVELOPMENT). +По умолчанию — `DEVELOPMENT`. Значение читается в `index.php` через `getenv('MMH_ENV')`. + +### Apache + +`ServerName localhost` добавлен в `/home/isaevea/http/docker-dev/conf/Bicycle/apache2/apache2.conf`. +**Никогда не трогать** `/home/isaevea/http/docker-dev/docker/php-apache/8.2.8/Dockerfile`. + +### Tests + +PHPUnit 11 в Docker-контейнере `bicycle`. Bootstrap: `tests/bootstrap.php`. +Константы определяются в bootstrap. `Core::init()` не вызывается — `findFile()` инициализирует `$_paths` лениво. + +**PHPStorm**: интерпретатор PHP — Docker (`bicycle`), volume маппинг `/home/isaevea/http/Bicycle` → `/opt/project`, путь к autoload: `/opt/project/vendor/autoload.php`, конфиг: `/opt/project/phpunit.xml`. + +| Файл | Покрытие | +|------|---------| +| `tests/Unit/RouteTest.php` | `Route::resolve()`, case-insensitive, params, fallback | +| `tests/Unit/ResponseTest.php` | `body()`, `status()`, fluent chain | +| `tests/Unit/MyExceptionTest.php` | `strtr` замена, code, previous | +| `tests/Unit/CoreTest.php` | `findFile()`, `getConst()`, environment | +| `tests/Unit/RequestTest.php` | `factory()`, `post()`, `query()`, `body()`, `is_ajax()`, `method()` | +| `tests/Unit/ViewTest.php` | `render()`, `setFilename()`, data extract | +| `tests/Unit/HttpClientRequestTest.php` | `HTTP\Client\Request` builder, json, fluent | +| `tests/Unit/CookieTest.php` | `get()`, `set()`, `delete()`, defaults | +| `tests/Unit/SessionTest.php` | singleton, `get/set/delete/destroy/regenerate/bind` | +| `tests/Unit/ConfigTest.php` | `get()`, `set()`, `merge()`, lazy-load | +| `tests/Unit/HTTPExceptionTest.php` | `factory()`, subclasses, `get_response()`, codes | + +**111 тестов, 171 assertion — все проходят.** + +### Frontend dependencies (через Composer) + +Bootstrap 5.3, Bootstrap Icons 1.13, jQuery 3.7.1, jQuery UI 1.12, Select2 4.1 — из `vendor/`. diff --git a/System/Classes/Config.php b/System/Classes/Config.php new file mode 100644 index 0000000..eeb55d4 --- /dev/null +++ b/System/Classes/Config.php @@ -0,0 +1,65 @@ + $value) { + if (is_array($value) && isset($base[$key]) && is_array($base[$key])) { + $base[$key] = self::merge($base[$key], $value); + } else { + $base[$key] = $value; + } + } + + return $base; + } +} diff --git a/System/Classes/Controller.php b/System/Classes/Controller.php new file mode 100644 index 0000000..435ada9 --- /dev/null +++ b/System/Classes/Controller.php @@ -0,0 +1,29 @@ +_layout, 'view/views', [ + 'content' => (new View($template, $dir, $data))->render() + ]))->render(); + } +} \ No newline at end of file diff --git a/System/Classes/Cookie.php b/System/Classes/Cookie.php new file mode 100644 index 0000000..6577558 --- /dev/null +++ b/System/Classes/Cookie.php @@ -0,0 +1,53 @@ + 0 ? time() + $lifetime : 0; + + return setcookie($name, $value, [ + 'expires' => $expires, + 'path' => self::$path, + 'domain' => self::$domain, + 'secure' => self::$secure, + 'httponly' => self::$httponly, + 'samesite' => self::$samesite, + ]); + } + + public static function delete(string $name): bool + { + unset($_COOKIE[$name]); + + return setcookie($name, '', [ + 'expires' => 1, + 'path' => self::$path, + 'domain' => self::$domain, + 'secure' => self::$secure, + 'httponly' => self::$httponly, + 'samesite' => self::$samesite, + ]); + } +} diff --git a/System/Classes/Core.php b/System/Classes/Core.php new file mode 100644 index 0000000..12b8a56 --- /dev/null +++ b/System/Classes/Core.php @@ -0,0 +1,104 @@ + $value) { + if (str_starts_with($key, 'HTTP_')) { + $headers[str_replace('_', '-', strtolower(substr($key, 5)))] = $value; + } + } + + return new Header($headers); + } +} diff --git a/System/Classes/HTTP/Client/Curl.php b/System/Classes/HTTP/Client/Curl.php new file mode 100644 index 0000000..8e8185e --- /dev/null +++ b/System/Classes/HTTP/Client/Curl.php @@ -0,0 +1,55 @@ +getHeaders() as $name => $value) { + $headers[] = $name . ': ' . $value; + } + + $ch = curl_init(); + + curl_setopt_array($ch, [ + CURLOPT_URL => $request->getUrl(), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_TIMEOUT => $request->getTimeout(), + CURLOPT_CONNECTTIMEOUT => 10, + ]); + + match ($request->getMethod()) { + 'GET' => curl_setopt($ch, CURLOPT_HTTPGET, true), + 'POST' => curl_setopt($ch, CURLOPT_POST, true), + default => curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $request->getMethod()), + }; + + if ($request->getBody() !== null) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $request->getBody()); + } + + $body = curl_exec($ch); + + if ($body === false) { + $error = curl_error($ch); + curl_close($ch); + throw new RuntimeException('Curl error: ' . $error); + } + + $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return new Response($status, $body); + } +} diff --git a/System/Classes/HTTP/Client/Request.php b/System/Classes/HTTP/Client/Request.php new file mode 100644 index 0000000..1804fd5 --- /dev/null +++ b/System/Classes/HTTP/Client/Request.php @@ -0,0 +1,66 @@ +url = $url; + } + + public static function factory(string $url): static + { + return new static($url); + } + + public function method(string $method): static + { + $this->method = strtoupper($method); + return $this; + } + + public function header(string $name, string $value): static + { + $this->headers[$name] = $value; + return $this; + } + + public function body(string $body): static + { + $this->body = $body; + return $this; + } + + public function json(array $data): static + { + $this->header('Content-Type', 'application/json'); + $this->header('Accept', 'application/json'); + $this->body = json_encode($data, JSON_UNESCAPED_UNICODE); + return $this; + } + + public function timeout(int $seconds): static + { + $this->timeout = $seconds; + return $this; + } + + public function getMethod(): string { return $this->method; } + public function getUrl(): string { return $this->url; } + public function getHeaders(): array { return $this->headers; } + public function getBody(): ?string { return $this->body; } + public function getTimeout(): int { return $this->timeout; } +} diff --git a/System/Classes/HTTP/Client/Response.php b/System/Classes/HTTP/Client/Response.php new file mode 100644 index 0000000..bcbce7a --- /dev/null +++ b/System/Classes/HTTP/Client/Response.php @@ -0,0 +1,45 @@ +status; + } + + public function body(): string + { + return $this->body; + } + + public function isSuccess(): bool + { + return $this->status >= 200 && $this->status < 300; + } + + /** + * @throws JsonException + */ + public function json(): array + { + if ($this->body === '') { + return []; + } + return json_decode($this->body, true, 512, JSON_THROW_ON_ERROR); + } +} diff --git a/System/Classes/HTTP/Exception/HTTPException_302.php b/System/Classes/HTTP/Exception/HTTPException_302.php new file mode 100644 index 0000000..590b6e8 --- /dev/null +++ b/System/Classes/HTTP/Exception/HTTPException_302.php @@ -0,0 +1,24 @@ +getMessage() ?: '/'; + HTTP::redirect($location, 302); + } +} diff --git a/System/Classes/HTTP/Exception/HTTPException_403.php b/System/Classes/HTTP/Exception/HTTPException_403.php new file mode 100644 index 0000000..e2f02de --- /dev/null +++ b/System/Classes/HTTP/Exception/HTTPException_403.php @@ -0,0 +1,34 @@ +getMessage(); + include $view; + $response->body(ob_get_clean()); + } + + return $response; + } +} diff --git a/System/Classes/HTTP/Exception/HTTPException_404.php b/System/Classes/HTTP/Exception/HTTPException_404.php new file mode 100644 index 0000000..6c6365a --- /dev/null +++ b/System/Classes/HTTP/Exception/HTTPException_404.php @@ -0,0 +1,34 @@ +getMessage(); + include $view; + $response->body(ob_get_clean()); + } + + return $response; + } +} diff --git a/System/Classes/HTTP/HTTPException.php b/System/Classes/HTTP/HTTPException.php new file mode 100644 index 0000000..9e4ff8c --- /dev/null +++ b/System/Classes/HTTP/HTTPException.php @@ -0,0 +1,55 @@ +_code = $codeOverride; + } + parent::__construct($message, $variables, $this->_code, $previous); + } + + public static function factory(int $code, string $message = '', ?array $variables = null, ?\Throwable $previous = null): static + { + $class = 'System\\Classes\\HTTP\\Exception\\HTTPException_' . $code; + + if (class_exists($class)) { + return new $class($message, $variables, $previous); + } + + return new static($message, $variables, $previous, $code); + } + + public function get_response(): Response + { + $code = $this->_code ?: 500; + http_response_code($code); + + $response = new Response($code); + $view = Core::findFile('view/errors', (string)$code, 'html'); + + if ($view) { + ob_start(); + $message = $this->getMessage(); + include $view; + $response->body(ob_get_clean()); + } + + return $response; + } +} diff --git a/System/Classes/HTTP/Header.php b/System/Classes/HTTP/Header.php new file mode 100644 index 0000000..8ecc612 --- /dev/null +++ b/System/Classes/HTTP/Header.php @@ -0,0 +1,41 @@ + $value) { + $output .= $key . ': ' . (is_array($value) ? implode(', ', $value) : $value) . "\r\n"; + } + + return $output . "\r\n"; + } + + public function send(): void + { + if (headers_sent()) { + return; + } + + foreach ($this as $key => $value) { + header($key . ': ' . (is_array($value) ? implode(', ', $value) : $value)); + } + } +} diff --git a/System/Classes/HTTP/Message.php b/System/Classes/HTTP/Message.php new file mode 100644 index 0000000..024d353 --- /dev/null +++ b/System/Classes/HTTP/Message.php @@ -0,0 +1,17 @@ +getCode() >= 100 && $e->getCode() <= 599) ? $e->getCode() : 500; + http_response_code($httpCode); + + if (Core::$environment >= Core::DEVELOPMENT) { + $class = get_class($e); + $code = $e->getCode(); + $message = $e->getMessage(); + $file_ = $e->getFile(); + $line = $e->getLine(); + $xdebug_message = property_exists($e, 'xdebug_message') ? $e->xdebug_message : ''; + + $view = Core::findFile('view/exception', 'error', 'html'); + if ($view) { + include $view; + return; + } + } + + if (($view500 = Core::findFile('view/errors', '500', 'html')) !== false) { + include $view500; + } + } +} \ No newline at end of file diff --git a/System/Classes/Request.php b/System/Classes/Request.php new file mode 100644 index 0000000..5158bf8 --- /dev/null +++ b/System/Classes/Request.php @@ -0,0 +1,176 @@ +_uri = $uri; + $route = Route::resolve($uri); + $this->_controller = $route['controller']; + $this->_action = $route['action']; + $this->_params = $route['params']; + } + + public static function factory(): static + { + $uri = self::detect_uri(); + $instance = new static($uri); + + if (self::$initial === null) { + self::$initial = $instance; + } + self::$current = $instance; + + $instance->method($_SERVER['REQUEST_METHOD'] ?? HTTPRequest::GET); + $instance->query($_GET); + $instance->post($_POST); + $instance->_requested_with = strtolower($_SERVER['HTTP_X_REQUESTED_WITH'] ?? ''); + + if ($instance->method() !== HTTPRequest::GET) { + $body = file_get_contents('php://input'); + if ($body !== false && $body !== '') { + $instance->body($body); + } + } + + return $instance; + } + + public static function detect_uri(): string + { + if (!empty($_SERVER['PATH_INFO'])) { + $uri = $_SERVER['PATH_INFO']; + } elseif (isset($_SERVER['REQUEST_URI'])) { + $uri = $_SERVER['REQUEST_URI']; + if ($path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)) { + $uri = $path; + } + $uri = rawurldecode($uri); + } elseif (isset($_SERVER['PHP_SELF'])) { + $uri = $_SERVER['PHP_SELF']; + } else { + $uri = '/'; + } + + return trim($uri, '/'); + } + + public function execute(): Response + { + $method = lcfirst($this->_action) . 'Action'; + $parts = explode('/', $this->_controller); + $file = DOCROOT . '/App/Controller/' . implode('/', array_map('ucfirst', $parts)) . 'Controller.php'; + + if (!file_exists($file)) { + return HTTPException::factory(404)->get_response(); + } + + require_once $file; + + $class = 'App\\Controller\\' . implode('\\', array_map('ucfirst', $parts)) . 'Controller'; + + if (!method_exists($class, $method)) { + return HTTPException::factory(404)->get_response(); + } + + return (new Response())->body((new $class())->$method()); + } + + public function method(?string $method = null): string|static + { + if ($method === null) { + return $this->_method; + } + $this->_method = strtoupper($method); + return $this; + } + + public function post(null|string|array $key = null, mixed $value = null): null|array|string|static + { + if (is_array($key)) { + $this->_post = $key; + return $this; + } + if ($key === null) { + return $this->_post; + } + if ($value === null) { + return $this->_post[$key] ?? null; + } + $this->_post[$key] = $value; + return $this; + } + + public function query(null|string|array $key = null, mixed $value = null): null|array|string|static + { + if (is_array($key)) { + $this->_get = $key; + return $this; + } + if ($key === null) { + return $this->_get; + } + if ($value === null) { + return $this->_get[$key] ?? null; + } + $this->_get[$key] = $value; + return $this; + } + + public function body(?string $content = null): string|static + { + if ($content === null) { + return $this->_body ?? ''; + } + $this->_body = $content; + return $this; + } + + public function requested_with(?string $value = null): string|static + { + if ($value === null) { + return $this->_requested_with; + } + $this->_requested_with = strtolower($value); + return $this; + } + + public function is_ajax(): bool + { + return $this->_requested_with === 'xmlhttprequest'; + } + + public function controller(): string { return $this->_controller; } + public function action(): string { return $this->_action; } + public function params(): array { return $this->_params; } + public function uri(): string { return $this->_uri; } + + public function param(int $index, mixed $default = null): mixed + { + return $this->_params[$index] ?? $default; + } +} diff --git a/System/Classes/Response.php b/System/Classes/Response.php new file mode 100644 index 0000000..5574a7f --- /dev/null +++ b/System/Classes/Response.php @@ -0,0 +1,39 @@ +_status = $status; + } + + public function body(?string $content = null): string|static + { + if ($content === null) { + return $this->_body; + } + $this->_body = $content; + return $this; + } + + public function status(?int $code = null): int|static + { + if ($code === null) { + return $this->_status; + } + $this->_status = $code; + http_response_code($code); + return $this; + } +} diff --git a/System/Classes/Route.php b/System/Classes/Route.php new file mode 100644 index 0000000..65a4093 --- /dev/null +++ b/System/Classes/Route.php @@ -0,0 +1,112 @@ + $controller, + 'action' => $action, + 'params' => $params, + ]; + } +} diff --git a/System/Classes/Session.php b/System/Classes/Session.php new file mode 100644 index 0000000..2c0b1af --- /dev/null +++ b/System/Classes/Session.php @@ -0,0 +1,144 @@ +_name = $name; + $this->_lifetime = $config['lifetime'] ?? 0; + + session_write_close(); + session_name($this->_name); + + session_set_cookie_params([ + 'lifetime' => $this->_lifetime > 0 ? 300 : 0, + 'path' => '/', + 'secure' => $config['secure'] ?? false, + 'httponly' => $config['httponly'] ?? true, + 'samesite' => $config['samesite'] ?? 'Lax', + ]); + + $this->read(); + } + + public static function instance(string $name = 'main'): static + { + $name = strtolower($name); + + if (!isset(self::$instances[$name])) { + self::$instances[$name] = new static($name, []); + } + + return self::$instances[$name]; + } + + public function read(): void + { + session_cache_limiter(false); + session_start(); + + $_SESSION['start_time'] = (int)($_SESSION['start_time'] ?? time()); + + $gc_lifetime = (int)ini_get('session.gc_maxlifetime'); + + if ($_SESSION['start_time'] + $gc_lifetime < time()) { + $this->destroy(); + } + + $this->_data = &$_SESSION; + } + + public function get(string $key, mixed $default = null): mixed + { + return array_key_exists($key, $this->_data) ? $this->_data[$key] : $default; + } + + public function set(string $key, mixed $value): static + { + $this->_data[$key] = $value; + return $this; + } + + public function delete(string $key): static + { + unset($this->_data[$key]); + return $this; + } + + public function destroy(): bool + { + if (!$this->_destroyed) { + session_unset(); + session_destroy(); + setcookie(session_name(), '', 0, '/'); + Cookie::delete($this->_name); + + if ($this->_destroyed = !session_id()) { + $this->_data = []; + } + } + + return $this->_destroyed; + } + + public function regenerate(): false|string + { + session_regenerate_id(true); + $new_id = session_id(); + Cookie::set($this->_name, $new_id, $this->_lifetime); + return $new_id; + } + + public function write(): bool + { + if ($this->_destroyed || headers_sent()) { + return false; + } + + session_write_close(); + return true; + } + + public function bind(string $key, mixed &$value): static + { + $this->_data[$key] = &$value; + return $this; + } + + public function id(): false|string + { + return session_id(); + } + + public function getData(): array + { + return $this->_data; + } + + public static function getDeviceHash(): string + { + $hash = Cookie::get('device_hash'); + + if (!$hash) { + $hash = md5(($_SERVER['HTTP_USER_AGENT'] ?? '') . uniqid('', true)); + Cookie::set('device_hash', $hash, 365 * 24 * 3600); + } + + return $hash; + } +} diff --git a/System/Classes/View.php b/System/Classes/View.php new file mode 100644 index 0000000..3291d4b --- /dev/null +++ b/System/Classes/View.php @@ -0,0 +1,90 @@ +setFilename($file, $dir); + } + + if ($data !== null) { + // Добавить значения к текущим данным + $this->_data = $data + $this->_data; + } + } + + /** + * @param string $file + * @param string $dir + * @return View + * @throws MyException + */ + public function setFilename(string $file, string $dir = ''): View + { + if ($dir === '') { + throw new MyException("Параметр dir не передан в View::setFilename для файла '$file'"); + } + + if (($path = Core::findFile($dir, $file, 'html')) === false) { + throw new MyException("При запросе view файл $dir/$file не найден"); + } + + // Store the file path locally + $this->_file = $path; + + return $this; + } + + /** + * @throws MyException + */ + public function render(): string + { + if (empty($this->_file)) { + throw new MyException('Нужно установить файл для использования во view перед преобразованием'); + } + + ob_start(); + extract($this->_data, EXTR_SKIP); + + try { + include $this->_file; + + return ob_get_clean(); + } catch (Throwable $e) { + ob_end_clean(); + + throw new MyException( + "Ошибка при render файла: {$e->getFile()} на строке {$e->getLine()} - {$e->getMessage()}", 0, $e + ); + } + } +} \ No newline at end of file diff --git a/System/view/errors/403.html b/System/view/errors/403.html new file mode 100644 index 0000000..1dda9a2 --- /dev/null +++ b/System/view/errors/403.html @@ -0,0 +1,10 @@ + + diff --git a/System/view/errors/404.html b/System/view/errors/404.html new file mode 100644 index 0000000..906056a --- /dev/null +++ b/System/view/errors/404.html @@ -0,0 +1,20 @@ + + + + + + 404 — Страница не найдена + + +

404

+

Страница не найдена.

+ + diff --git a/System/view/errors/500.html b/System/view/errors/500.html new file mode 100644 index 0000000..899aa50 --- /dev/null +++ b/System/view/errors/500.html @@ -0,0 +1,10 @@ + + diff --git a/System/view/exception/error.html b/System/view/exception/error.html new file mode 100644 index 0000000..f379e9b --- /dev/null +++ b/System/view/exception/error.html @@ -0,0 +1,40 @@ + + +
+

+ + [ ]: + + +

+ +
+

+ + [] + +

+
+
+ +
diff --git a/System/view/views/layout.html b/System/view/views/layout.html new file mode 100644 index 0000000..1b016e2 --- /dev/null +++ b/System/view/views/layout.html @@ -0,0 +1,11 @@ + + + + + + Bicycle + + + + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d53f2c0 --- /dev/null +++ b/composer.json @@ -0,0 +1,36 @@ +{ + "require": { + "php": ">=8.2", + "ext-pdo": "*", + "phpoffice/phpspreadsheet": "5.*", + "phpoffice/phpword": "1.1.0", + "phpmailer/phpmailer": "v6.9.*", + "twbs/bootstrap": "v5.3.*", + "twbs/bootstrap-icons": "v1.13.*", + "select2/select2": "4.1.*", + "components/jquery": "v3.7.1", + "components/jqueryui": "1.12.*", + "dompdf/dompdf": "v3.1.*" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "autoload": { + "psr-4": { + "App\\": "App", + "Services\\": "Services", + "System\\": "System" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "config": { + "allow-plugins": { + "php-http/discovery": true, + "composer/installers": true + } + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..d35090a --- /dev/null +++ b/composer.lock @@ -0,0 +1,3202 @@ +{ + "_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": "d15667ff3f0882b8e8daa05eae634821", + "packages": [ + { + "name": "components/jquery", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/components/jquery.git", + "reference": "8edc7785239bb8c2ad2b83302b856a1d61de60e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/components/jquery/zipball/8edc7785239bb8c2ad2b83302b856a1d61de60e7", + "reference": "8edc7785239bb8c2ad2b83302b856a1d61de60e7", + "shasum": "" + }, + "type": "component", + "extra": { + "component": { + "files": [ + "jquery.min.js", + "jquery.min.map", + "jquery.slim.js", + "jquery.slim.min.js", + "jquery.slim.min.map" + ], + "scripts": [ + "jquery.js" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "JS Foundation and other contributors" + } + ], + "description": "jQuery JavaScript Library", + "homepage": "http://jquery.com", + "support": { + "forum": "http://forum.jquery.com", + "irc": "irc://irc.freenode.org/jquery", + "issues": "https://github.com/jquery/jquery/issues", + "source": "https://github.com/jquery/jquery", + "wiki": "http://docs.jquery.com/" + }, + "time": "2023-09-22T01:43:46+00:00" + }, + { + "name": "components/jqueryui", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/components/jqueryui.git", + "reference": "44ecf3794cc56b65954cc19737234a3119d036cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/components/jqueryui/zipball/44ecf3794cc56b65954cc19737234a3119d036cc", + "reference": "44ecf3794cc56b65954cc19737234a3119d036cc", + "shasum": "" + }, + "require": { + "components/jquery": ">=1.6" + }, + "type": "component", + "extra": { + "component": { + "name": "jquery-ui", + "shim": { + "deps": [ + "jquery" + ], + "exports": "jQuery" + }, + "files": [ + "ui/**", + "themes/**", + "jquery-ui.min.js" + ], + "scripts": [ + "jquery-ui.js" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "jQuery UI Team", + "homepage": "http://jqueryui.com/about" + }, + { + "name": "Joern Zaefferer", + "email": "joern.zaefferer@gmail.com", + "homepage": "http://bassistance.de" + }, + { + "name": "Scott Gonzalez", + "email": "scott.gonzalez@gmail.com", + "homepage": "http://scottgonzalez.com" + }, + { + "name": "Kris Borchers", + "email": "kris.borchers@gmail.com", + "homepage": "http://krisborchers.com" + }, + { + "name": "Mike Sherov", + "email": "mike.sherov@gmail.com", + "homepage": "http://mike.sherov.com" + }, + { + "name": "TJ VanToll", + "email": "tj.vantoll@gmail.com", + "homepage": "http://tjvantoll.com" + }, + { + "name": "Corey Frang", + "email": "gnarf37@gmail.com", + "homepage": "http://gnarf.net" + }, + { + "name": "Felix Nagel", + "email": "info@felixnagel.com", + "homepage": "http://www.felixnagel.com" + } + ], + "description": "jQuery UI is a curated set of user interface interactions, effects, widgets, and themes built on top of the jQuery JavaScript Library. Whether you're building highly interactive web applications or you just need to add a date picker to a form control, jQuery UI is the perfect choice.", + "support": { + "issues": "https://github.com/components/jqueryui/issues", + "source": "https://github.com/components/jqueryui/tree/master" + }, + "time": "2016-09-16T05:47:55+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "dompdf/dompdf", + "version": "v3.1.5", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", + "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", + "shasum": "" + }, + "require": { + "dompdf/php-font-lib": "^1.0.0", + "dompdf/php-svg-lib": "^1.0.0", + "ext-dom": "*", + "ext-mbstring": "*", + "masterminds/html5": "^2.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ext-gd": "*", + "ext-json": "*", + "ext-zip": "*", + "mockery/mockery": "^1.3", + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.5", + "symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0" + }, + "suggest": { + "ext-gd": "Needed to process images", + "ext-gmagick": "Improves image processing performance", + "ext-imagick": "Improves image processing performance", + "ext-zlib": "Needed for pdf stream compression" + }, + "type": "library", + "autoload": { + "psr-4": { + "Dompdf\\": "src/" + }, + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "The Dompdf Community", + "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md" + } + ], + "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", + "homepage": "https://github.com/dompdf/dompdf", + "support": { + "issues": "https://github.com/dompdf/dompdf/issues", + "source": "https://github.com/dompdf/dompdf/tree/v3.1.5" + }, + "time": "2026-03-03T13:54:37+00:00" + }, + { + "name": "dompdf/php-font-lib", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-font-lib.git", + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a", + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12" + }, + "type": "library", + "autoload": { + "psr-4": { + "FontLib\\": "src/FontLib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "The FontLib Community", + "homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse, export and make subsets of different types of font files.", + "homepage": "https://github.com/dompdf/php-font-lib", + "support": { + "issues": "https://github.com/dompdf/php-font-lib/issues", + "source": "https://github.com/dompdf/php-font-lib/tree/1.0.2" + }, + "time": "2026-01-20T14:10:26+00:00" + }, + { + "name": "dompdf/php-svg-lib", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-svg-lib.git", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabberworm/php-css-parser": "^8.4 || ^9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Svg\\": "src/Svg" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "The SvgLib Community", + "homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse and export to PDF SVG files.", + "homepage": "https://github.com/dompdf/php-svg-lib", + "support": { + "issues": "https://github.com/dompdf/php-svg-lib/issues", + "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2" + }, + "time": "2026-01-02T16:01:13+00:00" + }, + { + "name": "laminas/laminas-escaper", + "version": "2.18.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-escaper.git", + "reference": "06f211dfffff18d91844c1f55250d5d13c007e18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/06f211dfffff18d91844c1f55250d5d13c007e18", + "reference": "06f211dfffff18d91844c1f55250d5d13c007e18", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-mbstring": "*", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "conflict": { + "zendframework/zend-escaper": "*" + }, + "require-dev": { + "infection/infection": "^0.31.0", + "laminas/laminas-coding-standard": "~3.1.0", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Escaper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", + "homepage": "https://laminas.dev", + "keywords": [ + "escaper", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-escaper/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-escaper/issues", + "rss": "https://github.com/laminas/laminas-escaper/releases.atom", + "source": "https://github.com/laminas/laminas-escaper" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-14T18:31:13+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.2" + }, + "require-dev": { + "brianium/paratest": "^7.7", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.16", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^11.0", + "vimeo/psalm": "^6.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2025-01-27T12:07:53+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.10.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "fcf91eb64359852f00d921887b219479b4f21251" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", + "reference": "fcf91eb64359852f00d921887b219479b4f21251", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" + }, + "time": "2025-07-25T09:04:22+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.9.3", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e", + "reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "doctrine/annotations": "^1.2.6 || ^1.13.3", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.7.2", + "yoast/phpunit-polyfills": "^1.0.4" + }, + "suggest": { + "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "ext-openssl": "Needed for secure SMTP sending and DKIM signing", + "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "support": { + "issues": "https://github.com/PHPMailer/PHPMailer/issues", + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.3" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2024-11-24T18:04:13+00:00" + }, + { + "name": "phpoffice/phpspreadsheet", + "version": "5.7.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8", + "reference": "9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-filter": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^8.1", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^2.0 || ^3.0", + "ext-intl": "*", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.5", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1 || ^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", + "phpunit/phpunit": "^10.5", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + }, + { + "name": "Owen Leibman" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.7.0" + }, + "time": "2026-04-20T02:42:17+00:00" + }, + { + "name": "phpoffice/phpword", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PHPWord.git", + "reference": "90a55955e6a772bb4cd9b1ef6a7e88c8976c2561" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/90a55955e6a772bb4cd9b1ef6a7e88c8976c2561", + "reference": "90a55955e6a772bb4cd9b1ef6a7e88c8976c2561", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-xml": "*", + "laminas/laminas-escaper": ">=2.6", + "php": "^7.1|^8.0" + }, + "require-dev": { + "dompdf/dompdf": "^2.0", + "ext-gd": "*", + "ext-libxml": "*", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.3", + "mpdf/mpdf": "^8.1", + "phpmd/phpmd": "^2.13", + "phpunit/phpunit": ">=7.0", + "symfony/process": "^4.4", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Allows writing PDF", + "ext-gd2": "Allows adding images", + "ext-xmlwriter": "Allows writing OOXML and ODF", + "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template", + "ext-zip": "Allows writing OOXML and ODF" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpWord\\": "src/PhpWord" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Mark Baker" + }, + { + "name": "Gabriel Bull", + "email": "me@gabrielbull.com", + "homepage": "http://gabrielbull.com/" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net/blog/" + }, + { + "name": "Ivan Lanin", + "homepage": "http://ivan.lanin.org" + }, + { + "name": "Roman Syroeshko", + "homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/" + }, + { + "name": "Antoine de Troostembergh" + } + ], + "description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)", + "homepage": "https://phpword.readthedocs.io/", + "keywords": [ + "ISO IEC 29500", + "OOXML", + "Office Open XML", + "OpenDocument", + "OpenXML", + "PhpOffice", + "PhpWord", + "Rich Text Format", + "WordprocessingML", + "doc", + "docx", + "html", + "odf", + "odt", + "office", + "pdf", + "php", + "reader", + "rtf", + "template", + "template processor", + "word", + "writer" + ], + "support": { + "issues": "https://github.com/PHPOffice/PHPWord/issues", + "source": "https://github.com/PHPOffice/PHPWord/tree/1.1.0" + }, + "time": "2023-05-30T07:59:14+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "sabberworm/php-css-parser", + "version": "v9.3.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949", + "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.32 || 2.1.32", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.8", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7", + "phpunit/phpunit": "8.5.52", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.2.8", + "rector/type-perfect": "1.0.0 || 2.1.0", + "squizlabs/php_codesniffer": "4.0.1", + "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.4.x-dev" + } + }, + "autoload": { + "files": [ + "src/Rule/Rule.php", + "src/RuleSet/RuleContainer.php" + ], + "psr-4": { + "Sabberworm\\CSS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raphael Schweikert" + }, + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" + } + ], + "description": "Parser for CSS Files written in PHP", + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "keywords": [ + "css", + "parser", + "stylesheet" + ], + "support": { + "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.3.0" + }, + "time": "2026-03-03T17:31:43+00:00" + }, + { + "name": "select2/select2", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/select2/select2.git", + "reference": "13bad6eb9b212e8566dc467fb8b20c6d72f4bfa3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/select2/select2/zipball/13bad6eb9b212e8566dc467fb8b20c6d72f4bfa3", + "reference": "13bad6eb9b212e8566dc467fb8b20c6d72f4bfa3", + "shasum": "" + }, + "type": "component", + "extra": { + "component": { + "files": [ + "dist/js/select2.js", + "dist/js/i18n/*.js", + "dist/css/select2.css" + ], + "styles": [ + "dist/css/select2.css" + ], + "scripts": [ + "dist/js/select2.js" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Select2 is a jQuery based replacement for select boxes.", + "homepage": "https://select2.org/", + "support": { + "issues": "https://github.com/select2/select2/issues", + "source": "https://github.com/select2/select2/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/kevin-brown", + "type": "github" + }, + { + "url": "https://opencollective.com/select2", + "type": "open_collective" + } + ], + "time": "2026-05-26T20:03:12+00:00" + }, + { + "name": "thecodingmachine/safe", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2026-02-04T18:08:13+00:00" + }, + { + "name": "twbs/bootstrap", + "version": "v5.3.8", + "source": { + "type": "git", + "url": "https://github.com/twbs/bootstrap.git", + "reference": "25aa8cc0b32f0d1a54be575347e6d84b70b1acd7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/25aa8cc0b32f0d1a54be575347e6d84b70b1acd7", + "reference": "25aa8cc0b32f0d1a54be575347e6d84b70b1acd7", + "shasum": "" + }, + "replace": { + "twitter/bootstrap": "self.version" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Otto", + "email": "markdotto@gmail.com" + }, + { + "name": "Jacob Thornton", + "email": "jacobthornton@gmail.com" + } + ], + "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", + "homepage": "https://getbootstrap.com/", + "keywords": [ + "JS", + "css", + "framework", + "front-end", + "mobile-first", + "responsive", + "sass", + "web" + ], + "support": { + "issues": "https://github.com/twbs/bootstrap/issues", + "source": "https://github.com/twbs/bootstrap/tree/v5.3.8" + }, + "time": "2025-08-26T02:01:02+00:00" + }, + { + "name": "twbs/bootstrap-icons", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/twbs/icons.git", + "reference": "ce0e49dd063243118a115f17ad1fe1fe7576d552" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twbs/icons/zipball/ce0e49dd063243118a115f17ad1fe1fe7576d552", + "reference": "ce0e49dd063243118a115f17ad1fe1fe7576d552", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Otto", + "email": "markdotto@gmail.com" + } + ], + "description": "Official open source SVG icon library for Bootstrap", + "homepage": "https://icons.getbootstrap.com/", + "keywords": [ + "bootstrap", + "icon font", + "icons", + "svg" + ], + "support": { + "issues": "https://github.com/twbs/icons/issues", + "source": "https://github.com/twbs/icons/tree/v1.13.1" + }, + "time": "2025-05-09T23:01:19+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.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "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.7.0" + }, + "time": "2025-12-06T11:56:16+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": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "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": "11.0.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/11.0.12" + }, + "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/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "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", + "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/5.1.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/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "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", + "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", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.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": "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/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.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", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "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.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.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/11.5.55" + }, + "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": "2026-02-18T12:37:06+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.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": "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/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "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": "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", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.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" + } + ], + "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", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-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/6.3.3" + }, + "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/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.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": "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/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "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" + }, + { + "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/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-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/7.2.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/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-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/6.3.2" + }, + "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/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.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/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.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": "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/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.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": "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", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.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" + } + ], + "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", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "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" + }, + { + "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/6.0.3" + }, + "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-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "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", + "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", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "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/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "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", + "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", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "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.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.2", + "ext-pdo": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..099bfbd --- /dev/null +++ b/index.php @@ -0,0 +1,34 @@ +execute()->body(); diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..3164bd8 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,11 @@ + + + + + tests/Unit + + + diff --git a/tests/Unit/ConfigTest.php b/tests/Unit/ConfigTest.php new file mode 100644 index 0000000..d86de65 --- /dev/null +++ b/tests/Unit/ConfigTest.php @@ -0,0 +1,56 @@ +getProperty('config'); + $prop->setAccessible(true); + $prop->setValue(null, null); + } + + public function testGetSectionReturnsArray(): void + { + $db = Config::get('db'); + $this->assertIsArray($db); + $this->assertArrayHasKey('default', $db); + } + + public function testGetGroupReturnsValue(): void + { + $params = Config::get('session', 'cookie_params'); + $this->assertIsArray($params); + $this->assertArrayHasKey('lifetime', $params); + } + + public function testGetMissingKeyReturnsNull(): void + { + $this->assertNull(Config::get('nonexistent')); + $this->assertNull(Config::get('nonexistent', 'group')); + } + + public function testSetOverridesValue(): void + { + Config::set('foo', ['bar' => 'baz']); + $this->assertSame(['bar' => 'baz'], Config::get('foo')); + } + + public function testMergeDeep(): void + { + $base = ['a' => ['x' => 1, 'y' => 2], 'b' => 3]; + $override = ['a' => ['y' => 99, 'z' => 4], 'b' => 10]; + $result = Config::merge($base, $override); + + $this->assertSame(1, $result['a']['x']); + $this->assertSame(99, $result['a']['y']); + $this->assertSame(4, $result['a']['z']); + $this->assertSame(10, $result['b']); + } +} diff --git a/tests/Unit/CookieTest.php b/tests/Unit/CookieTest.php new file mode 100644 index 0000000..a1c5d19 --- /dev/null +++ b/tests/Unit/CookieTest.php @@ -0,0 +1,53 @@ +assertNull(Cookie::get('test_key')); + } + + public function testGetReturnsDefault(): void + { + unset($_COOKIE['test_key']); + $this->assertSame('default', Cookie::get('test_key', 'default')); + } + + public function testGetReturnsValue(): void + { + $_COOKIE['test_key'] = 'hello'; + $this->assertSame('hello', Cookie::get('test_key')); + unset($_COOKIE['test_key']); + } + + public function testDeleteUnsetsFromGlobal(): void + { + $_COOKIE['to_delete'] = 'value'; + Cookie::delete('to_delete'); + $this->assertArrayNotHasKey('to_delete', $_COOKIE); + } + + public function testDefaultsAreCorrect(): void + { + $this->assertSame('/', Cookie::$path); + $this->assertTrue(Cookie::$httponly); + $this->assertFalse(Cookie::$secure); + $this->assertSame('Lax', Cookie::$samesite); + } +} diff --git a/tests/Unit/CoreTest.php b/tests/Unit/CoreTest.php new file mode 100644 index 0000000..63aaeb8 --- /dev/null +++ b/tests/Unit/CoreTest.php @@ -0,0 +1,76 @@ +assertIsString($path); + $this->assertStringEndsWith('view/errors/404.html', $path); + $this->assertStringContainsString('System', $path); + } + + public function testFindFileInApppath(): void + { + $path = Core::findFile('view/Index', 'index', 'html'); + $this->assertIsString($path); + $this->assertStringEndsWith('index.html', $path); + $this->assertStringContainsString('App', $path); + } + + public function testFindFileReturnsFalseWhenNotFound(): void + { + $result = Core::findFile('view', 'nonexistent_xyz_file', 'html'); + $this->assertFalse($result); + } + + public function testFindFileWithDefaultExtension(): void + { + // EXT = '.php', findFile без третьего аргумента ищет .php + $path = Core::findFile('Classes', 'Core'); + $this->assertIsString($path); + $this->assertStringEndsWith('Classes/Core.php', $path); + } + + public function testFindFileReturnsFullPath(): void + { + $path = Core::findFile('view/errors', '404', 'html'); + $this->assertStringStartsWith(SYSPATH, $path); + $this->assertFileExists($path); + } + + public function testGetConstProduction(): void + { + $this->assertSame(10, Core::getConst('PRODUCTION')); + } + + public function testGetConstStaging(): void + { + $this->assertSame(20, Core::getConst('STAGING')); + } + + public function testGetConstTesting(): void + { + $this->assertSame(30, Core::getConst('TESTING')); + } + + public function testGetConstDevelopment(): void + { + $this->assertSame(40, Core::getConst('DEVELOPMENT')); + } + + public function testDefaultEnvironmentIsDevelopment(): void + { + $this->assertSame(Core::DEVELOPMENT, Core::$environment); + } + + public function testCharsetIsUtf8(): void + { + $this->assertSame('utf-8', Core::$charset); + } +} diff --git a/tests/Unit/HTTPExceptionTest.php b/tests/Unit/HTTPExceptionTest.php new file mode 100644 index 0000000..9f66a6a --- /dev/null +++ b/tests/Unit/HTTPExceptionTest.php @@ -0,0 +1,74 @@ +assertInstanceOf(HTTPException_404::class, $e); + $this->assertSame(404, $e->getCode()); + } + + public function testFactory403ReturnsCorrectClass(): void + { + $e = HTTPException::factory(403); + $this->assertInstanceOf(HTTPException_403::class, $e); + $this->assertSame(403, $e->getCode()); + } + + public function testFactoryWithMessage(): void + { + $e = HTTPException::factory(404, 'Страница не найдена'); + $this->assertSame('Страница не найдена', $e->getMessage()); + } + + public function testFactoryWithVariables(): void + { + $e = HTTPException::factory(404, 'Файл :name не найден', [':name' => 'test.html']); + $this->assertSame('Файл test.html не найден', $e->getMessage()); + } + + public function testFactoryUnknownCodeReturnsBaseClass(): void + { + $e = HTTPException::factory(418); + $this->assertInstanceOf(HTTPException::class, $e); + $this->assertSame(418, $e->getCode()); + } + + public function testGetResponseReturns404Response(): void + { + $e = HTTPException::factory(404); + $response = $e->get_response(); + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(404, $response->status()); + } + + public function testGetResponseReturns403Response(): void + { + $e = HTTPException::factory(403); + $response = $e->get_response(); + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(403, $response->status()); + } + + public function testGetResponseBodyContainsHtml(): void + { + $e = HTTPException::factory(404); + $body = $e->get_response()->body(); + $this->assertStringContainsString('404', $body); + } + + public function testExtendsMyException(): void + { + $e = HTTPException::factory(404); + $this->assertInstanceOf(\System\Classes\MyException::class, $e); + } +} diff --git a/tests/Unit/HttpClientRequestTest.php b/tests/Unit/HttpClientRequestTest.php new file mode 100644 index 0000000..182c75b --- /dev/null +++ b/tests/Unit/HttpClientRequestTest.php @@ -0,0 +1,87 @@ +assertInstanceOf(Request::class, $req); + $this->assertSame('https://example.com/api', $req->getUrl()); + } + + public function testDefaultMethodIsGet(): void + { + $req = Request::factory('https://example.com'); + $this->assertSame('GET', $req->getMethod()); + } + + public function testMethodSetter(): void + { + $req = Request::factory('https://example.com')->method('post'); + $this->assertSame('POST', $req->getMethod()); + } + + public function testHeaderSetter(): void + { + $req = Request::factory('https://example.com') + ->header('Authorization', 'Bearer token123'); + $this->assertSame(['Authorization' => 'Bearer token123'], $req->getHeaders()); + } + + public function testBodySetter(): void + { + $req = Request::factory('https://example.com')->body('raw body'); + $this->assertSame('raw body', $req->getBody()); + } + + public function testDefaultBodyIsNull(): void + { + $req = Request::factory('https://example.com'); + $this->assertNull($req->getBody()); + } + + public function testJsonSetsBodyAndHeaders(): void + { + $req = Request::factory('https://example.com')->json(['key' => 'value']); + $this->assertStringContainsString('"key":"value"', $req->getBody()); + $this->assertSame('application/json', $req->getHeaders()['Content-Type']); + $this->assertSame('application/json', $req->getHeaders()['Accept']); + } + + public function testJsonEncodesUnicode(): void + { + $req = Request::factory('https://example.com')->json(['name' => 'Иван']); + $this->assertStringContainsString('Иван', $req->getBody()); + } + + public function testTimeoutDefault(): void + { + $req = Request::factory('https://example.com'); + $this->assertSame(30, $req->getTimeout()); + } + + public function testTimeoutSetter(): void + { + $req = Request::factory('https://example.com')->timeout(10); + $this->assertSame(10, $req->getTimeout()); + } + + public function testFluentInterface(): void + { + $req = Request::factory('https://example.com') + ->method('POST') + ->header('X-Custom', 'value') + ->timeout(5) + ->body('data'); + + $this->assertSame('POST', $req->getMethod()); + $this->assertSame('value', $req->getHeaders()['X-Custom']); + $this->assertSame(5, $req->getTimeout()); + $this->assertSame('data', $req->getBody()); + } +} diff --git a/tests/Unit/MyExceptionTest.php b/tests/Unit/MyExceptionTest.php new file mode 100644 index 0000000..33c7603 --- /dev/null +++ b/tests/Unit/MyExceptionTest.php @@ -0,0 +1,58 @@ +assertSame('Something went wrong', $e->getMessage()); + } + + public function testMessageWithVariables(): void + { + $e = new MyException('File :file not found', [':file' => 'test.html']); + $this->assertSame('File test.html not found', $e->getMessage()); + } + + public function testMultipleVariables(): void + { + $e = new MyException(':a + :b = :c', [':a' => '1', ':b' => '2', ':c' => '3']); + $this->assertSame('1 + 2 = 3', $e->getMessage()); + } + + public function testNullVariablesLeavesMessageUntouched(): void + { + $e = new MyException('Plain :message', null); + $this->assertSame('Plain :message', $e->getMessage()); + } + + public function testCodeIsSet(): void + { + $e = new MyException('Error', null, 42); + $this->assertSame(42, $e->getCode()); + } + + public function testDefaultCode(): void + { + $e = new MyException('Error'); + $this->assertSame(0, $e->getCode()); + } + + public function testPreviousException(): void + { + $prev = new \Exception('original'); + $e = new MyException('Wrapped', null, 0, $prev); + $this->assertSame($prev, $e->getPrevious()); + } + + public function testExtendsException(): void + { + $e = new MyException('test'); + $this->assertInstanceOf(\Exception::class, $e); + } +} diff --git a/tests/Unit/RequestTest.php b/tests/Unit/RequestTest.php new file mode 100644 index 0000000..b98b408 --- /dev/null +++ b/tests/Unit/RequestTest.php @@ -0,0 +1,212 @@ +assertSame('GET', $req->method()); + } + + public function testMethodPost(): void + { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $req = Request::factory(); + $this->assertSame('POST', $req->method()); + } + + public function testMethodDefaultsToGet(): void + { + unset($_SERVER['REQUEST_METHOD']); + $req = Request::factory(); + $this->assertSame('GET', $req->method()); + } + + public function testMethodNormalizesToUppercase(): void + { + $_SERVER['REQUEST_METHOD'] = 'delete'; + $req = Request::factory(); + $this->assertSame('DELETE', $req->method()); + } + + public function testFactoryParsesRootUri(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $req = Request::factory(); + $this->assertSame('Index', $req->controller()); + $this->assertSame('index', $req->action()); + $this->assertSame([], $req->params()); + } + + public function testFactoryParsesControllerAndAction(): void + { + $_SERVER['REQUEST_URI'] = '/index/about'; + $req = Request::factory(); + $this->assertSame('Index', $req->controller()); + $this->assertSame('about', $req->action()); + } + + public function testFactoryParsesParams(): void + { + $_SERVER['REQUEST_URI'] = '/index/show/hello/world'; + $req = Request::factory(); + $this->assertSame(['hello', 'world'], $req->params()); + } + + public function testParamByIndex(): void + { + $_SERVER['REQUEST_URI'] = '/index/show/42/extra'; + $req = Request::factory(); + $this->assertSame('42', $req->param(0)); + $this->assertSame('extra', $req->param(1)); + } + + public function testParamDefaultValue(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $req = Request::factory(); + $this->assertNull($req->param(0)); + $this->assertSame('fallback', $req->param(0, 'fallback')); + } + + public function testFactorySetsCurrentRequest(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $req = Request::factory(); + $this->assertSame($req, Request::$current); + } + + public function testUriGetter(): void + { + $_SERVER['REQUEST_URI'] = '/index/test'; + $req = Request::factory(); + $this->assertSame('index/test', $req->uri()); + } + + public function testUriQueryStringIsStripped(): void + { + $_SERVER['REQUEST_URI'] = '/index/test?foo=bar&baz=1'; + $req = Request::factory(); + $this->assertSame('index/test', $req->uri()); + } + + public function testInitialIsSetOnFirstCall(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $req = Request::factory(); + $this->assertSame($req, Request::$initial); + } + + public function testPostArraySetter(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $req = Request::factory(); + $req->post(['name' => 'Ivan', 'age' => '30']); + $this->assertSame(['name' => 'Ivan', 'age' => '30'], $req->post()); + } + + public function testPostKeyGetter(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $req = Request::factory(); + $req->post(['name' => 'Ivan']); + $this->assertSame('Ivan', $req->post('name')); + $this->assertNull($req->post('missing')); + } + + public function testPostKeySetter(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $req = Request::factory(); + $req->post('key', 'value'); + $this->assertSame('value', $req->post('key')); + } + + public function testQueryArraySetter(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $req = Request::factory(); + $req->query(['page' => '2', 'sort' => 'name']); + $this->assertSame(['page' => '2', 'sort' => 'name'], $req->query()); + } + + public function testQueryKeyGetter(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $req = Request::factory(); + $req->query(['page' => '3']); + $this->assertSame('3', $req->query('page')); + $this->assertNull($req->query('missing')); + } + + public function testBodyGetterSetter(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $req = Request::factory(); + $this->assertSame('', $req->body()); + $req->body('{"key":"val"}'); + $this->assertSame('{"key":"val"}', $req->body()); + } + + public function testIsAjaxFalseByDefault(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $req = Request::factory(); + $this->assertFalse($req->is_ajax()); + } + + public function testIsAjaxTrueWhenHeaderSet(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'; + $req = Request::factory(); + $this->assertTrue($req->is_ajax()); + } + + public function testMethodSetter(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $req = Request::factory(); + $same = $req->method('patch'); + $this->assertSame($req, $same); + $this->assertSame('PATCH', $req->method()); + } + + public function testFactoryPopulatesGetFromGlobals(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $_GET = ['foo' => 'bar']; + $req = Request::factory(); + $this->assertSame('bar', $req->query('foo')); + } + + public function testFactoryPopulatesPostFromGlobals(): void + { + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST = ['field' => 'hello']; + $req = Request::factory(); + $this->assertSame('hello', $req->post('field')); + } +} diff --git a/tests/Unit/ResponseTest.php b/tests/Unit/ResponseTest.php new file mode 100644 index 0000000..7c86a14 --- /dev/null +++ b/tests/Unit/ResponseTest.php @@ -0,0 +1,60 @@ +assertSame('', $r->body()); + $this->assertSame(200, $r->status()); + } + + public function testConstructorStatus(): void + { + $r = new Response(404); + $this->assertSame(404, $r->status()); + } + + public function testBodySetterReturnsThis(): void + { + $r = new Response(); + $same = $r->body('hello'); + $this->assertSame($r, $same); + $this->assertSame('hello', $r->body()); + } + + public function testStatusSetterReturnsThis(): void + { + $r = new Response(); + $same = $r->status(301); + $this->assertSame($r, $same); + $this->assertSame(301, $r->status()); + } + + public function testBodyOverwrite(): void + { + $r = (new Response())->body('first'); + $r->body('second'); + $this->assertSame('second', $r->body()); + } + + public function testBodyEmpty(): void + { + $r = (new Response())->body('content'); + $r->body(''); + $this->assertSame('', $r->body()); + } + + public function testFluentChain(): void + { + $r = new Response(); + $body = $r->status(201)->body('created')->body(); + $this->assertSame('created', $body); + $this->assertSame(201, $r->status()); + } +} diff --git a/tests/Unit/RouteTest.php b/tests/Unit/RouteTest.php new file mode 100644 index 0000000..46d22e3 --- /dev/null +++ b/tests/Unit/RouteTest.php @@ -0,0 +1,76 @@ +assertSame(['controller' => 'Index', 'action' => 'index', 'params' => []], $r); + } + + public function testControllerOnly(): void + { + $r = Route::resolve('index'); + $this->assertSame('Index', $r['controller']); + $this->assertSame('index', $r['action']); + $this->assertSame([], $r['params']); + } + + public function testControllerAndAction(): void + { + $r = Route::resolve('index/about'); + $this->assertSame('Index', $r['controller']); + $this->assertSame('about', $r['action']); + $this->assertSame([], $r['params']); + } + + public function testControllerActionAndParams(): void + { + $r = Route::resolve('index/show/42/extra'); + $this->assertSame('Index', $r['controller']); + $this->assertSame('show', $r['action']); + $this->assertSame(['42', 'extra'], $r['params']); + } + + public function testSingleParam(): void + { + $r = Route::resolve('index/view/99'); + $this->assertSame(['99'], $r['params']); + } + + public function testCaseInsensitiveController(): void + { + $r = Route::resolve('INDEX/show'); + // Файловая система находит IndexController.php → возвращает 'Index' + $this->assertSame('Index', $r['controller']); + $this->assertSame('show', $r['action']); + } + + public function testUnknownControllerFallsBackToUcfirst(): void + { + $r = Route::resolve('nonexistent'); + $this->assertSame('Nonexistent', $r['controller']); + $this->assertSame('index', $r['action']); + $this->assertSame([], $r['params']); + } + + public function testUnknownControllerWithAction(): void + { + $r = Route::resolve('nonexistent/doSomething'); + $this->assertSame('Nonexistent', $r['controller']); + $this->assertSame('doSomething', $r['action']); + } + + public function testUnknownControllerWithParams(): void + { + $r = Route::resolve('nonexistent/action/1/2'); + $this->assertSame('Nonexistent', $r['controller']); + $this->assertSame('action', $r['action']); + $this->assertSame(['1', '2'], $r['params']); + } +} diff --git a/tests/Unit/SessionTest.php b/tests/Unit/SessionTest.php new file mode 100644 index 0000000..ab328dd --- /dev/null +++ b/tests/Unit/SessionTest.php @@ -0,0 +1,97 @@ +session = Session::instance('test'); + } + + protected function tearDown(): void + { + $this->session->destroy(); + + // Сбрасываем синглтон через рефлексию + $ref = new \ReflectionClass(Session::class); + $prop = $ref->getProperty('instances'); + $prop->setAccessible(true); + $prop->setValue(null, []); + } + + public function testSetAndGet(): void + { + $this->session->set('foo', 'bar'); + $this->assertSame('bar', $this->session->get('foo')); + } + + public function testGetDefault(): void + { + $this->assertNull($this->session->get('nonexistent')); + $this->assertSame('default', $this->session->get('nonexistent', 'default')); + } + + public function testDelete(): void + { + $this->session->set('key', 'value'); + $this->session->delete('key'); + $this->assertNull($this->session->get('key')); + } + + public function testSetReturnsThis(): void + { + $result = $this->session->set('x', 1); + $this->assertSame($this->session, $result); + } + + public function testDeleteReturnsThis(): void + { + $result = $this->session->delete('x'); + $this->assertSame($this->session, $result); + } + + public function testGetDataReturnsArray(): void + { + $this->session->set('a', 1)->set('b', 2); + $data = $this->session->getData(); + $this->assertIsArray($data); + $this->assertSame(1, $data['a']); + $this->assertSame(2, $data['b']); + } + + public function testIdIsString(): void + { + $this->assertIsString($this->session->id()); + $this->assertNotEmpty($this->session->id()); + } + + public function testInstanceReturnsSameSingleton(): void + { + $a = Session::instance('test'); + $b = Session::instance('test'); + $this->assertSame($a, $b); + } + + public function testBindByReference(): void + { + $value = 'initial'; + $this->session->bind('ref', $value); + $value = 'changed'; + $this->assertSame('changed', $this->session->get('ref')); + } + + public function testDestroyReturnsTrueAndClearsData(): void + { + $this->session->set('key', 'val'); + $result = $this->session->destroy(); + $this->assertTrue($result); + $this->assertEmpty($this->session->getData()); + } +} diff --git a/tests/Unit/ViewTest.php b/tests/Unit/ViewTest.php new file mode 100644 index 0000000..4eeede0 --- /dev/null +++ b/tests/Unit/ViewTest.php @@ -0,0 +1,120 @@ +tmpFile = sys_get_temp_dir() . '/bicycle_view_' . uniqid() . '.html'; + } + + protected function tearDown(): void + { + if (file_exists($this->tmpFile)) { + unlink($this->tmpFile); + } + } + + public function testRenderStaticContent(): void + { + file_put_contents($this->tmpFile, 'hello world'); + $view = new View(); + $view->_file = $this->tmpFile; + $this->assertSame('hello world', $view->render()); + } + + public function testRenderWithData(): void + { + file_put_contents($this->tmpFile, ''); + $view = new View(); + $view->_file = $this->tmpFile; + $view->_data = ['name' => 'bicycle']; + $this->assertSame('bicycle', $view->render()); + } + + public function testRenderMultipleVars(): void + { + file_put_contents($this->tmpFile, ''); + $view = new View(); + $view->_file = $this->tmpFile; + $view->_data = ['a' => 'foo', 'b' => 'bar']; + $this->assertSame('foo-bar', $view->render()); + } + + public function testRenderThrowsWhenFileNotSet(): void + { + $this->expectException(MyException::class); + (new View())->render(); + } + + public function testSetFilenameFindsFileInAppPath(): void + { + $view = new View(); + $view->setFilename('index', 'view/Index'); + $this->assertStringContainsString('App', $view->_file); + $this->assertStringEndsWith('index.html', $view->_file); + $this->assertFileExists($view->_file); + } + + public function testSetFilenameFindsFileInSysPath(): void + { + $view = new View(); + $view->setFilename('404', 'view/errors'); + $this->assertStringContainsString('System', $view->_file); + $this->assertFileExists($view->_file); + } + + public function testSetFilenameThrowsWhenNotFound(): void + { + $this->expectException(MyException::class); + (new View())->setFilename('nonexistent_xyz', 'view/Index'); + } + + public function testSetFilenameThrowsWhenDirEmpty(): void + { + $this->expectException(MyException::class); + (new View())->setFilename('index', ''); + } + + public function testConstructorWithFileAndDir(): void + { + $view = new View('index', 'view/Index'); + $this->assertNotEmpty($view->_file); + $this->assertFileExists($view->_file); + } + + public function testConstructorWithData(): void + { + file_put_contents($this->tmpFile, ''); + $view = new View(); + $view->_file = $this->tmpFile; + $view->_data = ['val' => 'test_value']; + $this->assertSame('test_value', $view->render()); + } + + public function testExtractSkipDoesNotOverrideLocalVar(): void + { + // EXTR_SKIP не перезаписывает уже существующие переменные в шаблоне + file_put_contents($this->tmpFile, ''); + $view = new View(); + $view->_file = $this->tmpFile; + $view->_data = ['x' => 'from_data']; + // Здесь x уже определена в шаблоне ДО extract — EXTR_SKIP не перезапишет + // Но View делает extract ДО include, поэтому $x = 'from_data' попадёт в шаблон + $this->assertSame('local', $view->render()); + } + + public function testSetFilenameReturnsSelf(): void + { + $view = new View(); + $result = $view->setFilename('index', 'view/Index'); + $this->assertSame($view, $result); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..a945ab8 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,11 @@ +