Core MVC, HTTP client, Session, Cookie, Config, HTTPException — 111 PHPUnit tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
12 KiB
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
# Установка зависимостей
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: XMLHttpRequestdetect_uri()— определяет URI черезPATH_INFO→REQUEST_URI→PHP_SELF
External HTTP Client (System\Classes\HTTP\Client*)
Исходящие запросы к внешним API.
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)
Синглтон, поддерживает именованные сессии.
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)
Статический хелпер.
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 — глубоко мержится поверх базового.
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)
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/.