Bicycle/CLAUDE.md
Egor Isaev b65603b4d0 dev
2026-06-23 10:44:18 +03:00

254 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
# Один файл / один тест
docker exec bicycle vendor/bin/phpunit tests/Unit/RouteTest.php
docker exec bicycle vendor/bin/phpunit --filter testControllerAndAction tests/Unit/RouteTest.php
```
Composer-скриптов нет — phpunit вызывается напрямую. Запускать тесты надо именно внутри контейнера: PHP-интерпретатор живёт в Docker, volume `/home/isaevea/http/Bicycle``/opt/project`.
Проект запускается как веб-приложение через 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/config/ — конфиги (config.php; config.local.php — в .gitignore)
App/view/ — шаблоны приложения (.html файлы с PHP-кодом)
App/media/ — статические ресурсы (js, css, img)
Services/DataBase/, Mail/, PDF/ — пустые каталоги-заготовки под будущие сервисы (PDO / PHPMailer / dompdf); кода пока нет
System/view/ — системные шаблоны (ошибки 404/500/403, exception)
tools/sort_html_attrs.php — CLI-утилита сортировки HTML-атрибутов (gitignored: /tools/* в .gitignore)
tests/ — PHPUnit тесты
```
> Каталоги `App/Classes/` и наполнение `Services/*` ещё не созданы — namespace-конвенции ниже описывают, *куда* класть код, когда он появится, а не существующие файлы.
### 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()`, `isAjax()` |
| `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()`, `requestHeaders()` |
| `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)`, `getResponse(): 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() # detectUri(), 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-метода
- `isAjax()` — проверяет `X-Requested-With: XMLHttpRequest`
- `detectUri()` — определяет 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]);
// или через getResponse() без throw:
$response = HTTPException::factory(403)->getResponse();
```
`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 APP_ENV` в `.htaccess` (PRODUCTION / STAGING / TESTING / DEVELOPMENT).
По умолчанию — `DEVELOPMENT`. Значение читается в `index.php` через `getenv('APP_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()`, `isAjax()`, `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, `getResponse()`, 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/`.