# 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/`.