Bicycle/CLAUDE.md
Egor Isaev b516ca07dc 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 <noreply@anthropic.com>
2026-06-04 10:04:19 +03:00

248 lines
12 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
```
Проект запускается как веб-приложение через 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/`.