- сверять новые файлы с `erp24/docs/*`
- предупреждать о конфликтующих материалах
+
+Вывод: готовая инструкция для разработки.
+
+##
+Задачи:
+- проверять отсутствие дублирования
+- сверять новые файлы с `erp24/docs/*`
+- предупреждать о конфликтующих материалах
+
+---
+
+# === PHP & YII2 STYLE GUIDE (SKILLS) ===
+
+При написании и анализе PHP-кода для проекта ERP24 необходимо использовать следующие гайдлайны, расположенные в `erp24/php_skills/`:
+
+## Основы PHP
+
+| Файл | Описание |
+|------|----------|
+| [01-php-basics.md](erp24/php_skills/01-php-basics.md) | Базовые правила форматирования и синтаксиса |
+| [02-php-naming.md](erp24/php_skills/02-php-naming.md) | Соглашения об именовании переменных, классов, методов |
+| [03-php-methods.md](erp24/php_skills/03-php-methods.md) | Методы и функции: сигнатуры, возвращаемые типы |
+| [04-php-classes.md](erp24/php_skills/04-php-classes.md) | Классы, интерфейсы, трейты, абстракции |
+| [05-php-collections.md](erp24/php_skills/05-php-collections.md) | Работа с массивами и коллекциями |
+| [06-php-strings.md](erp24/php_skills/06-php-strings.md) | Работа со строками |
+| [07-php-flow-control.md](erp24/php_skills/07-php-flow-control.md) | Управление потоком выполнения (if, switch, loops) |
+| [08-php-exceptions.md](erp24/php_skills/08-php-exceptions.md) | Обработка исключений |
+| [09-php-closures.md](erp24/php_skills/09-php-closures.md) | Замыкания и колбэки |
+
+## Yii2-специфичные правила
+
+| Файл | Описание |
+|------|----------|
+| [10-yii2-structure.md](erp24/php_skills/10-yii2-structure.md) | Структура Yii2 приложения |
+| [11-yii2-models.md](erp24/php_skills/11-yii2-models.md) | Модели и ActiveRecord |
+| [12-yii2-controllers.md](erp24/php_skills/12-yii2-controllers.md) | Контроллеры и actions |
+| [13-yii2-views.md](erp24/php_skills/13-yii2-views.md) | Представления и шаблоны |
+| [14-yii2-routing.md](erp24/php_skills/14-yii2-routing.md) | Маршрутизация |
+| [15-yii2-migrations.md](erp24/php_skills/15-yii2-migrations.md) | Миграции базы данных |
+| [16-yii2-testing.md](erp24/php_skills/16-yii2-testing.md) | Тестирование |
+| [17-yii2-security.md](erp24/php_skills/17-yii2-security.md) | Безопасность |
+| [18-yii2-performance.md](erp24/php_skills/18-yii2-performance.md) | Производительность и оптимизация |
+| [19-yii2-api.md](erp24/php_skills/19-yii2-api.md) | REST API разработка |
+| [20-yii2-widgets.md](erp24/php_skills/20-yii2-widgets.md) | Виджеты и компоненты |
+
+## Правила применения Skills
+
+1. **При написании нового кода** — обязательно сверяться с соответствующим гайдлайном
+2. **При code review** — проверять соответствие кода описанным стандартам
+3. **При рефакторинге** — приводить код в соответствие с гайдлайнами
+4. **При документировании** — использовать примеры из skills как образцы
+
+## Стандарты
+
+Гайдлайны основаны на:
+- **PSR-1**: Basic Coding Standard
+- **PSR-4**: Autoloading Standard
+- **PSR-12**: Extended Coding Style Guide
+- **Yii2 Coding Standards**
+
+## Версии технологий
+
+- **PHP**: 8.1+
+- **Yii2**: 2.0.45+
+
---
# === END OF CLAUDE.md ===
$this->stdout($json . "\n");
return ExitCode::OK;
}
+
+ /**
+ * Проверяет вложения документов списания и помечает документы с недоступными файлами.
+ * Если все вложения документа недоступны на диске, устанавливает attachment_cleared = 1.
+ *
+ * Пример запуска: php yii write-offs-attachments/check-missing
+ * С флагом --dry-run только выводит информацию без изменений в БД.
+ *
+ * @param bool $dryRun Режим тестового запуска (без изменений в БД)
+ * @return int
+ */
+ public function actionCheckMissing(bool $dryRun = false): int
+ {
+ $this->stdout("Проверка вложений документов списания...\n");
+
+ if ($dryRun) {
+ $this->stdout("Режим dry-run: изменения в БД не будут сохранены\n\n");
+ }
+
+ $docs = WriteOffsErp::find()
+ ->andWhere(['status' => WriteOffsErp::STATUS_CREATED_1C])
+ ->andWhere(['attachment_cleared' => 0])
+ ->all();
+
+ $this->stdout("Найдено документов для проверки: " . count($docs) . "\n\n");
+
+ $markedCount = 0;
+ $checkedCount = 0;
+
+ foreach ($docs as $doc) {
+ $attachments = $doc->getAttachments();
+
+ if (empty($attachments)) {
+ continue;
+ }
+
+ $checkedCount++;
+ $allMissing = true;
+ $missingFiles = [];
+ $existingFiles = [];
+
+ foreach ($attachments as $attachment) {
+ $url = $attachment['url'] ?? null;
+ if (empty($url)) {
+ continue;
+ }
+
+ $filePath = $this->getFilePathFromUrl($url);
+
+ if ($filePath && file_exists($filePath)) {
+ $allMissing = false;
+ $existingFiles[] = $url;
+ } else {
+ $missingFiles[] = $url;
+ }
+ }
+
+ if ($allMissing && !empty($missingFiles)) {
+ $markedCount++;
+ $this->stdout("Документ #{$doc->id} ({$doc->number}): все вложения недоступны\n");
+ $this->stdout(" Недоступные файлы: " . count($missingFiles) . "\n");
+
+ if (!$dryRun) {
+ $doc->attachment_cleared = 1;
+ $doc->attachment_cleared_at = date('Y-m-d H:i:s');
+ if (!$doc->save(false, ['attachment_cleared', 'attachment_cleared_at'])) {
+ $this->stderr(" Ошибка сохранения: " . json_encode($doc->getErrors(), JSON_UNESCAPED_UNICODE) . "\n");
+ } else {
+ $this->stdout(" Установлен attachment_cleared = 1, attachment_cleared_at = {$doc->attachment_cleared_at}\n");
+ }
+ }
+ }
+ }
+
+ $this->stdout("\n--- Итоги ---\n");
+ $this->stdout("Проверено документов с вложениями: {$checkedCount}\n");
+ $this->stdout("Помечено документов (attachment_cleared=1): {$markedCount}\n");
+
+ if ($dryRun && $markedCount > 0) {
+ $this->stdout("\nДля применения изменений запустите без --dry-run\n");
+ }
+
+ return ExitCode::OK;
+ }
+
+ /**
+ * Преобразует URL вложения в абсолютный путь к файлу на диске.
+ *
+ * @param string $url URL вида /uploads/images/xx/filename.jpg
+ * @return string|null Абсолютный путь к файлу или null
+ */
+ private function getFilePathFromUrl(string $url): ?string
+ {
+ if (strpos($url, '/uploads/') === 0) {
+ $relativePath = substr($url, 1);
+ $basePath = Yii::getAlias('@app');
+ $filePath = $basePath . '/' . $relativePath;
+ return $filePath;
+ }
+
+ return null;
+ }
}
'defaultSchema' => 'erp24' //specify your schema here, public is the default schema
]
],
- // 'on afterOpen' => function($event) { $event->sender->createCommand("SET search_path TO public, erp24;")->execute(); },
+ 'on afterOpen' => function($event) { $event->sender->createCommand("SET search_path TO public, erp24;")->execute(); },
// PostgreSQL
'charset' => 'utf8',
'enableSchemaCache' => true,
--- /dev/null
+<?php
+
+use yii\db\Migration;
+
+class m251209_120000_add_attachment_cleared_to_write_offs_erp extends Migration
+{
+ const TABLE_NAME = 'erp24.write_offs_erp';
+
+ /**
+ * {@inheritdoc}
+ */
+ public function safeUp()
+ {
+ $table = $this->db->schema->getTableSchema(self::TABLE_NAME);
+ if ($table === null) {
+ return;
+ }
+
+ if (!$this->db->schema->getTableSchema(self::TABLE_NAME, true)->getColumn('attachment_cleared')) {
+ $this->addColumn(
+ self::TABLE_NAME,
+ 'attachment_cleared',
+ $this->smallInteger()->notNull()->defaultValue(0)->comment('Флаг очистки вложений')
+ );
+ }
+
+ if (!$this->db->schema->getTableSchema(self::TABLE_NAME, true)->getColumn('attachment_cleared_at')) {
+ $this->addColumn(
+ self::TABLE_NAME,
+ 'attachment_cleared_at',
+ $this->timestamp()->null()->comment('Дата очистки вложений')
+ );
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function safeDown()
+ {
+ if ($this->db->schema->getTableSchema(self::TABLE_NAME, true)->getColumn('attachment_cleared_at')) {
+ $this->dropColumn(self::TABLE_NAME, 'attachment_cleared_at');
+ }
+
+ if ($this->db->schema->getTableSchema(self::TABLE_NAME, true)->getColumn('attachment_cleared')) {
+ $this->dropColumn(self::TABLE_NAME, 'attachment_cleared');
+ }
+ }
+}
--- /dev/null
+<?php
+
+use yii\db\Migration;
+
+class m251209_130000_add_attachment_cleared_at_to_write_offs_erp extends Migration
+{
+ const TABLE_NAME = 'erp24.write_offs_erp';
+
+ /**
+ * {@inheritdoc}
+ */
+ public function safeUp()
+ {
+ $table = $this->db->schema->getTableSchema(self::TABLE_NAME);
+ if ($table === null) {
+ return;
+ }
+
+ if (!$this->db->schema->getTableSchema(self::TABLE_NAME, true)->getColumn('attachment_cleared_at')) {
+ $this->addColumn(
+ self::TABLE_NAME,
+ 'attachment_cleared_at',
+ $this->timestamp()->null()->comment('Дата очистки вложений')
+ );
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function safeDown()
+ {
+ if ($this->db->schema->getTableSchema(self::TABLE_NAME, true)->getColumn('attachment_cleared_at')) {
+ $this->dropColumn(self::TABLE_NAME, 'attachment_cleared_at');
+ }
+ }
+}
--- /dev/null
+# PHP Basics - Основы форматирования и синтаксиса
+
+## Кодировка и форматирование
+
+### Кодировка файлов
+```php
+<?php
+// Все файлы должны использовать UTF-8 без BOM
+// Объявление строгой типизации в начале файла
+declare(strict_types=1);
+```
+
+### Теги PHP
+```php
+<?php
+// ПРАВИЛЬНО - полный тег <?php
+
+// НЕПРАВИЛЬНО - короткий тег
+<? echo 'text'; ?>
+
+// ПРАВИЛЬНО для шаблонов - короткий echo тег
+<?= $variable ?>
+
+// НЕПРАВИЛЬНО - закрывающий тег в конце файла с чистым PHP
+?>
+```
+
+### Отступы
+```php
+<?php
+// ПРАВИЛЬНО - 4 пробела
+class User
+{
+ public function getName(): string
+ {
+ return $this->name;
+ }
+}
+
+// НЕПРАВИЛЬНО - 2 пробела или табы
+class User
+{
+ public function getName(): string
+ {
+ return $this->name;
+ }
+}
+```
+
+### Максимальная длина строки
+- **120 символов** - мягкое ограничение PSR-12
+- **80 символов** - рекомендуемая длина для читаемости
+- Настройте редактор для визуализации границы
+
+### Окончания строк
+- Используйте Unix-style (LF)
+- Каждый файл должен заканчиваться одной пустой строкой
+- Нет пробелов в конце строк
+
+## Пробелы и операторы
+
+### Вокруг операторов
+```php
+<?php
+// ПРАВИЛЬНО
+$sum = 1 + 2;
+$a = 1;
+$isEqual = ($a === $b);
+
+// НЕПРАВИЛЬНО
+$sum=1+2;
+$a=1;
+```
+
+### Унарные операторы
+```php
+<?php
+// ПРАВИЛЬНО - без пробела
+$i++;
+--$count;
+$result = !$flag;
+
+// НЕПРАВИЛЬНО
+$i ++;
+-- $count;
+```
+
+### Операторы конкатенации
+```php
+<?php
+// ПРАВИЛЬНО - пробелы вокруг точки
+$fullName = $firstName . ' ' . $lastName;
+
+// НЕПРАВИЛЬНО
+$fullName = $firstName.' '.$lastName;
+```
+
+### Тернарный оператор
+```php
+<?php
+// ПРАВИЛЬНО - пробелы вокруг ? и :
+$status = $isActive ? 'active' : 'inactive';
+
+// Для многострочных выражений
+$status = $isActive
+ ? 'active'
+ : 'inactive';
+```
+
+## Скобки и фигурные скобки
+
+### Управляющие конструкции
+```php
+<?php
+// ПРАВИЛЬНО - скобка на новой строке для классов/методов (Allman style)
+class User
+{
+ public function save(): bool
+ {
+ if ($this->validate()) {
+ // код
+ }
+
+ return true;
+ }
+}
+
+// ПРАВИЛЬНО для if/for/while - скобка на той же строке (K&R style)
+if ($condition) {
+ // код
+} elseif ($anotherCondition) {
+ // код
+} else {
+ // код
+}
+
+for ($i = 0; $i < 10; $i++) {
+ // код
+}
+
+while ($condition) {
+ // код
+}
+
+foreach ($items as $key => $value) {
+ // код
+}
+```
+
+### Вызов функций и методов
+```php
+<?php
+// ПРАВИЛЬНО - без пробела перед скобкой
+$result = someFunction($arg1, $arg2);
+$user->getName();
+
+// НЕПРАВИЛЬНО
+$result = someFunction ($arg1, $arg2);
+```
+
+### Массивы
+```php
+<?php
+// ПРАВИЛЬНО - короткий синтаксис
+$array = ['one', 'two', 'three'];
+
+$associative = [
+ 'key1' => 'value1',
+ 'key2' => 'value2',
+];
+
+// НЕПРАВИЛЬНО - длинный синтаксис (устаревший)
+$array = array('one', 'two', 'three');
+```
+
+## Пустые строки
+
+### Между методами
+```php
+<?php
+class User
+{
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): void
+ {
+ $this->name = $name;
+ }
+}
+```
+
+### После use statements
+```php
+<?php
+
+namespace App\Models;
+
+use Yii;
+use yii\db\ActiveRecord;
+use yii\behaviors\TimestampBehavior;
+
+class User extends ActiveRecord
+{
+ // код
+}
+```
+
+### Группировка use statements
+```php
+<?php
+
+namespace App\Controllers;
+
+// Встроенные PHP классы
+use DateTime;
+use RuntimeException;
+
+// Фреймворк
+use yii\web\Controller;
+use yii\web\Response;
+
+// Приложение
+use app\models\User;
+use app\services\AuthService;
+```
+
+## Комментарии
+
+### PHPDoc комментарии
+```php
+<?php
+/**
+ * Класс для работы с пользователями.
+ *
+ * Предоставляет методы для создания, обновления и удаления пользователей.
+ *
+ * @author John Doe <john@example.com>
+ * @since 1.0
+ */
+class User extends ActiveRecord
+{
+ /**
+ * Находит пользователя по email.
+ *
+ * @param string $email Email адрес для поиска
+ * @return User|null Найденный пользователь или null
+ * @throws InvalidArgumentException если email невалидный
+ */
+ public static function findByEmail(string $email): ?User
+ {
+ // код
+ }
+}
+```
+
+### Однострочные комментарии
+```php
+<?php
+// ПРАВИЛЬНО - пробел после //
+// Это комментарий
+$value = 10; // Inline комментарий
+
+// НЕПРАВИЛЬНО
+//Это комментарий
+$value = 10; //Inline комментарий
+
+# Не используйте # для комментариев
+```
+
+### TODO и FIXME
+```php
+<?php
+// TODO: добавить валидацию email
+// FIXME: исправить утечку памяти при больших выборках
+// HACK: временное решение до рефакторинга
+// NOTE: важное замечание о бизнес-логике
+```
+
+## Объявление типов (PHP 8+)
+
+### Типы параметров
+```php
+<?php
+// ПРАВИЛЬНО - строгая типизация
+declare(strict_types=1);
+
+function processUser(User $user, int $limit = 10): array
+{
+ // код
+}
+
+// Union types (PHP 8.0+)
+function process(int|string $value): void
+{
+ // код
+}
+
+// Nullable типы
+function findUser(int $id): ?User
+{
+ return User::findOne($id);
+}
+
+// Intersection types (PHP 8.1+)
+function handle(Countable&Iterator $collection): void
+{
+ // код
+}
+```
+
+### Типизированные свойства
+```php
+<?php
+class User
+{
+ public int $id;
+ public string $name;
+ public ?string $email = null;
+ public readonly string $createdAt; // PHP 8.1+
+
+ // PHP 8.2+ - свойства только для чтения в конструкторе
+ public function __construct(
+ public readonly int $id,
+ public readonly string $name,
+ ) {}
+}
+```
+
+## Константы
+
+### Константы класса
+```php
+<?php
+class Order
+{
+ // ПРАВИЛЬНО - SCREAMING_SNAKE_CASE
+ public const STATUS_PENDING = 'pending';
+ public const STATUS_COMPLETED = 'completed';
+ public const MAX_ITEMS = 100;
+
+ // PHP 8.1+ - final константы
+ final public const VERSION = '1.0';
+
+ // PHP 8.1+ - enum вместо констант
+}
+
+// Enum (PHP 8.1+)
+enum OrderStatus: string
+{
+ case Pending = 'pending';
+ case Completed = 'completed';
+ case Cancelled = 'cancelled';
+}
+```
+
+## Атрибуты (PHP 8.0+)
+
+```php
+<?php
+use Attribute;
+
+#[Attribute]
+class Route
+{
+ public function __construct(
+ public string $path,
+ public string $method = 'GET',
+ ) {}
+}
+
+class UserController
+{
+ #[Route('/users', 'GET')]
+ public function index(): array
+ {
+ // код
+ }
+
+ #[Route('/users/{id}', 'GET')]
+ #[Cache(ttl: 3600)]
+ public function show(int $id): User
+ {
+ // код
+ }
+}
+```
+
+## Именованные аргументы (PHP 8.0+)
+
+```php
+<?php
+// ПРАВИЛЬНО - именованные аргументы для ясности
+$user = new User(
+ name: 'John',
+ email: 'john@example.com',
+ isActive: true,
+);
+
+// Полезно когда много опциональных параметров
+htmlspecialchars(
+ string: $text,
+ flags: ENT_QUOTES,
+ encoding: 'UTF-8',
+);
+
+// НЕПРАВИЛЬНО - смешивание позиционных и именованных после именованного
+// someFunction($value, name: 'test', $another); // Ошибка!
+```
+
+## Match выражение (PHP 8.0+)
+
+```php
+<?php
+// ПРАВИЛЬНО - match вместо switch для возврата значений
+$statusText = match ($status) {
+ 'pending' => 'В ожидании',
+ 'active' => 'Активен',
+ 'cancelled', 'deleted' => 'Недоступен',
+ default => 'Неизвестный статус',
+};
+
+// НЕПРАВИЛЬНО - громоздкий switch
+$statusText = '';
+switch ($status) {
+ case 'pending':
+ $statusText = 'В ожидании';
+ break;
+ case 'active':
+ $statusText = 'Активен';
+ break;
+ // ...
+}
+```
+
+## Nullsafe оператор (PHP 8.0+)
+
+```php
+<?php
+// ПРАВИЛЬНО - nullsafe оператор
+$city = $user?->getAddress()?->getCity();
+
+// НЕПРАВИЛЬНО - множественные проверки
+$city = null;
+if ($user !== null) {
+ $address = $user->getAddress();
+ if ($address !== null) {
+ $city = $address->getCity();
+ }
+}
+```
+
+## Рекомендации для Claude Code
+
+1. **Всегда используйте `declare(strict_types=1);`** в начале файлов
+2. **Указывайте типы** для всех параметров, возвращаемых значений и свойств
+3. **Используйте PSR-12** как основу форматирования
+4. **Не смешивайте** табы и пробелы
+5. **Используйте автоформатирование** через PHP-CS-Fixer
+6. **Предпочитайте** новый синтаксис PHP 8+ где возможно
--- /dev/null
+# PHP Naming - Соглашения об именовании
+
+## Общие правила
+
+### Язык идентификаторов
+```php
+<?php
+// ПРАВИЛЬНО - английский язык
+$salary = 1000;
+$userName = 'John';
+
+// НЕПРАВИЛЬНО - транслитерация или кириллица
+$zaplata = 1000;
+// $заплата = 1000; // вызовет ошибку
+```
+
+## Стили именования
+
+### camelCase для методов и свойств
+```php
+<?php
+class User
+{
+ // ПРАВИЛЬНО - camelCase для свойств
+ public string $firstName;
+ public string $lastName;
+ private int $accessLevel;
+
+ // ПРАВИЛЬНО - camelCase для методов
+ public function getFullName(): string
+ {
+ return $this->firstName . ' ' . $this->lastName;
+ }
+
+ public function setAccessLevel(int $level): void
+ {
+ $this->accessLevel = $level;
+ }
+}
+
+// НЕПРАВИЛЬНО - snake_case для методов/свойств
+class User
+{
+ public string $first_name;
+
+ public function get_full_name(): string
+ {
+ // ...
+ }
+}
+```
+
+### PascalCase для классов и интерфейсов
+```php
+<?php
+// ПРАВИЛЬНО - PascalCase
+class UserAccount
+{
+ // код
+}
+
+interface Authenticatable
+{
+ // код
+}
+
+trait HasTimestamps
+{
+ // код
+}
+
+// Аббревиатуры - как обычные слова
+class HttpClient // не HTTPClient
+{
+ // код
+}
+
+class XmlParser // не XMLParser
+{
+ // код
+}
+
+// НЕПРАВИЛЬНО
+class user_account
+{
+}
+
+class userAccount
+{
+}
+```
+
+### SCREAMING_SNAKE_CASE для констант
+```php
+<?php
+class Config
+{
+ // ПРАВИЛЬНО
+ public const MAX_LOGIN_ATTEMPTS = 5;
+ public const API_VERSION = 'v1';
+ public const DEFAULT_TIMEOUT = 30;
+
+ // НЕПРАВИЛЬНО
+ public const maxLoginAttempts = 5;
+ public const api_version = 'v1';
+}
+
+// Глобальные константы (избегайте)
+define('APP_DEBUG', true);
+```
+
+### snake_case для функций (вне классов)
+```php
+<?php
+// В Yii2 и PHP принято для helper-функций
+function array_key_exists_recursive(array $array, string $key): bool
+{
+ // код
+}
+
+// Но для автозагружаемых классов лучше методы
+class ArrayHelper
+{
+ public static function keyExistsRecursive(array $array, string $key): bool
+ {
+ // код
+ }
+}
+```
+
+## Файлы и директории
+
+### Имена файлов классов
+```php
+<?php
+// ПРАВИЛЬНО - совпадает с именем класса
+// Файл: User.php
+class User
+{
+}
+
+// Файл: UserController.php
+class UserController
+{
+}
+
+// Файл: UserService.php
+class UserService
+{
+}
+
+// НЕПРАВИЛЬНО
+// Файл: user.php или user_controller.php
+```
+
+### Структура директорий (PSR-4)
+```
+// ПРАВИЛЬНО
+src/
+├── Controllers/
+│ └── UserController.php
+├── Models/
+│ └── User.php
+├── Services/
+│ └── UserService.php
+└── Helpers/
+ └── StringHelper.php
+
+// Пространство имён соответствует директории
+namespace App\Controllers;
+
+class UserController
+{
+}
+```
+
+### Соответствие имени файла и namespace
+```php
+<?php
+// Файл: app/models/User.php
+namespace app\models;
+
+class User extends \yii\db\ActiveRecord
+{
+ // код
+}
+
+// Файл: app/services/user/RegistrationService.php
+namespace app\services\user;
+
+class RegistrationService
+{
+ // код
+}
+```
+
+## Методы
+
+### Предикаты (возвращают boolean)
+```php
+<?php
+class User
+{
+ // ПРАВИЛЬНО - префиксы is/has/can/should
+ public function isActive(): bool
+ {
+ return $this->status === self::STATUS_ACTIVE;
+ }
+
+ public function hasPermission(string $permission): bool
+ {
+ return in_array($permission, $this->permissions, true);
+ }
+
+ public function canEdit(): bool
+ {
+ return $this->role === 'admin';
+ }
+
+ public function shouldNotify(): bool
+ {
+ return $this->notificationsEnabled;
+ }
+
+ // НЕПРАВИЛЬНО
+ public function active(): bool // неясно, что это предикат
+ {
+ // ...
+ }
+
+ public function checkPermission(): bool // check не говорит о возврате bool
+ {
+ // ...
+ }
+}
+```
+
+### Геттеры и сеттеры
+```php
+<?php
+class Product
+{
+ private float $price;
+ private int $quantity;
+
+ // ПРАВИЛЬНО - стандартные get/set
+ public function getPrice(): float
+ {
+ return $this->price;
+ }
+
+ public function setPrice(float $price): void
+ {
+ $this->price = $price;
+ }
+
+ // Fluent интерфейс - возвращаем $this
+ public function setQuantity(int $quantity): self
+ {
+ $this->quantity = $quantity;
+ return $this;
+ }
+
+ // PHP 8 - можно использовать promoted properties
+ public function __construct(
+ private float $price,
+ private int $quantity = 0,
+ ) {}
+}
+```
+
+### Действия и команды
+```php
+<?php
+class OrderService
+{
+ // ПРАВИЛЬНО - глаголы для действий
+ public function create(array $data): Order
+ {
+ // создание
+ }
+
+ public function update(Order $order, array $data): Order
+ {
+ // обновление
+ }
+
+ public function delete(Order $order): void
+ {
+ // удаление
+ }
+
+ public function process(Order $order): void
+ {
+ // обработка
+ }
+
+ public function calculateTotal(Order $order): float
+ {
+ // вычисление
+ }
+
+ // НЕПРАВИЛЬНО - существительные для действий
+ public function orderCreation(array $data): Order
+ {
+ // ...
+ }
+}
+```
+
+## Переменные
+
+### Локальные переменные
+```php
+<?php
+function calculateTotal(array $items): float
+{
+ // ПРАВИЛЬНО - camelCase, описательные имена
+ $subtotal = 0.0;
+ $taxAmount = 0.0;
+ $discountPercent = 10;
+
+ foreach ($items as $item) {
+ $itemPrice = $item->getPrice();
+ $subtotal += $itemPrice;
+ }
+
+ // ПРАВИЛЬНО для счётчиков/итераторов - короткие имена
+ for ($i = 0; $i < count($items); $i++) {
+ // ...
+ }
+
+ // НЕПРАВИЛЬНО
+ $s = 0.0; // неясно что это
+ $sub_total = 0.0; // snake_case
+}
+```
+
+### Переменные в циклах
+```php
+<?php
+// ПРАВИЛЬНО - описательные имена для коллекций
+foreach ($users as $user) {
+ echo $user->getName();
+}
+
+foreach ($orderItems as $item) {
+ // ...
+}
+
+// ПРАВИЛЬНО - key => value
+foreach ($settings as $key => $value) {
+ // ...
+}
+
+foreach ($users as $userId => $userData) {
+ // ...
+}
+
+// Неиспользуемые переменные - с подчёркиванием (соглашение)
+foreach ($items as $_ => $item) {
+ // используем только $item
+}
+```
+
+## Параметры методов
+
+```php
+<?php
+class UserService
+{
+ // ПРАВИЛЬНО - описательные имена параметров
+ public function createUser(
+ string $email,
+ string $password,
+ string $firstName,
+ string $lastName,
+ bool $isAdmin = false,
+ ): User {
+ // ...
+ }
+
+ // PHP 8+ - именованные аргументы делают код чище
+ // Вызов: createUser(email: 'test@test.com', ...)
+
+ // НЕПРАВИЛЬНО - однобуквенные или неясные имена
+ public function createUser(string $e, string $p, string $f, string $l): User
+ {
+ // ...
+ }
+}
+```
+
+## Специальные случаи
+
+### Приватные/protected свойства
+```php
+<?php
+class User
+{
+ // Yii2 стиль - без префикса для private/protected
+ private string $password;
+ protected int $accessLevel;
+
+ // Некоторые стандарты используют _ префикс (избегайте в новом коде)
+ // private string $_password; // устаревший стиль
+}
+```
+
+### Статические свойства и методы
+```php
+<?php
+class Logger
+{
+ // Статические свойства - camelCase
+ private static array $instances = [];
+
+ // Статические методы - camelCase
+ public static function getInstance(string $channel): self
+ {
+ // ...
+ }
+
+ // Фабричные методы - create/from/make префиксы
+ public static function createFromArray(array $config): self
+ {
+ // ...
+ }
+
+ public static function fromJson(string $json): self
+ {
+ // ...
+ }
+}
+```
+
+### Интерфейсы и трейты
+```php
+<?php
+// Интерфейсы - *able, *Interface суффиксы
+interface Authenticatable
+{
+ public function authenticate(): bool;
+}
+
+interface UserRepositoryInterface
+{
+ public function findById(int $id): ?User;
+}
+
+// Трейты - *Trait суффикс или описательное имя
+trait HasTimestamps
+{
+ public int $createdAt;
+ public int $updatedAt;
+}
+
+trait LoggerAwareTrait
+{
+ protected ?LoggerInterface $logger = null;
+}
+```
+
+### Exceptions
+```php
+<?php
+// ПРАВИЛЬНО - *Exception суффикс
+class UserNotFoundException extends \Exception
+{
+}
+
+class InvalidCredentialsException extends \Exception
+{
+}
+
+class ValidationException extends \Exception
+{
+}
+
+// НЕПРАВИЛЬНО
+class UserNotFound extends \Exception
+{
+}
+```
+
+## Yii2-специфичные соглашения
+
+### Модели ActiveRecord
+```php
+<?php
+// ПРАВИЛЬНО - единственное число, PascalCase
+class User extends ActiveRecord
+{
+ public static function tableName(): string
+ {
+ return '{{%user}}'; // таблица users или user
+ }
+}
+
+class OrderItem extends ActiveRecord
+{
+ public static function tableName(): string
+ {
+ return '{{%order_item}}'; // таблица order_items
+ }
+}
+```
+
+### Контроллеры
+```php
+<?php
+// ПРАВИЛЬНО - *Controller суффикс
+class UserController extends Controller
+{
+ // Actions - actionХХХ
+ public function actionIndex(): string
+ {
+ // ...
+ }
+
+ public function actionView(int $id): string
+ {
+ // ...
+ }
+
+ public function actionCreate(): string|Response
+ {
+ // ...
+ }
+}
+```
+
+### Виджеты
+```php
+<?php
+// ПРАВИЛЬНО - описательные имена с Widget суффиксом (опционально)
+class UserCard extends Widget
+{
+}
+
+class DataTable extends Widget
+{
+}
+
+class LoginForm extends Widget
+{
+}
+```
+
+## Рекомендации для Claude Code
+
+1. **Консистентность** - придерживайтесь одного стиля во всём проекте
+2. **Описательность** - имена должны чётко описывать назначение
+3. **Длина** - избегайте слишком коротких (`$a`, `$x`) и слишком длинных имён
+4. **Контекст** - учитывайте контекст использования при выборе имени
+5. **PSR-12** - следуйте PSR-12 для новых проектов
+6. **Yii2 стиль** - в Yii2 проектах следуйте конвенциям фреймворка
--- /dev/null
+# PHP Methods - Методы и функции
+
+## Определение методов
+
+### Сигнатура метода
+```php
+<?php
+declare(strict_types=1);
+
+class UserService
+{
+ // ПРАВИЛЬНО - полная типизация
+ public function createUser(string $email, string $password): User
+ {
+ // код
+ }
+
+ // С nullable типами
+ public function findUser(int $id): ?User
+ {
+ return User::findOne($id);
+ }
+
+ // С union types (PHP 8.0+)
+ public function process(int|string $value): int|float
+ {
+ // код
+ }
+
+ // Void для методов без возврата
+ public function delete(User $user): void
+ {
+ $user->delete();
+ }
+}
+```
+
+### Скобки фигурные - стиль размещения
+```php
+<?php
+class Calculator
+{
+ // ПРАВИЛЬНО (PSR-12) - открывающая скобка на новой строке для методов
+ public function add(int $a, int $b): int
+ {
+ return $a + $b;
+ }
+
+ // Многострочная сигнатура
+ public function complexOperation(
+ int $firstParameter,
+ string $secondParameter,
+ ?array $optionalParameter = null,
+ ): array {
+ // код
+ }
+}
+```
+
+### Длина методов
+```php
+<?php
+class OrderProcessor
+{
+ // ПРАВИЛЬНО - короткие методы (10-20 строк максимум)
+ public function process(Order $order): Result
+ {
+ $this->validate($order);
+ $this->calculateTotals($order);
+ $this->applyDiscounts($order);
+ $this->reserveInventory($order);
+
+ return $this->save($order);
+ }
+
+ // НЕПРАВИЛЬНО - слишком длинный метод
+ public function processOrder(Order $order): Result
+ {
+ // 100+ строк кода
+ // Разбейте на несколько методов
+ }
+}
+```
+
+### Однострочные методы
+```php
+<?php
+class User
+{
+ // ПРАВИЛЬНО - простые геттеры
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function isActive(): bool
+ {
+ return $this->status === self::STATUS_ACTIVE;
+ }
+
+ // НЕПРАВИЛЬНО - несколько выражений в одной строке
+ public function process(): void { $this->validate(); $this->save(); }
+}
+```
+
+## Параметры методов
+
+### Порядок параметров
+```php
+<?php
+class Mailer
+{
+ // ПРАВИЛЬНО - обязательные первыми, опциональные в конце
+ public function send(
+ string $to,
+ string $subject,
+ string $body,
+ ?string $from = null,
+ array $attachments = [],
+ ): bool {
+ // код
+ }
+
+ // НЕПРАВИЛЬНО - опциональные перед обязательными
+ public function send(
+ ?string $from = null, // опциональный
+ string $to, // обязательный - ошибка!
+ string $subject,
+ ): bool {
+ // код
+ }
+}
+```
+
+### Максимальное количество параметров
+```php
+<?php
+class UserCreator
+{
+ // ПРАВИЛЬНО - до 3-4 параметров
+ public function create(string $email, string $password, string $name): User
+ {
+ // код
+ }
+
+ // ПРАВИЛЬНО - DTO/Value Object для большего числа
+ public function createFromRequest(CreateUserRequest $request): User
+ {
+ // код
+ }
+
+ // НЕПРАВИЛЬНО - слишком много параметров
+ public function create(
+ string $email,
+ string $password,
+ string $firstName,
+ string $lastName,
+ ?string $phone,
+ ?string $address,
+ bool $isActive,
+ int $roleId,
+ ): User {
+ // код
+ }
+}
+
+// DTO для группировки параметров
+readonly class CreateUserRequest
+{
+ public function __construct(
+ public string $email,
+ public string $password,
+ public string $firstName,
+ public string $lastName,
+ public ?string $phone = null,
+ public ?string $address = null,
+ public bool $isActive = true,
+ public int $roleId = 1,
+ ) {}
+}
+```
+
+### Именованные аргументы (PHP 8.0+)
+```php
+<?php
+class ReportGenerator
+{
+ public function generate(
+ string $type,
+ \DateTimeInterface $startDate,
+ \DateTimeInterface $endDate,
+ string $format = 'pdf',
+ bool $includeCharts = true,
+ ?int $limit = null,
+ ): Report {
+ // код
+ }
+}
+
+// Вызов с именованными аргументами - ясность
+$report = $generator->generate(
+ type: 'sales',
+ startDate: new DateTime('2024-01-01'),
+ endDate: new DateTime('2024-12-31'),
+ format: 'excel',
+ includeCharts: false,
+);
+```
+
+### Variadic параметры
+```php
+<?php
+class Calculator
+{
+ // ПРАВИЛЬНО - splat оператор для произвольного числа аргументов
+ public function sum(int|float ...$numbers): float
+ {
+ return array_sum($numbers);
+ }
+
+ public function formatMessage(string $template, mixed ...$args): string
+ {
+ return sprintf($template, ...$args);
+ }
+}
+
+// Использование
+$calc = new Calculator();
+$total = $calc->sum(1, 2, 3, 4, 5);
+
+// Распаковка массива
+$numbers = [1, 2, 3];
+$total = $calc->sum(...$numbers);
+```
+
+## Возвращаемые значения
+
+### Explicit return
+```php
+<?php
+class Validator
+{
+ // ПРАВИЛЬНО - явный return
+ public function validate(array $data): bool
+ {
+ if (empty($data)) {
+ return false;
+ }
+
+ foreach ($data as $item) {
+ if (!$this->isValid($item)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // Для void методов - return необязателен
+ public function log(string $message): void
+ {
+ // код
+ // return не нужен
+ }
+}
+```
+
+### Early return (Guard Clauses)
+```php
+<?php
+class OrderService
+{
+ // ПРАВИЛЬНО - ранний выход
+ public function process(Order $order): Result
+ {
+ if ($order === null) {
+ return Result::error('Order is null');
+ }
+
+ if (!$order->isValid()) {
+ return Result::error('Order is invalid');
+ }
+
+ if ($order->isEmpty()) {
+ return Result::error('Order is empty');
+ }
+
+ // Основная логика
+ return $this->doProcess($order);
+ }
+
+ // НЕПРАВИЛЬНО - глубокая вложенность
+ public function process(Order $order): Result
+ {
+ if ($order !== null) {
+ if ($order->isValid()) {
+ if (!$order->isEmpty()) {
+ // Основная логика
+ return $this->doProcess($order);
+ } else {
+ return Result::error('Order is empty');
+ }
+ } else {
+ return Result::error('Order is invalid');
+ }
+ } else {
+ return Result::error('Order is null');
+ }
+ }
+}
+```
+
+### Возврат нескольких значений
+```php
+<?php
+class Parser
+{
+ // ПРАВИЛЬНО - кортеж через массив с деструктуризацией
+ public function parse(string $input): array
+ {
+ $success = true;
+ $data = [];
+ $errors = [];
+
+ // парсинг...
+
+ return [$success, $data, $errors];
+ }
+
+ // Лучше - использовать Result/DTO объект
+ public function parse(string $input): ParseResult
+ {
+ // парсинг...
+ return new ParseResult(
+ success: true,
+ data: $data,
+ errors: [],
+ );
+ }
+}
+
+// Использование деструктуризации
+[$success, $data, $errors] = $parser->parse($input);
+
+// Или с DTO
+$result = $parser->parse($input);
+if ($result->success) {
+ // работа с $result->data
+}
+```
+
+## Статические методы
+
+### Когда использовать
+```php
+<?php
+class StringHelper
+{
+ // ПРАВИЛЬНО - чистые функции без состояния
+ public static function truncate(string $text, int $length): string
+ {
+ if (mb_strlen($text) <= $length) {
+ return $text;
+ }
+
+ return mb_substr($text, 0, $length) . '...';
+ }
+
+ // Фабричные методы
+ public static function fromArray(array $data): self
+ {
+ // создание экземпляра из массива
+ }
+}
+
+class User
+{
+ // ПРАВИЛЬНО - альтернативные конструкторы
+ public static function createGuest(): self
+ {
+ $user = new self();
+ $user->role = 'guest';
+ return $user;
+ }
+
+ public static function createFromOAuth(array $oauthData): self
+ {
+ // создание из OAuth данных
+ }
+}
+```
+
+### Когда избегать
+```php
+<?php
+// НЕПРАВИЛЬНО - статические методы с зависимостями
+class OrderService
+{
+ // Сложно тестировать и мокать
+ public static function process(Order $order): Result
+ {
+ // Использует глобальное состояние или синглтоны
+ $db = Database::getInstance();
+ $mailer = Mailer::getInstance();
+ // ...
+ }
+}
+
+// ПРАВИЛЬНО - инъекция зависимостей
+class OrderService
+{
+ public function __construct(
+ private Database $db,
+ private Mailer $mailer,
+ ) {}
+
+ public function process(Order $order): Result
+ {
+ // использует $this->db и $this->mailer
+ }
+}
+```
+
+## Модификаторы доступа
+
+### Порядок в объявлении
+```php
+<?php
+class Example
+{
+ // ПРАВИЛЬНО порядок модификаторов (PSR-12):
+ // [visibility] [static] [abstract/final] function
+
+ public function publicMethod(): void {}
+
+ public static function staticMethod(): void {}
+
+ final public function finalMethod(): void {}
+
+ abstract public function abstractMethod(): void;
+
+ // PHP 8.1+ readonly
+ public readonly string $property;
+}
+```
+
+### Visibility
+```php
+<?php
+class User
+{
+ // Public - API класса
+ public function getFullName(): string
+ {
+ return $this->formatName($this->firstName, $this->lastName);
+ }
+
+ // Protected - для наследников
+ protected function beforeSave(): void
+ {
+ $this->updatedAt = time();
+ }
+
+ // Private - внутренняя реализация
+ private function formatName(string $first, string $last): string
+ {
+ return trim("$first $last");
+ }
+}
+```
+
+## Специальные методы
+
+### Конструктор
+```php
+<?php
+class User
+{
+ private string $email;
+ private string $name;
+ private array $roles;
+
+ // Классический конструктор
+ public function __construct(string $email, string $name, array $roles = [])
+ {
+ $this->email = $email;
+ $this->name = $name;
+ $this->roles = $roles;
+ }
+}
+
+// PHP 8.0+ - Constructor Property Promotion
+class User
+{
+ public function __construct(
+ private string $email,
+ private string $name,
+ private array $roles = [],
+ ) {}
+}
+
+// PHP 8.1+ - readonly properties
+class User
+{
+ public function __construct(
+ public readonly string $email,
+ public readonly string $name,
+ public readonly array $roles = [],
+ ) {}
+}
+```
+
+### Magic методы
+```php
+<?php
+class Config
+{
+ private array $data = [];
+
+ // __get - доступ к несуществующим свойствам
+ public function __get(string $name): mixed
+ {
+ return $this->data[$name] ?? null;
+ }
+
+ // __set - установка несуществующих свойств
+ public function __set(string $name, mixed $value): void
+ {
+ $this->data[$name] = $value;
+ }
+
+ // __isset - проверка isset() для несуществующих свойств
+ public function __isset(string $name): bool
+ {
+ return isset($this->data[$name]);
+ }
+
+ // __call - вызов несуществующих методов
+ public function __call(string $method, array $args): mixed
+ {
+ if (str_starts_with($method, 'get')) {
+ $property = lcfirst(substr($method, 3));
+ return $this->data[$property] ?? null;
+ }
+
+ throw new \BadMethodCallException("Method $method does not exist");
+ }
+
+ // __toString - преобразование в строку
+ public function __toString(): string
+ {
+ return json_encode($this->data);
+ }
+}
+```
+
+## Документирование методов
+
+### PHPDoc
+```php
+<?php
+class UserRepository
+{
+ /**
+ * Находит пользователя по ID.
+ *
+ * Выполняет поиск активного пользователя в базе данных.
+ * Если пользователь не найден или деактивирован, возвращает null.
+ *
+ * @param int $id Идентификатор пользователя
+ * @return User|null Найденный пользователь или null
+ * @throws DatabaseException При ошибке подключения к БД
+ *
+ * @example
+ * $user = $repository->findById(123);
+ * if ($user !== null) {
+ * echo $user->getName();
+ * }
+ */
+ public function findById(int $id): ?User
+ {
+ // код
+ }
+
+ /**
+ * Находит пользователей по критериям.
+ *
+ * @param array<string, mixed> $criteria Критерии поиска
+ * @param array{
+ * limit?: int,
+ * offset?: int,
+ * orderBy?: string
+ * } $options Опции запроса
+ * @return User[] Массив найденных пользователей
+ */
+ public function findByCriteria(array $criteria, array $options = []): array
+ {
+ // код
+ }
+}
+```
+
+## Рекомендации для Claude Code
+
+1. **Короткие методы** - каждый метод должен делать одну вещь (SRP)
+2. **Типизация** - всегда указывайте типы параметров и возвращаемых значений
+3. **Guard Clauses** - используйте ранний выход для уменьшения вложенности
+4. **Именование** - имя метода должно ясно говорить, что он делает
+5. **Документирование** - добавляйте PHPDoc для публичных методов API
+6. **Тестируемость** - избегайте static с зависимостями, предпочитайте DI
--- /dev/null
+# PHP Classes - Классы, интерфейсы и трейты
+
+## Структура класса
+
+### Стандартный порядок элементов
+```php
+<?php
+
+declare(strict_types=1);
+
+namespace App\Models;
+
+use App\Contracts\Authenticatable;
+use App\Traits\HasTimestamps;
+use yii\db\ActiveRecord;
+
+/**
+ * Класс пользователя системы.
+ *
+ * @property int $id
+ * @property string $email
+ * @property string $name
+ */
+class User extends ActiveRecord implements Authenticatable
+{
+ // 1. Трейты
+ use HasTimestamps;
+
+ // 2. Константы
+ public const STATUS_ACTIVE = 1;
+ public const STATUS_INACTIVE = 0;
+ public const MAX_NAME_LENGTH = 100;
+
+ // 3. Статические свойства
+ private static array $instances = [];
+
+ // 4. Свойства экземпляра (public, protected, private)
+ public int $id;
+ public string $email;
+ protected string $passwordHash;
+ private array $cachedPermissions = [];
+
+ // 5. Конструктор
+ public function __construct(
+ string $email,
+ string $name,
+ ) {
+ $this->email = $email;
+ $this->name = $name;
+ }
+
+ // 6. Статические методы (фабричные)
+ public static function createGuest(): self
+ {
+ return new self('guest@example.com', 'Guest');
+ }
+
+ // 7. Публичные методы
+ public function getFullName(): string
+ {
+ return $this->name;
+ }
+
+ public function activate(): void
+ {
+ $this->status = self::STATUS_ACTIVE;
+ }
+
+ // 8. Protected методы
+ protected function beforeSave(): bool
+ {
+ $this->updatedAt = time();
+ return parent::beforeSave();
+ }
+
+ // 9. Private методы
+ private function hashPassword(string $password): string
+ {
+ return password_hash($password, PASSWORD_DEFAULT);
+ }
+}
+```
+
+## Определение классов
+
+### Базовый класс
+```php
+<?php
+
+declare(strict_types=1);
+
+namespace App\Services;
+
+// ПРАВИЛЬНО - скобка на новой строке
+class UserService
+{
+ public function __construct(
+ private readonly UserRepository $repository,
+ private readonly Mailer $mailer,
+ ) {}
+
+ public function register(array $data): User
+ {
+ // код
+ }
+}
+```
+
+### Финальные классы
+```php
+<?php
+// ПРАВИЛЬНО - final для классов, которые не предназначены для наследования
+final class PaymentProcessor
+{
+ public function process(Payment $payment): Result
+ {
+ // код
+ }
+}
+
+// Используйте final по умолчанию, убирайте когда нужно наследование
+```
+
+### Абстрактные классы
+```php
+<?php
+
+abstract class BaseRepository
+{
+ abstract public function find(int $id): ?object;
+ abstract public function save(object $entity): void;
+ abstract public function delete(object $entity): void;
+
+ // Общая реализация
+ protected function buildQuery(): QueryBuilder
+ {
+ return new QueryBuilder($this->getTableName());
+ }
+
+ abstract protected function getTableName(): string;
+}
+
+class UserRepository extends BaseRepository
+{
+ public function find(int $id): ?User
+ {
+ return User::findOne($id);
+ }
+
+ public function save(object $entity): void
+ {
+ $entity->save();
+ }
+
+ public function delete(object $entity): void
+ {
+ $entity->delete();
+ }
+
+ protected function getTableName(): string
+ {
+ return 'users';
+ }
+}
+```
+
+### Readonly классы (PHP 8.2+)
+```php
+<?php
+// Все свойства автоматически readonly
+readonly class UserDto
+{
+ public function __construct(
+ public int $id,
+ public string $email,
+ public string $name,
+ ) {}
+}
+
+// Эквивалентно:
+class UserDto
+{
+ public function __construct(
+ public readonly int $id,
+ public readonly string $email,
+ public readonly string $name,
+ ) {}
+}
+```
+
+## Интерфейсы
+
+### Определение интерфейсов
+```php
+<?php
+
+namespace App\Contracts;
+
+// ПРАВИЛЬНО - маленькие, сфокусированные интерфейсы (ISP)
+interface Authenticatable
+{
+ public function getAuthIdentifier(): int|string;
+ public function getAuthPassword(): string;
+}
+
+interface Authorizable
+{
+ public function hasPermission(string $permission): bool;
+ public function hasRole(string $role): bool;
+}
+
+interface HasTimestamps
+{
+ public function getCreatedAt(): \DateTimeInterface;
+ public function getUpdatedAt(): \DateTimeInterface;
+}
+
+// НЕПРАВИЛЬНО - слишком большой интерфейс
+interface UserInterface
+{
+ public function getId(): int;
+ public function getEmail(): string;
+ public function getName(): string;
+ public function getPassword(): string;
+ public function getCreatedAt(): \DateTimeInterface;
+ public function hasPermission(string $permission): bool;
+ // ... ещё 20 методов
+}
+```
+
+### Реализация интерфейсов
+```php
+<?php
+
+class User implements Authenticatable, Authorizable
+{
+ public function getAuthIdentifier(): int|string
+ {
+ return $this->id;
+ }
+
+ public function getAuthPassword(): string
+ {
+ return $this->passwordHash;
+ }
+
+ public function hasPermission(string $permission): bool
+ {
+ return in_array($permission, $this->permissions, true);
+ }
+
+ public function hasRole(string $role): bool
+ {
+ return $this->role === $role;
+ }
+}
+```
+
+### Интерфейсы с константами
+```php
+<?php
+
+interface StatusCodes
+{
+ public const SUCCESS = 200;
+ public const NOT_FOUND = 404;
+ public const SERVER_ERROR = 500;
+}
+
+// PHP 8.1+ - interface с default реализацией через trait
+interface Loggable
+{
+ public function log(string $message): void;
+}
+
+trait LoggableTrait
+{
+ public function log(string $message): void
+ {
+ error_log(static::class . ': ' . $message);
+ }
+}
+
+class Service implements Loggable
+{
+ use LoggableTrait;
+}
+```
+
+## Трейты
+
+### Определение трейтов
+```php
+<?php
+
+trait HasTimestamps
+{
+ public ?int $createdAt = null;
+ public ?int $updatedAt = null;
+
+ public function touch(): void
+ {
+ $this->updatedAt = time();
+ }
+
+ protected function initTimestamps(): void
+ {
+ if ($this->createdAt === null) {
+ $this->createdAt = time();
+ }
+ $this->updatedAt = time();
+ }
+}
+
+trait SoftDeletes
+{
+ public ?int $deletedAt = null;
+
+ public function delete(): void
+ {
+ $this->deletedAt = time();
+ $this->save();
+ }
+
+ public function restore(): void
+ {
+ $this->deletedAt = null;
+ $this->save();
+ }
+
+ public function isDeleted(): bool
+ {
+ return $this->deletedAt !== null;
+ }
+
+ abstract public function save(): bool;
+}
+```
+
+### Использование трейтов
+```php
+<?php
+
+class Article extends ActiveRecord
+{
+ use HasTimestamps;
+ use SoftDeletes;
+
+ // Разрешение конфликтов
+ // use TraitA, TraitB {
+ // TraitA::method insteadof TraitB;
+ // TraitB::method as aliasMethod;
+ // }
+}
+```
+
+### Трейты с абстрактными методами
+```php
+<?php
+
+trait Searchable
+{
+ abstract public function getSearchableAttributes(): array;
+
+ public function toSearchArray(): array
+ {
+ $data = [];
+ foreach ($this->getSearchableAttributes() as $attribute) {
+ $data[$attribute] = $this->$attribute;
+ }
+ return $data;
+ }
+}
+
+class Product extends ActiveRecord
+{
+ use Searchable;
+
+ public function getSearchableAttributes(): array
+ {
+ return ['name', 'description', 'sku'];
+ }
+}
+```
+
+## Наследование vs Композиция
+
+### Предпочитайте композицию
+```php
+<?php
+// НЕПРАВИЛЬНО - глубокая иерархия наследования
+class Animal {}
+class Mammal extends Animal {}
+class Dog extends Mammal {}
+class GermanShepherd extends Dog {}
+
+// ПРАВИЛЬНО - композиция через интерфейсы и DI
+interface CanBark
+{
+ public function bark(): string;
+}
+
+interface CanRun
+{
+ public function run(): void;
+}
+
+class Dog implements CanBark, CanRun
+{
+ public function __construct(
+ private BarkBehavior $barkBehavior,
+ private RunBehavior $runBehavior,
+ ) {}
+
+ public function bark(): string
+ {
+ return $this->barkBehavior->execute();
+ }
+
+ public function run(): void
+ {
+ $this->runBehavior->execute();
+ }
+}
+```
+
+## Свойства класса
+
+### Типизированные свойства
+```php
+<?php
+
+class User
+{
+ // Типизированные свойства (PHP 7.4+)
+ public int $id;
+ public string $email;
+ public ?string $phone = null;
+
+ // Readonly (PHP 8.1+)
+ public readonly string $createdAt;
+
+ // Promoted properties в конструкторе (PHP 8.0+)
+ public function __construct(
+ public int $id,
+ public string $email,
+ private string $password,
+ public readonly string $createdAt = '',
+ ) {}
+}
+```
+
+### Visibility
+```php
+<?php
+
+class Account
+{
+ // Public - доступно везде
+ public float $balance = 0.0;
+
+ // Protected - доступно в классе и наследниках
+ protected int $accountNumber;
+
+ // Private - доступно только в этом классе
+ private string $secretKey;
+
+ // PHP 8.1+ asymmetric visibility (PER)
+ // public private(set) string $name; // читать публично, писать приватно
+}
+```
+
+## Константы класса
+
+### Определение констант
+```php
+<?php
+
+class Order
+{
+ // Публичные константы
+ public const STATUS_PENDING = 'pending';
+ public const STATUS_PROCESSING = 'processing';
+ public const STATUS_COMPLETED = 'completed';
+ public const STATUS_CANCELLED = 'cancelled';
+
+ // Группировка в массив
+ public const STATUSES = [
+ self::STATUS_PENDING,
+ self::STATUS_PROCESSING,
+ self::STATUS_COMPLETED,
+ self::STATUS_CANCELLED,
+ ];
+
+ // Private константы
+ private const INTERNAL_CODE = 'xyz';
+
+ // Final константы (PHP 8.1+)
+ final public const VERSION = '1.0';
+}
+```
+
+### Enum вместо констант (PHP 8.1+)
+```php
+<?php
+
+// ПРАВИЛЬНО - используйте Enum для связанных значений
+enum OrderStatus: string
+{
+ case Pending = 'pending';
+ case Processing = 'processing';
+ case Completed = 'completed';
+ case Cancelled = 'cancelled';
+
+ public function label(): string
+ {
+ return match ($this) {
+ self::Pending => 'В ожидании',
+ self::Processing => 'В обработке',
+ self::Completed => 'Завершён',
+ self::Cancelled => 'Отменён',
+ };
+ }
+
+ public function canTransitionTo(self $newStatus): bool
+ {
+ return match ($this) {
+ self::Pending => in_array($newStatus, [self::Processing, self::Cancelled]),
+ self::Processing => in_array($newStatus, [self::Completed, self::Cancelled]),
+ self::Completed, self::Cancelled => false,
+ };
+ }
+}
+
+// Использование
+$status = OrderStatus::Pending;
+echo $status->value; // 'pending'
+echo $status->label(); // 'В ожидании'
+
+if ($status->canTransitionTo(OrderStatus::Processing)) {
+ // можно перевести в обработку
+}
+```
+
+## Инициализация
+
+### Конструкторы
+```php
+<?php
+
+class User
+{
+ // PHP 8.0+ Constructor Property Promotion
+ public function __construct(
+ private string $email,
+ private string $name,
+ private array $roles = [],
+ private ?\DateTimeInterface $createdAt = null,
+ ) {
+ // Дополнительная инициализация
+ $this->createdAt ??= new \DateTimeImmutable();
+ }
+}
+
+// Фабричные методы для альтернативной инициализации
+class User
+{
+ private function __construct(
+ private string $email,
+ private string $name,
+ ) {}
+
+ public static function create(string $email, string $name): self
+ {
+ return new self($email, $name);
+ }
+
+ public static function createFromArray(array $data): self
+ {
+ return new self(
+ $data['email'] ?? throw new \InvalidArgumentException('Email required'),
+ $data['name'] ?? throw new \InvalidArgumentException('Name required'),
+ );
+ }
+
+ public static function createGuest(): self
+ {
+ return new self('guest@example.com', 'Guest');
+ }
+}
+```
+
+## SOLID принципы
+
+### Single Responsibility (SRP)
+```php
+<?php
+// ПРАВИЛЬНО - один класс = одна ответственность
+class UserValidator
+{
+ public function validate(array $data): array
+ {
+ // только валидация
+ }
+}
+
+class UserRepository
+{
+ public function save(User $user): void
+ {
+ // только сохранение
+ }
+}
+
+class UserMailer
+{
+ public function sendWelcome(User $user): void
+ {
+ // только отправка email
+ }
+}
+
+// НЕПРАВИЛЬНО - God Object
+class User
+{
+ public function validate(): bool { /* ... */ }
+ public function save(): void { /* ... */ }
+ public function sendEmail(): void { /* ... */ }
+ public function generateReport(): string { /* ... */ }
+ public function exportToCsv(): string { /* ... */ }
+}
+```
+
+### Open/Closed Principle (OCP)
+```php
+<?php
+// ПРАВИЛЬНО - открыт для расширения, закрыт для модификации
+interface PaymentGateway
+{
+ public function charge(float $amount): bool;
+}
+
+class StripeGateway implements PaymentGateway
+{
+ public function charge(float $amount): bool
+ {
+ // Stripe implementation
+ }
+}
+
+class PayPalGateway implements PaymentGateway
+{
+ public function charge(float $amount): bool
+ {
+ // PayPal implementation
+ }
+}
+
+class PaymentProcessor
+{
+ public function process(PaymentGateway $gateway, float $amount): bool
+ {
+ return $gateway->charge($amount);
+ }
+}
+```
+
+### Dependency Inversion (DIP)
+```php
+<?php
+// ПРАВИЛЬНО - зависимость от абстракций
+interface LoggerInterface
+{
+ public function log(string $message): void;
+}
+
+class UserService
+{
+ public function __construct(
+ private LoggerInterface $logger,
+ ) {}
+
+ public function createUser(array $data): User
+ {
+ $this->logger->log('Creating user...');
+ // создание пользователя
+ }
+}
+
+// Можно использовать любую реализацию
+$service = new UserService(new FileLogger());
+$service = new UserService(new DatabaseLogger());
+```
+
+## Рекомендации для Claude Code
+
+1. **Маленькие классы** - один класс = одна ответственность (SRP)
+2. **Final по умолчанию** - делайте классы final, пока не нужно наследование
+3. **Композиция > наследование** - используйте интерфейсы и DI
+4. **Типизация** - всегда указывайте типы свойств и методов
+5. **Readonly** - используйте readonly для иммутабельных данных
+6. **Enum** - используйте Enum вместо констант для связанных значений
--- /dev/null
+# PHP Collections - Работа с массивами
+
+## Создание массивов
+
+### Литералы vs конструкторы
+```php
+<?php
+// ПРАВИЛЬНО - короткий синтаксис []
+$array = [];
+$numbers = [1, 2, 3, 4, 5];
+
+// ПРАВИЛЬНО - ассоциативный массив
+$user = [
+ 'name' => 'John',
+ 'email' => 'john@example.com',
+ 'age' => 30,
+];
+
+// НЕПРАВИЛЬНО - устаревший синтаксис array()
+$array = array();
+$numbers = array(1, 2, 3);
+```
+
+### Многомерные массивы
+```php
+<?php
+// ПРАВИЛЬНО - отформатированная структура
+$config = [
+ 'database' => [
+ 'host' => 'localhost',
+ 'port' => 5432,
+ 'name' => 'myapp',
+ ],
+ 'cache' => [
+ 'driver' => 'redis',
+ 'ttl' => 3600,
+ ],
+];
+
+// Доступ к вложенным элементам
+$host = $config['database']['host'];
+```
+
+### Trailing comma
+```php
+<?php
+// ПРАВИЛЬНО - запятая после последнего элемента
+$array = [
+ 'first',
+ 'second',
+ 'third', // trailing comma - облегчает diff в git
+];
+
+$users = [
+ ['name' => 'John', 'age' => 30],
+ ['name' => 'Jane', 'age' => 25],
+];
+```
+
+## Доступ к элементам
+
+### Базовый доступ
+```php
+<?php
+$array = ['a', 'b', 'c', 'd', 'e'];
+
+// Доступ по индексу
+$first = $array[0];
+$last = $array[count($array) - 1];
+
+// PHP 7.4+ - отрицательные индексы в array_slice
+$lastTwo = array_slice($array, -2);
+
+// ПРАВИЛЬНО - проверка существования ключа
+if (isset($array['key'])) {
+ $value = $array['key'];
+}
+
+// Или null coalescing
+$value = $array['key'] ?? 'default';
+
+// array_key_exists vs isset
+// isset возвращает false для null значений
+$data = ['key' => null];
+isset($data['key']); // false
+array_key_exists('key', $data); // true
+```
+
+### Безопасный доступ
+```php
+<?php
+// ПРАВИЛЬНО - null coalescing для отсутствующих ключей
+$name = $user['name'] ?? 'Anonymous';
+$city = $user['address']['city'] ?? 'Unknown';
+
+// ПРАВИЛЬНО - null coalescing assignment (PHP 7.4+)
+$user['role'] ??= 'guest';
+
+// НЕПРАВИЛЬНО - без проверки
+$name = $user['name']; // Warning если ключа нет
+```
+
+## Итерация
+
+### foreach
+```php
+<?php
+$users = [
+ ['name' => 'John', 'age' => 30],
+ ['name' => 'Jane', 'age' => 25],
+];
+
+// ПРАВИЛЬНО - foreach для перебора
+foreach ($users as $user) {
+ echo $user['name'];
+}
+
+// С ключом
+foreach ($users as $index => $user) {
+ echo "User $index: {$user['name']}";
+}
+
+// По ссылке (осторожно!)
+foreach ($users as &$user) {
+ $user['processed'] = true;
+}
+unset($user); // ВАЖНО - сбросить ссылку после цикла
+
+// НЕПРАВИЛЬНО - for для простого перебора (медленнее)
+for ($i = 0; $i < count($users); $i++) {
+ echo $users[$i]['name'];
+}
+```
+
+### Итерация с модификацией
+```php
+<?php
+// НЕПРАВИЛЬНО - модификация массива во время итерации
+foreach ($items as $key => $item) {
+ if ($item === null) {
+ unset($items[$key]); // Может привести к проблемам
+ }
+}
+
+// ПРАВИЛЬНО - используйте array_filter
+$items = array_filter($items, fn($item) => $item !== null);
+
+// Или итерируйте по копии
+foreach ($items as $key => $item) {
+ if ($item === null) {
+ unset($items[$key]);
+ }
+}
+$items = array_values($items); // Переиндексация если нужно
+```
+
+## Трансформация массивов
+
+### array_map
+```php
+<?php
+$numbers = [1, 2, 3, 4, 5];
+
+// ПРАВИЛЬНО - функциональный стиль
+$doubled = array_map(fn($n) => $n * 2, $numbers);
+
+// Несколько массивов
+$a = [1, 2, 3];
+$b = [10, 20, 30];
+$sum = array_map(fn($x, $y) => $x + $y, $a, $b);
+
+// Трансформация объектов
+$users = [new User('John'), new User('Jane')];
+$names = array_map(fn($user) => $user->getName(), $users);
+
+// С сохранением ключей
+$prices = ['apple' => 1.0, 'banana' => 0.5];
+$discounted = array_map(fn($price) => $price * 0.9, $prices);
+```
+
+### array_filter
+```php
+<?php
+$numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+
+// Фильтрация с callback
+$even = array_filter($numbers, fn($n) => $n % 2 === 0);
+
+// Без callback - удаляет falsy значения
+$values = [0, 1, '', 'hello', null, false];
+$truthy = array_filter($values); // [1 => 1, 3 => 'hello']
+
+// С флагами
+$data = ['a' => 1, 'b' => 2, 'c' => 3];
+
+// ARRAY_FILTER_USE_KEY - только ключи
+$filtered = array_filter($data, fn($key) => $key !== 'b', ARRAY_FILTER_USE_KEY);
+
+// ARRAY_FILTER_USE_BOTH - ключи и значения
+$filtered = array_filter(
+ $data,
+ fn($value, $key) => $key !== 'a' && $value > 1,
+ ARRAY_FILTER_USE_BOTH
+);
+```
+
+### array_reduce
+```php
+<?php
+$numbers = [1, 2, 3, 4, 5];
+
+// Сумма
+$sum = array_reduce($numbers, fn($carry, $item) => $carry + $item, 0);
+
+// Произведение
+$product = array_reduce($numbers, fn($carry, $item) => $carry * $item, 1);
+
+// Построение строки
+$words = ['Hello', 'World'];
+$sentence = array_reduce(
+ $words,
+ fn($carry, $word) => $carry . ($carry ? ' ' : '') . $word,
+ ''
+);
+
+// Группировка
+$users = [
+ ['name' => 'John', 'role' => 'admin'],
+ ['name' => 'Jane', 'role' => 'user'],
+ ['name' => 'Bob', 'role' => 'admin'],
+];
+
+$grouped = array_reduce($users, function ($carry, $user) {
+ $carry[$user['role']][] = $user['name'];
+ return $carry;
+}, []);
+// ['admin' => ['John', 'Bob'], 'user' => ['Jane']]
+```
+
+### array_column
+```php
+<?php
+$users = [
+ ['id' => 1, 'name' => 'John', 'email' => 'john@test.com'],
+ ['id' => 2, 'name' => 'Jane', 'email' => 'jane@test.com'],
+];
+
+// Извлечение колонки
+$names = array_column($users, 'name'); // ['John', 'Jane']
+
+// С ключом
+$emailById = array_column($users, 'email', 'id');
+// [1 => 'john@test.com', 2 => 'jane@test.com']
+
+// Вся строка с ключом
+$usersById = array_column($users, null, 'id');
+// [1 => ['id' => 1, ...], 2 => ['id' => 2, ...]]
+```
+
+## Поиск и проверка
+
+### Проверка наличия
+```php
+<?php
+$array = ['apple', 'banana', 'orange'];
+
+// in_array - проверка значения
+if (in_array('apple', $array, true)) { // true для строгого сравнения
+ echo 'Found';
+}
+
+// array_key_exists - проверка ключа
+$data = ['name' => 'John', 'age' => null];
+if (array_key_exists('age', $data)) {
+ echo 'Key exists';
+}
+
+// isset - проверка ключа и что значение не null
+if (isset($data['name'])) {
+ echo 'Set and not null';
+}
+```
+
+### Поиск
+```php
+<?php
+$fruits = ['apple', 'banana', 'orange', 'apple'];
+
+// array_search - найти индекс
+$index = array_search('banana', $fruits, true); // 1
+
+// array_keys - все ключи со значением
+$indices = array_keys($fruits, 'apple'); // [0, 3]
+
+// Поиск в массиве объектов
+$users = [new User(1, 'John'), new User(2, 'Jane')];
+
+$found = null;
+foreach ($users as $user) {
+ if ($user->getId() === 2) {
+ $found = $user;
+ break;
+ }
+}
+
+// Или с array_filter
+$matches = array_filter($users, fn($u) => $u->getId() === 2);
+$found = reset($matches) ?: null;
+```
+
+### Проверки массива
+```php
+<?php
+$array = [1, 2, 3, 4, 5];
+
+// Проверка пустоты
+if (empty($array)) {
+ echo 'Empty';
+}
+
+// count / sizeof
+$count = count($array);
+
+// Проверка всех элементов (нет встроенного all, используйте reduce)
+$allPositive = array_reduce(
+ $array,
+ fn($carry, $item) => $carry && $item > 0,
+ true
+);
+
+// Проверка хотя бы одного (нет встроенного any)
+$hasNegative = count(array_filter($array, fn($n) => $n < 0)) > 0;
+
+// Или через loop с break
+$hasNegative = false;
+foreach ($array as $item) {
+ if ($item < 0) {
+ $hasNegative = true;
+ break;
+ }
+}
+```
+
+## Сортировка
+
+### Базовая сортировка
+```php
+<?php
+$numbers = [3, 1, 4, 1, 5, 9, 2, 6];
+
+// sort - по значению, переиндексирует
+sort($numbers); // [1, 1, 2, 3, 4, 5, 6, 9]
+
+// rsort - в обратном порядке
+rsort($numbers);
+
+// asort - сохраняет ключи
+$prices = ['apple' => 1.5, 'banana' => 0.5, 'orange' => 2.0];
+asort($prices); // ['banana' => 0.5, 'apple' => 1.5, 'orange' => 2.0]
+
+// ksort - по ключам
+ksort($prices);
+
+// SORT_NATURAL - натуральная сортировка
+$files = ['file1.txt', 'file10.txt', 'file2.txt'];
+sort($files, SORT_NATURAL); // ['file1.txt', 'file2.txt', 'file10.txt']
+```
+
+### Пользовательская сортировка
+```php
+<?php
+$users = [
+ ['name' => 'John', 'age' => 30],
+ ['name' => 'Jane', 'age' => 25],
+ ['name' => 'Bob', 'age' => 35],
+];
+
+// usort - пользовательская функция сравнения
+usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
+
+// Spaceship operator <=> для сравнения
+// Возвращает -1, 0 или 1
+
+// Сортировка по нескольким полям
+usort($users, function ($a, $b) {
+ return $a['role'] <=> $b['role']
+ ?: $a['name'] <=> $b['name'];
+});
+
+// Сортировка объектов
+usort($users, fn($a, $b) => $a->getAge() <=> $b->getAge());
+```
+
+## Слияние и разделение
+
+### array_merge
+```php
+<?php
+$a = [1, 2, 3];
+$b = [4, 5, 6];
+
+// Слияние индексированных массивов
+$merged = array_merge($a, $b); // [1, 2, 3, 4, 5, 6]
+
+// Слияние ассоциативных массивов (поздние значения перезаписывают)
+$defaults = ['color' => 'red', 'size' => 'medium'];
+$options = ['size' => 'large'];
+$config = array_merge($defaults, $options);
+// ['color' => 'red', 'size' => 'large']
+
+// Spread operator (PHP 7.4+)
+$merged = [...$a, ...$b];
+$config = [...$defaults, ...$options];
+```
+
+### array_combine
+```php
+<?php
+$keys = ['name', 'email', 'age'];
+$values = ['John', 'john@test.com', 30];
+
+$user = array_combine($keys, $values);
+// ['name' => 'John', 'email' => 'john@test.com', 'age' => 30]
+```
+
+### array_chunk
+```php
+<?php
+$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+
+// Разбиение на части
+$chunks = array_chunk($items, 3);
+// [[1,2,3], [4,5,6], [7,8,9], [10]]
+
+// С сохранением ключей
+$chunks = array_chunk($items, 3, true);
+```
+
+### array_slice
+```php
+<?php
+$array = ['a', 'b', 'c', 'd', 'e'];
+
+// Срез от индекса
+$slice = array_slice($array, 2); // ['c', 'd', 'e']
+
+// Срез с ограничением
+$slice = array_slice($array, 1, 2); // ['b', 'c']
+
+// С отрицательным индексом
+$slice = array_slice($array, -2); // ['d', 'e']
+
+// С сохранением ключей
+$slice = array_slice($array, 1, 2, true); // [1 => 'b', 2 => 'c']
+```
+
+## Операции над множествами
+
+```php
+<?php
+$a = [1, 2, 3, 4, 5];
+$b = [4, 5, 6, 7, 8];
+
+// Разность
+$diff = array_diff($a, $b); // [1, 2, 3]
+
+// Пересечение
+$intersect = array_intersect($a, $b); // [4, 5]
+
+// Уникальные значения
+$unique = array_unique([1, 2, 2, 3, 3, 3]); // [1, 2, 3]
+
+// Разность с проверкой ключей
+$a = ['x' => 1, 'y' => 2];
+$b = ['x' => 1, 'z' => 3];
+$diff = array_diff_key($a, $b); // ['y' => 2]
+```
+
+## Полезные функции
+
+```php
+<?php
+// array_flip - меняет местами ключи и значения
+$flipped = array_flip(['a' => 0, 'b' => 1]); // [0 => 'a', 1 => 'b']
+
+// array_reverse - обратный порядок
+$reversed = array_reverse([1, 2, 3]); // [3, 2, 1]
+
+// array_fill - создание с заполнением
+$zeros = array_fill(0, 5, 0); // [0, 0, 0, 0, 0]
+
+// array_fill_keys - создание с ключами
+$defaults = array_fill_keys(['a', 'b', 'c'], null);
+// ['a' => null, 'b' => null, 'c' => null]
+
+// range - диапазон значений
+$numbers = range(1, 10); // [1, 2, 3, ..., 10]
+$letters = range('a', 'z');
+
+// array_sum, array_product
+$sum = array_sum([1, 2, 3, 4, 5]); // 15
+$product = array_product([1, 2, 3, 4]); // 24
+
+// compact / extract
+$name = 'John';
+$age = 30;
+$data = compact('name', 'age'); // ['name' => 'John', 'age' => 30]
+
+extract(['foo' => 'bar']); // создаёт $foo = 'bar' (осторожно!)
+```
+
+## Рекомендации для Claude Code
+
+1. **Короткий синтаксис** - всегда используйте `[]` вместо `array()`
+2. **Строгое сравнение** - используйте `true` в `in_array` и `array_search`
+3. **Функциональный стиль** - предпочитайте `array_map/filter/reduce` циклам
+4. **Null coalescing** - используйте `??` для безопасного доступа
+5. **Spread operator** - используйте `...` для слияния массивов
+6. **Unset после &** - всегда сбрасывайте ссылочную переменную после foreach
--- /dev/null
+# PHP Strings - Работа со строками
+
+## Кавычки
+
+### Выбор типа кавычек
+```php
+<?php
+// ПРАВИЛЬНО - одинарные для простых строк (быстрее)
+$name = 'John';
+$message = 'Hello, World!';
+$path = '/var/www/html';
+
+// ПРАВИЛЬНО - двойные для интерполяции
+$greeting = "Hello, $name!";
+$fullGreeting = "Hello, {$user->getName()}!";
+
+// ПРАВИЛЬНО - двойные для escape-последовательностей
+$newline = "First line\nSecond line";
+$tab = "Column1\tColumn2";
+
+// ПРАВИЛЬНО - одинарные для строк с двойными кавычками
+$html = '<div class="container">Content</div>';
+
+// ПРАВИЛЬНО - двойные для строк с одинарными
+$sql = "SELECT * FROM users WHERE name = 'John'";
+```
+
+### Escape-последовательности
+```php
+<?php
+// Работают только в двойных кавычках и heredoc
+$escaped = "Line1\nLine2\tTabbed";
+
+// Основные последовательности:
+// \n - новая строка
+// \r - возврат каретки
+// \t - табуляция
+// \\ - обратный слэш
+// \$ - знак доллара
+// \" - двойная кавычка
+
+// В одинарных кавычках работают только:
+// \\ - обратный слэш
+// \' - одинарная кавычка
+$literal = 'This is \n literally'; // не будет новой строки
+```
+
+## Интерполяция
+
+### Базовая интерполяция
+```php
+<?php
+$name = 'John';
+$age = 30;
+
+// ПРАВИЛЬНО - простые переменные
+$message = "Hello, $name!";
+
+// ПРАВИЛЬНО - сложные выражения в {}
+$message = "User {$user->name} has {$user->ordersCount} orders";
+$message = "Value: {$array['key']}";
+$message = "Result: {$object->getResult()}";
+
+// НЕПРАВИЛЬНО - конкатенация для простых случаев
+$message = 'Hello, ' . $name . '!'; // менее читаемо
+```
+
+### Фигурные скобки
+```php
+<?php
+// ПРАВИЛЬНО - всегда используйте {} для свойств и методов
+$message = "Name: {$user->name}";
+$message = "Count: {$items[0]}";
+
+// ПРАВИЛЬНО - для устранения неоднозначности
+$noun = 'apple';
+echo "I have 3 {$noun}s"; // I have 3 apples
+
+// НЕПРАВИЛЬНО - неоднозначность
+echo "I have 3 $nouns"; // ищет переменную $nouns
+```
+
+## Heredoc и Nowdoc
+
+### Heredoc (с интерполяцией)
+```php
+<?php
+$name = 'John';
+$items = ['apple', 'banana'];
+
+// ПРАВИЛЬНО - для многострочного текста с переменными
+$html = <<<HTML
+<div class="user-card">
+ <h1>Welcome, {$name}!</h1>
+ <p>You have {$items[0]} in your cart.</p>
+</div>
+HTML;
+
+// Идентификатор в двойных кавычках (необязательно)
+$sql = <<<"SQL"
+SELECT *
+FROM users
+WHERE name = '{$name}'
+ORDER BY created_at DESC
+SQL;
+```
+
+### Nowdoc (без интерполяции)
+```php
+<?php
+// ПРАВИЛЬНО - когда переменные не нужны
+$code = <<<'PHP'
+<?php
+$variable = 'not interpolated';
+echo $variable;
+PHP;
+
+// Полезно для примеров кода, регулярок и т.д.
+$pattern = <<<'REGEX'
+/^[\w.%+-]+@[\w.-]+\.[a-zA-Z]{2,}$/
+REGEX;
+```
+
+## Форматирование строк
+
+### sprintf/printf
+```php
+<?php
+// ПРАВИЛЬНО - sprintf для форматирования
+$message = sprintf('User %s has %d orders', $name, $count);
+
+// Спецификаторы формата:
+// %s - строка
+// %d - целое число
+// %f - число с плавающей точкой
+// %02d - целое с ведущими нулями (минимум 2 цифры)
+// %.2f - число с 2 знаками после запятой
+// %% - символ процента
+
+// Форматирование чисел
+$price = sprintf('%.2f', 19.5); // "19.50"
+$padded = sprintf('%05d', 42); // "00042"
+
+// Позиционные аргументы
+$message = sprintf('%2$s has %1$d items', $count, $name);
+
+// printf - вывод напрямую
+printf('Total: %.2f', $total);
+```
+
+### number_format
+```php
+<?php
+$number = 1234567.891;
+
+// Базовое форматирование
+echo number_format($number); // "1,234,568"
+echo number_format($number, 2); // "1,234,567.89"
+
+// Русская локаль
+echo number_format($number, 2, ',', ' '); // "1 234 567,89"
+```
+
+### Многострочные строки
+```php
+<?php
+// ПРАВИЛЬНО - heredoc для больших текстов
+$description = <<<TEXT
+This is a long description
+that spans multiple lines
+with proper formatting.
+TEXT;
+
+// ПРАВИЛЬНО - конкатенация для коротких строк
+$longString = 'This is a very long string that would be '
+ . 'too long to fit on a single line, so we split it '
+ . 'across multiple lines for readability.';
+
+// ПРАВИЛЬНО - implode для построения из частей
+$parts = [
+ 'SELECT * FROM users',
+ 'WHERE active = 1',
+ 'ORDER BY name',
+];
+$sql = implode(' ', $parts);
+```
+
+## Операции со строками
+
+### Конкатенация
+```php
+<?php
+// ПРАВИЛЬНО - для построения больших строк используйте массив + implode
+$parts = [];
+$parts[] = '<h1>Title</h1>';
+$parts[] = '<p>Content</p>';
+$html = implode("\n", $parts);
+
+// Или .= для накопления (медленнее для больших строк)
+$html = '';
+$html .= '<h1>Title</h1>';
+$html .= '<p>Content</p>';
+
+// Для единичной конкатенации
+$fullName = $firstName . ' ' . $lastName;
+```
+
+### Базовые операции
+```php
+<?php
+$str = 'Hello, World!';
+
+// Длина строки (для UTF-8 используйте mb_strlen)
+$length = strlen($str); // 13 (байты)
+$length = mb_strlen($str); // 13 (символы)
+
+// Изменение регистра
+$upper = strtoupper($str); // HELLO, WORLD!
+$lower = strtolower($str); // hello, world!
+$ucfirst = ucfirst($lower); // Hello, world!
+$ucwords = ucwords($lower); // Hello, World!
+
+// Для UTF-8
+$upper = mb_strtoupper($str);
+$lower = mb_strtolower($str);
+```
+
+### Обрезка и дополнение
+```php
+<?php
+$str = ' Hello, World! ';
+
+// Обрезка пробелов
+$trimmed = trim($str); // 'Hello, World!'
+$ltrimmed = ltrim($str); // 'Hello, World! '
+$rtrimmed = rtrim($str); // ' Hello, World!'
+
+// Обрезка конкретных символов
+$str = '###Hello###';
+$clean = trim($str, '#'); // 'Hello'
+
+// Дополнение
+$padded = str_pad('42', 5, '0', STR_PAD_LEFT); // '00042'
+$padded = str_pad('Hi', 10, '-', STR_PAD_BOTH); // '----Hi----'
+```
+
+### Подстроки
+```php
+<?php
+$str = 'Hello, World!';
+
+// Извлечение подстроки
+$sub = substr($str, 0, 5); // 'Hello'
+$sub = substr($str, -6); // 'World!'
+$sub = substr($str, 7, 5); // 'World'
+
+// Для UTF-8
+$sub = mb_substr($str, 0, 5);
+
+// Замена подстроки
+$new = substr_replace($str, 'PHP', 7, 5); // 'Hello, PHP!'
+```
+
+## Поиск и замена
+
+### Поиск
+```php
+<?php
+$str = 'Hello, World!';
+
+// Позиция подстроки
+$pos = strpos($str, 'World'); // 7
+$pos = stripos($str, 'world'); // 7 (без учёта регистра)
+
+// Проверка наличия (PHP 8.0+)
+if (str_contains($str, 'World')) {
+ echo 'Found!';
+}
+
+// Начинается/заканчивается (PHP 8.0+)
+if (str_starts_with($str, 'Hello')) {
+ echo 'Starts with Hello';
+}
+
+if (str_ends_with($str, '!')) {
+ echo 'Ends with !';
+}
+
+// До PHP 8.0
+$contains = strpos($str, 'World') !== false;
+$startsWith = strpos($str, 'Hello') === 0;
+$endsWith = substr($str, -1) === '!';
+```
+
+### Замена
+```php
+<?php
+$str = 'Hello, World!';
+
+// Простая замена
+$new = str_replace('World', 'PHP', $str); // 'Hello, PHP!'
+
+// Без учёта регистра
+$new = str_ireplace('world', 'PHP', $str);
+
+// Множественная замена
+$search = ['Hello', 'World'];
+$replace = ['Hi', 'PHP'];
+$new = str_replace($search, $replace, $str); // 'Hi, PHP!'
+
+// С подсчётом замен
+$count = 0;
+$new = str_replace('o', '0', $str, $count);
+echo "Replaced $count times";
+```
+
+### Регулярные выражения
+```php
+<?php
+$str = 'User: john@example.com';
+
+// Поиск
+if (preg_match('/[\w.]+@[\w.]+/', $str, $matches)) {
+ echo $matches[0]; // 'john@example.com'
+}
+
+// Все совпадения
+preg_match_all('/\d+/', 'a1b2c3', $matches);
+// $matches[0] = ['1', '2', '3']
+
+// Замена
+$new = preg_replace('/\d+/', '#', 'a1b2c3'); // 'a#b#c#'
+
+// С callback
+$new = preg_replace_callback(
+ '/\d+/',
+ fn($m) => $m[0] * 2,
+ 'a1b2c3'
+); // 'a2b4c6'
+```
+
+## Разбиение и объединение
+
+```php
+<?php
+// Разбиение
+$parts = explode(',', 'one,two,three'); // ['one', 'two', 'three']
+
+// С ограничением
+$parts = explode(',', 'one,two,three', 2); // ['one', 'two,three']
+
+// По символам
+$chars = str_split('Hello'); // ['H', 'e', 'l', 'l', 'o']
+$chunks = str_split('Hello', 2); // ['He', 'll', 'o']
+
+// Для UTF-8
+$chars = mb_str_split('Привет'); // ['П', 'р', 'и', 'в', 'е', 'т']
+
+// Объединение
+$str = implode(', ', ['one', 'two', 'three']); // 'one, two, three'
+$str = implode('', ['H', 'e', 'l', 'l', 'o']); // 'Hello'
+```
+
+## Сравнение строк
+
+```php
+<?php
+// Строгое сравнение
+if ($str1 === $str2) {
+ // точное совпадение
+}
+
+// strcmp - с учётом регистра
+$result = strcmp('abc', 'ABC'); // > 0
+
+// strcasecmp - без учёта регистра
+$result = strcasecmp('abc', 'ABC'); // 0
+
+// Натуральное сравнение
+$result = strnatcmp('file2', 'file10'); // < 0
+
+// similar_text - похожесть
+$percent = 0;
+similar_text('Hello', 'Hallo', $percent);
+echo "$percent%";
+
+// levenshtein - расстояние редактирования
+$distance = levenshtein('Hello', 'Hallo'); // 1
+```
+
+## Кодировка
+
+### UTF-8 операции
+```php
+<?php
+// Всегда используйте mb_* функции для UTF-8
+$str = 'Привет мир!';
+
+$length = mb_strlen($str); // 11
+$upper = mb_strtoupper($str); // ПРИВЕТ МИР!
+$sub = mb_substr($str, 0, 6); // Привет
+$pos = mb_strpos($str, 'мир'); // 7
+
+// Установка кодировки по умолчанию
+mb_internal_encoding('UTF-8');
+
+// Конвертация кодировки
+$utf8 = mb_convert_encoding($str, 'UTF-8', 'Windows-1251');
+```
+
+### Проверка кодировки
+```php
+<?php
+// Проверка валидности UTF-8
+if (mb_check_encoding($str, 'UTF-8')) {
+ echo 'Valid UTF-8';
+}
+
+// Определение кодировки
+$encoding = mb_detect_encoding($str, ['UTF-8', 'Windows-1251', 'ISO-8859-1']);
+```
+
+## Полезные функции
+
+```php
+<?php
+// wordwrap - перенос по словам
+$wrapped = wordwrap($text, 80, "\n", true);
+
+// nl2br - новые строки в <br>
+$html = nl2br($text);
+
+// strip_tags - удаление HTML тегов
+$clean = strip_tags($html);
+$clean = strip_tags($html, '<p><br>'); // сохранить некоторые теги
+
+// htmlspecialchars - экранирование HTML
+$safe = htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
+
+// html_entity_decode - декодирование
+$decoded = html_entity_decode('&'); // '&'
+
+// json_encode/decode для Unicode
+$json = json_encode(['name' => 'Иван'], JSON_UNESCAPED_UNICODE);
+```
+
+## Рекомендации для Claude Code
+
+1. **Одинарные кавычки** - для строк без интерполяции (быстрее)
+2. **mb_* функции** - всегда для UTF-8 строк
+3. **sprintf** - для форматированного вывода
+4. **heredoc** - для многострочного текста с переменными
+5. **str_contains/starts_with/ends_with** - вместо strpos (PHP 8+)
+6. **htmlspecialchars** - всегда экранируйте пользовательский ввод
--- /dev/null
+# PHP Flow Control - Управление потоком выполнения
+
+## Условные операторы
+
+### if/elseif/else
+```php
+<?php
+// ПРАВИЛЬНО - стандартный формат
+if ($condition) {
+ // код
+} elseif ($anotherCondition) {
+ // код
+} else {
+ // код
+}
+
+// ПРАВИЛЬНО - elseif (одно слово, не else if)
+if ($a > $b) {
+ echo 'a больше';
+} elseif ($a < $b) {
+ echo 'a меньше';
+} else {
+ echo 'равны';
+}
+
+// НЕПРАВИЛЬНО - else if (два слова)
+if ($a > $b) {
+ // ...
+} else if ($a < $b) { // допустимо, но не рекомендуется
+ // ...
+}
+```
+
+### Однострочные условия
+```php
+<?php
+// ПРАВИЛЬНО - простые однострочные присваивания
+$status = $isActive ? 'active' : 'inactive';
+
+// ПРАВИЛЬНО - всегда используйте скобки даже для одной строки
+if ($condition) {
+ return $value;
+}
+
+// НЕПРАВИЛЬНО - без скобок (даже если работает)
+if ($condition)
+ return $value;
+
+// ПРАВИЛЬНО - ранний выход
+if ($user === null) {
+ return null;
+}
+```
+
+### Тернарный оператор
+```php
+<?php
+// ПРАВИЛЬНО - для простых условий
+$status = $user->isActive() ? 'active' : 'inactive';
+$name = $user->getName() ?: 'Anonymous'; // Elvis operator
+
+// ПРАВИЛЬНО - null coalescing
+$name = $user->name ?? 'Anonymous';
+$config = $options['timeout'] ?? 30;
+
+// НЕПРАВИЛЬНО - вложенные тернарные операторы
+$result = $a ? $b : $c ? $d : $e; // непонятно!
+
+// ПРАВИЛЬНО - разбить на if/else
+if ($a) {
+ $result = $b;
+} elseif ($c) {
+ $result = $d;
+} else {
+ $result = $e;
+}
+```
+
+### Null coalescing и null coalescing assignment
+```php
+<?php
+// Null coalescing (??)
+$username = $_GET['user'] ?? 'guest';
+$city = $user?->address?->city ?? 'Unknown';
+
+// Вложенный null coalescing
+$value = $a ?? $b ?? $c ?? 'default';
+
+// Null coalescing assignment (??=) PHP 7.4+
+$options['timeout'] ??= 30;
+// Эквивалентно:
+// $options['timeout'] = $options['timeout'] ?? 30;
+```
+
+### Nullsafe оператор (PHP 8.0+)
+```php
+<?php
+// ПРАВИЛЬНО - nullsafe для цепочек
+$city = $user?->getAddress()?->getCity();
+
+// Вместо множества проверок
+$city = null;
+if ($user !== null) {
+ $address = $user->getAddress();
+ if ($address !== null) {
+ $city = $address->getCity();
+ }
+}
+
+// Комбинирование с ?? для default значений
+$city = $user?->getAddress()?->getCity() ?? 'Unknown';
+```
+
+## Switch и Match
+
+### Switch
+```php
+<?php
+// ПРАВИЛЬНО - с фигурными скобками для case
+switch ($status) {
+ case 'pending':
+ $this->processPending();
+ break;
+
+ case 'active':
+ case 'verified': // fall-through
+ $this->processActive();
+ break;
+
+ case 'blocked':
+ $this->processBlocked();
+ break;
+
+ default:
+ throw new InvalidStatusException($status);
+}
+
+// ВАЖНО - не забывайте break!
+```
+
+### Match (PHP 8.0+)
+```php
+<?php
+// ПРАВИЛЬНО - match для возврата значений
+$statusText = match ($status) {
+ 'pending' => 'В ожидании',
+ 'active', 'verified' => 'Активен',
+ 'blocked' => 'Заблокирован',
+ default => throw new InvalidStatusException($status),
+};
+
+// Match со строгим сравнением (===)
+$result = match (true) {
+ $age < 18 => 'minor',
+ $age < 65 => 'adult',
+ default => 'senior',
+};
+
+// Match с выражениями
+$discount = match (true) {
+ $total >= 1000 => 0.2,
+ $total >= 500 => 0.1,
+ $total >= 100 => 0.05,
+ default => 0,
+};
+```
+
+### Когда использовать switch vs match
+```php
+<?php
+// Используйте match когда:
+// - Нужно вернуть значение
+// - Достаточно строгого сравнения
+// - Каждый case - одно выражение
+
+// Используйте switch когда:
+// - Нужно выполнить несколько операций
+// - Нужен fall-through
+// - Нужно нестрогое сравнение
+```
+
+## Циклы
+
+### foreach
+```php
+<?php
+$users = ['John', 'Jane', 'Bob'];
+
+// ПРАВИЛЬНО - базовый foreach
+foreach ($users as $user) {
+ echo $user;
+}
+
+// С ключом
+foreach ($users as $index => $user) {
+ echo "$index: $user";
+}
+
+// Модификация значений (по ссылке)
+foreach ($users as &$user) {
+ $user = strtoupper($user);
+}
+unset($user); // ВАЖНО - сбросить ссылку!
+
+// Деструктуризация (PHP 7.1+)
+$points = [
+ ['x' => 1, 'y' => 2],
+ ['x' => 3, 'y' => 4],
+];
+
+foreach ($points as ['x' => $x, 'y' => $y]) {
+ echo "Point: $x, $y";
+}
+```
+
+### for
+```php
+<?php
+// ПРАВИЛЬНО - когда нужен индекс или счётчик
+for ($i = 0; $i < 10; $i++) {
+ echo $i;
+}
+
+// ПРАВИЛЬНО - кэширование count
+$count = count($items);
+for ($i = 0; $i < $count; $i++) {
+ // работа с $items[$i]
+}
+
+// НЕПРАВИЛЬНО - count в условии (вызывается каждую итерацию)
+for ($i = 0; $i < count($items); $i++) {
+ // ...
+}
+```
+
+### while и do-while
+```php
+<?php
+// while - проверка перед итерацией
+while ($row = $result->fetch()) {
+ processRow($row);
+}
+
+// do-while - хотя бы одна итерация
+do {
+ $input = readline('Enter value: ');
+} while (!isValid($input));
+
+// Чтение файла построчно
+$handle = fopen('file.txt', 'r');
+while (($line = fgets($handle)) !== false) {
+ echo $line;
+}
+fclose($handle);
+```
+
+### break и continue
+```php
+<?php
+// break - выход из цикла
+foreach ($items as $item) {
+ if ($item === null) {
+ break; // выход из foreach
+ }
+ process($item);
+}
+
+// continue - переход к следующей итерации
+foreach ($items as $item) {
+ if (!$item->isValid()) {
+ continue; // пропустить невалидные
+ }
+ process($item);
+}
+
+// break/continue с уровнем
+foreach ($rows as $row) {
+ foreach ($row as $cell) {
+ if ($cell === 'stop') {
+ break 2; // выход из обоих циклов
+ }
+ }
+}
+```
+
+## Guard Clauses (Защитные условия)
+
+### Ранний выход
+```php
+<?php
+class OrderService
+{
+ // ПРАВИЛЬНО - guard clauses в начале
+ public function process(Order $order): Result
+ {
+ if ($order === null) {
+ return Result::error('Order is null');
+ }
+
+ if (!$order->isValid()) {
+ return Result::error('Order is invalid');
+ }
+
+ if ($order->isEmpty()) {
+ return Result::error('Order is empty');
+ }
+
+ // Основная логика
+ return $this->doProcess($order);
+ }
+
+ // НЕПРАВИЛЬНО - глубокая вложенность
+ public function process(Order $order): Result
+ {
+ if ($order !== null) {
+ if ($order->isValid()) {
+ if (!$order->isEmpty()) {
+ // Основная логика
+ return $this->doProcess($order);
+ } else {
+ return Result::error('Order is empty');
+ }
+ } else {
+ return Result::error('Order is invalid');
+ }
+ } else {
+ return Result::error('Order is null');
+ }
+ }
+}
+```
+
+### В циклах
+```php
+<?php
+// ПРАВИЛЬНО - continue вместо вложенных if
+foreach ($users as $user) {
+ if (!$user->isActive()) {
+ continue;
+ }
+
+ if ($user->isBlocked()) {
+ continue;
+ }
+
+ sendNewsletter($user);
+}
+
+// НЕПРАВИЛЬНО
+foreach ($users as $user) {
+ if ($user->isActive()) {
+ if (!$user->isBlocked()) {
+ sendNewsletter($user);
+ }
+ }
+}
+```
+
+## Логические операторы
+
+### && и || vs and и or
+```php
+<?php
+// ПРАВИЛЬНО - && и || для условий (высокий приоритет)
+if ($user->isActive() && $user->hasPermission('edit')) {
+ // ...
+}
+
+$result = $value1 || $value2;
+
+// and/or имеют НИЗКИЙ приоритет - осторожно!
+$result = doSomething() or die('Error'); // работает
+$result = doSomething() || die('Error'); // присваивание!
+
+// ПРАВИЛЬНО - явные скобки для ясности
+$result = ($a && $b) || ($c && $d);
+```
+
+### Короткое замыкание
+```php
+<?php
+// && - второй операнд вычисляется только если первый true
+$user !== null && $user->isActive(); // безопасно
+
+// || - второй операнд вычисляется только если первый false
+$value = $cache->get($key) || $database->get($key);
+
+// Практическое применение
+$user && $user->notify(); // notify только если $user не null
+$config = loadFromFile() ?: loadDefaults(); // fallback
+```
+
+## Исключения в control flow
+
+### throw expressions (PHP 8.0+)
+```php
+<?php
+// ПРАВИЛЬНО - throw в выражениях
+$user = $repository->find($id) ?? throw new NotFoundException();
+
+$name = $data['name'] ?? throw new InvalidArgumentException('Name required');
+
+$result = match ($type) {
+ 'user' => new UserProcessor(),
+ 'order' => new OrderProcessor(),
+ default => throw new InvalidTypeException($type),
+};
+```
+
+## Альтернативный синтаксис (для шаблонов)
+
+```php
+<!-- ПРАВИЛЬНО - в шаблонах -->
+<?php if ($user->isAdmin()): ?>
+ <div class="admin-panel">
+ <!-- контент -->
+ </div>
+<?php endif; ?>
+
+<?php foreach ($items as $item): ?>
+ <li><?= htmlspecialchars($item->name) ?></li>
+<?php endforeach; ?>
+
+<?php switch ($status): ?>
+ <?php case 'active': ?>
+ <span class="badge-success">Active</span>
+ <?php break; ?>
+ <?php case 'pending': ?>
+ <span class="badge-warning">Pending</span>
+ <?php break; ?>
+<?php endswitch; ?>
+
+<!-- НЕПРАВИЛЬНО - фигурные скобки в шаблонах -->
+<?php if ($condition) { ?>
+ <!-- плохо смешивать -->
+<?php } ?>
+```
+
+## Рекомендации для Claude Code
+
+1. **Guard clauses** - используйте для раннего выхода и уменьшения вложенности
+2. **match > switch** - используйте match для возврата значений (PHP 8+)
+3. **Nullsafe (?->)** - вместо множественных проверок на null
+4. **Null coalescing (??)** - для значений по умолчанию
+5. **Всегда break** - не забывайте break в switch
+6. **unset после &** - сбрасывайте ссылку после foreach по ссылке
+7. **Скобки** - всегда используйте {} даже для однострочных блоков
--- /dev/null
+# PHP Exceptions - Обработка исключений
+
+## Основные принципы
+
+### Создание исключений
+```php
+<?php
+// ПРАВИЛЬНО - информативное сообщение
+throw new InvalidArgumentException('Email cannot be empty');
+
+// С кодом ошибки
+throw new RuntimeException('Database connection failed', 500);
+
+// С предыдущим исключением (цепочка)
+try {
+ $db->query($sql);
+} catch (PDOException $e) {
+ throw new DatabaseException('Query failed: ' . $sql, 0, $e);
+}
+```
+
+### Throw expressions (PHP 8.0+)
+```php
+<?php
+// ПРАВИЛЬНО - throw в выражениях
+$user = $repository->find($id) ?? throw new UserNotFoundException($id);
+
+$name = $data['name'] ?? throw new InvalidArgumentException('Name required');
+
+// В тернарном операторе
+$value = $input > 0
+ ? $input
+ : throw new InvalidArgumentException('Value must be positive');
+
+// В match
+$handler = match ($type) {
+ 'email' => new EmailHandler(),
+ 'sms' => new SmsHandler(),
+ default => throw new UnsupportedTypeException($type),
+};
+```
+
+## Обработка исключений
+
+### try/catch/finally
+```php
+<?php
+try {
+ // Опасный код
+ $result = $this->riskyOperation();
+
+} catch (InvalidArgumentException $e) {
+ // Обработка конкретного типа
+ $this->logger->warning('Invalid argument', ['error' => $e->getMessage()]);
+ return null;
+
+} catch (RuntimeException | LogicException $e) {
+ // PHP 7.1+ - несколько типов
+ $this->logger->error('Runtime or logic error', ['error' => $e->getMessage()]);
+ throw $e;
+
+} catch (Exception $e) {
+ // Общий обработчик
+ $this->logger->error('Unexpected error', [
+ 'class' => get_class($e),
+ 'message' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]);
+ throw $e;
+
+} finally {
+ // Выполняется ВСЕГДА
+ $this->cleanup();
+}
+```
+
+### Порядок catch блоков
+```php
+<?php
+// ПРАВИЛЬНО - от специфичных к общим
+try {
+ $this->processFile($path);
+} catch (FileNotFoundException $e) {
+ // Самое специфичное
+} catch (IOException $e) {
+ // Более общее
+} catch (Exception $e) {
+ // Самое общее
+}
+
+// НЕПРАВИЛЬНО - Exception первым поймает всё
+try {
+ $this->processFile($path);
+} catch (Exception $e) {
+ // Поймает все исключения!
+} catch (FileNotFoundException $e) {
+ // Никогда не выполнится
+}
+```
+
+## Кастомные исключения
+
+### Иерархия исключений
+```php
+<?php
+
+namespace App\Exceptions;
+
+/**
+ * Базовое исключение приложения.
+ */
+class ApplicationException extends \Exception
+{
+ protected array $context = [];
+
+ public function __construct(
+ string $message = '',
+ int $code = 0,
+ ?\Throwable $previous = null,
+ array $context = []
+ ) {
+ parent::__construct($message, $code, $previous);
+ $this->context = $context;
+ }
+
+ public function getContext(): array
+ {
+ return $this->context;
+ }
+}
+
+/**
+ * Исключения валидации.
+ */
+class ValidationException extends ApplicationException
+{
+ protected array $errors = [];
+
+ public function __construct(array $errors, string $message = 'Validation failed')
+ {
+ parent::__construct($message, 422);
+ $this->errors = $errors;
+ }
+
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+}
+
+/**
+ * Исключения для ненайденных ресурсов.
+ */
+class NotFoundException extends ApplicationException
+{
+ public function __construct(string $resource, int|string $id)
+ {
+ parent::__construct(
+ sprintf('%s with ID %s not found', $resource, $id),
+ 404,
+ null,
+ ['resource' => $resource, 'id' => $id]
+ );
+ }
+}
+
+class UserNotFoundException extends NotFoundException
+{
+ public function __construct(int|string $id)
+ {
+ parent::__construct('User', $id);
+ }
+}
+
+/**
+ * Исключения авторизации.
+ */
+class AuthorizationException extends ApplicationException
+{
+ public function __construct(string $message = 'Access denied')
+ {
+ parent::__construct($message, 403);
+ }
+}
+
+/**
+ * Исключения аутентификации.
+ */
+class AuthenticationException extends ApplicationException
+{
+ public function __construct(string $message = 'Authentication required')
+ {
+ parent::__construct($message, 401);
+ }
+}
+```
+
+### Использование кастомных исключений
+```php
+<?php
+
+class UserService
+{
+ public function find(int $id): User
+ {
+ $user = $this->repository->find($id);
+
+ if ($user === null) {
+ throw new UserNotFoundException($id);
+ }
+
+ return $user;
+ }
+
+ public function update(int $id, array $data): User
+ {
+ $user = $this->find($id);
+
+ $errors = $this->validate($data);
+ if (!empty($errors)) {
+ throw new ValidationException($errors);
+ }
+
+ return $this->repository->update($user, $data);
+ }
+
+ public function delete(int $id, User $actor): void
+ {
+ $user = $this->find($id);
+
+ if (!$actor->canDelete($user)) {
+ throw new AuthorizationException('Cannot delete this user');
+ }
+
+ $this->repository->delete($user);
+ }
+}
+```
+
+## Антипаттерны
+
+### Не скрывайте исключения
+```php
+<?php
+// НЕПРАВИЛЬНО - тихое проглатывание
+try {
+ $this->riskyOperation();
+} catch (Exception $e) {
+ // Ничего не делаем - плохо!
+}
+
+// ПРАВИЛЬНО - хотя бы логирование
+try {
+ $this->riskyOperation();
+} catch (Exception $e) {
+ $this->logger->error('Operation failed', [
+ 'exception' => $e,
+ ]);
+ // Можно вернуть default или re-throw
+}
+```
+
+### Не используйте исключения для control flow
+```php
+<?php
+// НЕПРАВИЛЬНО - исключения для обычной логики
+try {
+ $user = $this->findUser($id);
+} catch (UserNotFoundException $e) {
+ return $this->createUser($data);
+}
+
+// ПРАВИЛЬНО - проверка условия
+$user = $this->findUser($id);
+if ($user === null) {
+ return $this->createUser($data);
+}
+```
+
+### Не перехватывайте Exception/Throwable без необходимости
+```php
+<?php
+// НЕПРАВИЛЬНО - ловит всё подряд
+try {
+ $this->process();
+} catch (Throwable $e) {
+ // Ловит даже Error (TypeError, ParseError и т.д.)
+}
+
+// ПРАВИЛЬНО - ловите конкретные исключения
+try {
+ $this->process();
+} catch (ProcessingException $e) {
+ // Обработка известных ошибок
+} catch (Exception $e) {
+ // Обработка неожиданных ошибок
+ $this->logger->error('Unexpected exception', ['exception' => $e]);
+ throw $e;
+}
+```
+
+## Повторные попытки (Retry)
+
+```php
+<?php
+class RetryableOperation
+{
+ private int $maxAttempts = 3;
+ private int $delayMs = 100;
+
+ public function execute(callable $operation): mixed
+ {
+ $attempt = 0;
+ $lastException = null;
+
+ while ($attempt < $this->maxAttempts) {
+ try {
+ return $operation();
+ } catch (RetryableException $e) {
+ $lastException = $e;
+ $attempt++;
+
+ if ($attempt < $this->maxAttempts) {
+ // Экспоненциальная задержка
+ usleep($this->delayMs * 1000 * (2 ** $attempt));
+ }
+ }
+ }
+
+ throw new MaxRetriesExceededException(
+ "Failed after {$this->maxAttempts} attempts",
+ 0,
+ $lastException
+ );
+ }
+}
+
+// Использование
+$operation = new RetryableOperation();
+$result = $operation->execute(function () use ($api) {
+ return $api->request('/endpoint');
+});
+```
+
+## Логирование исключений
+
+```php
+<?php
+class ExceptionHandler
+{
+ public function __construct(
+ private LoggerInterface $logger,
+ ) {}
+
+ public function handle(Throwable $e): void
+ {
+ $context = [
+ 'class' => get_class($e),
+ 'message' => $e->getMessage(),
+ 'code' => $e->getCode(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ ];
+
+ // Добавляем контекст из кастомных исключений
+ if ($e instanceof ApplicationException) {
+ $context['custom_context'] = $e->getContext();
+ }
+
+ // Добавляем предыдущие исключения
+ if ($previous = $e->getPrevious()) {
+ $context['previous'] = [
+ 'class' => get_class($previous),
+ 'message' => $previous->getMessage(),
+ ];
+ }
+
+ // Уровень логирования зависит от типа
+ match (true) {
+ $e instanceof ValidationException => $this->logger->notice('Validation error', $context),
+ $e instanceof NotFoundException => $this->logger->info('Resource not found', $context),
+ $e instanceof AuthorizationException => $this->logger->warning('Access denied', $context),
+ default => $this->logger->error('Exception occurred', $context),
+ };
+ }
+}
+```
+
+## Стандартные исключения PHP
+
+```php
+<?php
+// Иерархия исключений PHP
+
+// Throwable (интерфейс)
+// ├── Error (ошибки PHP, не ловите обычно)
+// │ ├── TypeError
+// │ ├── ParseError
+// │ ├── ArithmeticError
+// │ │ └── DivisionByZeroError
+// │ └── AssertionError
+// └── Exception
+// ├── LogicException (ошибки в логике программы)
+// │ ├── BadFunctionCallException
+// │ │ └── BadMethodCallException
+// │ ├── DomainException
+// │ ├── InvalidArgumentException
+// │ ├── LengthException
+// │ └── OutOfRangeException
+// └── RuntimeException (ошибки во время выполнения)
+// ├── OutOfBoundsException
+// ├── OverflowException
+// ├── RangeException
+// ├── UnderflowException
+// └── UnexpectedValueException
+
+// Когда использовать:
+throw new InvalidArgumentException('Invalid email format');
+throw new RuntimeException('Cannot connect to database');
+throw new LogicException('Method must be overridden');
+throw new BadMethodCallException('Method not implemented');
+throw new OutOfBoundsException('Index out of bounds');
+```
+
+## finally и возврат значений
+
+```php
+<?php
+function riskyOperation(): string
+{
+ try {
+ // Какая-то операция
+ return 'success';
+ } catch (Exception $e) {
+ return 'error';
+ } finally {
+ // ВСЕГДА выполняется перед возвратом
+ cleanup();
+ // НЕ используйте return в finally - перезапишет результат!
+ }
+}
+
+// ОСТОРОЖНО с return в finally
+function badExample(): string
+{
+ try {
+ throw new Exception('Error');
+ } catch (Exception $e) {
+ return 'from catch';
+ } finally {
+ return 'from finally'; // Перезапишет 'from catch'!
+ }
+}
+echo badExample(); // 'from finally'
+```
+
+## Рекомендации для Claude Code
+
+1. **Кастомные исключения** - создавайте иерархию для вашего приложения
+2. **Информативные сообщения** - включайте контекст в сообщения
+3. **Цепочка исключений** - передавайте $previous для отладки
+4. **Логирование** - всегда логируйте исключения
+5. **Не скрывайте** - никогда не проглатывайте исключения молча
+6. **Конкретные типы** - ловите конкретные исключения, не Exception
+7. **finally** - используйте для очистки, но без return
--- /dev/null
+# PHP Closures - Замыкания и колбэки
+
+## Анонимные функции (Closures)
+
+### Базовый синтаксис
+```php
+<?php
+// Анонимная функция
+$greet = function (string $name): string {
+ return "Hello, $name!";
+};
+
+echo $greet('World'); // Hello, World!
+
+// С переменными из внешней области (use)
+$prefix = 'Mr.';
+$greet = function (string $name) use ($prefix): string {
+ return "Hello, $prefix $name!";
+};
+
+echo $greet('Smith'); // Hello, Mr. Smith!
+```
+
+### Захват переменных
+```php
+<?php
+// По значению (копия)
+$counter = 0;
+$increment = function () use ($counter) {
+ $counter++; // Изменяет только локальную копию
+ return $counter;
+};
+
+echo $increment(); // 1
+echo $counter; // 0 - оригинал не изменился
+
+// По ссылке
+$counter = 0;
+$increment = function () use (&$counter) {
+ $counter++;
+ return $counter;
+};
+
+echo $increment(); // 1
+echo $increment(); // 2
+echo $counter; // 2 - оригинал изменён
+```
+
+### Множественный захват
+```php
+<?php
+$prefix = 'User';
+$suffix = '!';
+$counter = 0;
+
+$format = function (string $name) use ($prefix, $suffix, &$counter): string {
+ $counter++;
+ return "$prefix: $name$suffix ($counter)";
+};
+
+echo $format('John'); // User: John! (1)
+echo $format('Jane'); // User: Jane! (2)
+```
+
+## Arrow функции (PHP 7.4+)
+
+### Базовый синтаксис
+```php
+<?php
+// ПРАВИЛЬНО - для простых выражений
+$double = fn(int $n): int => $n * 2;
+$sum = fn(int $a, int $b): int => $a + $b;
+
+// Автоматический захват переменных (по значению)
+$multiplier = 3;
+$multiply = fn(int $n): int => $n * $multiplier;
+
+echo $multiply(5); // 15
+```
+
+### Arrow vs традиционные замыкания
+```php
+<?php
+$numbers = [1, 2, 3, 4, 5];
+$multiplier = 2;
+
+// Arrow функция - короче и автозахват
+$doubled = array_map(fn($n) => $n * $multiplier, $numbers);
+
+// Традиционная - нужен явный use
+$doubled = array_map(function ($n) use ($multiplier) {
+ return $n * $multiplier;
+}, $numbers);
+
+// Arrow - только одно выражение
+// Традиционная - для сложной логики
+$process = function ($item) use ($config) {
+ if (!$item->isValid()) {
+ return null;
+ }
+
+ $result = $item->transform($config);
+ $this->log($result);
+
+ return $result;
+};
+```
+
+### Ограничения Arrow функций
+```php
+<?php
+// НЕЛЬЗЯ - несколько выражений
+// $fn = fn($x) => { $a = $x; return $a * 2; }; // Syntax error!
+
+// НЕЛЬЗЯ - захват по ссылке (всегда по значению)
+$counter = 0;
+$increment = fn() => $counter++; // $counter не изменится
+
+// Для этих случаев используйте традиционные замыкания
+$increment = function () use (&$counter) {
+ return $counter++;
+};
+```
+
+## Использование с array функциями
+
+### array_map
+```php
+<?php
+$users = [
+ ['name' => 'John', 'age' => 30],
+ ['name' => 'Jane', 'age' => 25],
+];
+
+// Извлечение значений
+$names = array_map(fn($u) => $u['name'], $users);
+
+// Трансформация
+$formatted = array_map(
+ fn($u) => sprintf('%s (%d)', $u['name'], $u['age']),
+ $users
+);
+
+// Несколько массивов
+$a = [1, 2, 3];
+$b = [10, 20, 30];
+$sum = array_map(fn($x, $y) => $x + $y, $a, $b);
+```
+
+### array_filter
+```php
+<?php
+$numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+
+// Фильтрация чётных
+$even = array_filter($numbers, fn($n) => $n % 2 === 0);
+
+// Фильтрация объектов
+$users = [/* ... */];
+$activeUsers = array_filter($users, fn($u) => $u->isActive());
+
+// С ключами
+$data = ['a' => 1, 'b' => 2, 'c' => 3];
+$filtered = array_filter(
+ $data,
+ fn($value, $key) => $key !== 'b' && $value > 1,
+ ARRAY_FILTER_USE_BOTH
+);
+```
+
+### array_reduce
+```php
+<?php
+$numbers = [1, 2, 3, 4, 5];
+
+// Сумма
+$sum = array_reduce($numbers, fn($carry, $n) => $carry + $n, 0);
+
+// Группировка
+$users = [
+ ['name' => 'John', 'role' => 'admin'],
+ ['name' => 'Jane', 'role' => 'user'],
+ ['name' => 'Bob', 'role' => 'admin'],
+];
+
+$grouped = array_reduce($users, function ($carry, $user) {
+ $carry[$user['role']][] = $user['name'];
+ return $carry;
+}, []);
+// ['admin' => ['John', 'Bob'], 'user' => ['Jane']]
+```
+
+### usort и сортировка
+```php
+<?php
+$users = [
+ ['name' => 'John', 'age' => 30],
+ ['name' => 'Jane', 'age' => 25],
+ ['name' => 'Bob', 'age' => 35],
+];
+
+// Сортировка по возрасту
+usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
+
+// Сортировка по нескольким полям
+usort($users, fn($a, $b) =>
+ $a['role'] <=> $b['role']
+ ?: $a['name'] <=> $b['name']
+);
+
+// uasort - сохраняет ключи
+uasort($users, fn($a, $b) => $a['age'] <=> $b['age']);
+```
+
+## Callable и типы
+
+### Callable type hint
+```php
+<?php
+class Processor
+{
+ /**
+ * @param callable(mixed): mixed $transformer
+ */
+ public function process(array $items, callable $transformer): array
+ {
+ return array_map($transformer, $items);
+ }
+}
+
+$processor = new Processor();
+
+// Анонимная функция
+$result = $processor->process([1, 2, 3], fn($n) => $n * 2);
+
+// Функция по имени
+$result = $processor->process(['a', 'b'], 'strtoupper');
+
+// Метод объекта
+$result = $processor->process($items, [$object, 'transform']);
+
+// Статический метод
+$result = $processor->process($items, [MyClass::class, 'transform']);
+
+// Invokable объект
+$result = $processor->process($items, new Transformer());
+```
+
+### Closure type hint
+```php
+<?php
+class EventDispatcher
+{
+ private array $listeners = [];
+
+ // Принимает только Closure, не любой callable
+ public function on(string $event, \Closure $listener): void
+ {
+ $this->listeners[$event][] = $listener;
+ }
+
+ public function dispatch(string $event, mixed $data = null): void
+ {
+ foreach ($this->listeners[$event] ?? [] as $listener) {
+ $listener($data);
+ }
+ }
+}
+
+$dispatcher = new EventDispatcher();
+
+// Работает
+$dispatcher->on('user.created', fn($user) => sendEmail($user));
+
+// Не работает - не Closure
+// $dispatcher->on('user.created', 'sendEmail'); // TypeError
+// $dispatcher->on('user.created', [$mailer, 'send']); // TypeError
+```
+
+## Closure методы
+
+### Closure::bind и bindTo
+```php
+<?php
+class User
+{
+ private string $name = 'John';
+ private int $age = 30;
+}
+
+$getPrivateData = function () {
+ return [$this->name, $this->age];
+};
+
+$user = new User();
+
+// Привязка к объекту и классу
+$bound = Closure::bind($getPrivateData, $user, User::class);
+[$name, $age] = $bound(); // ['John', 30]
+
+// Или через bindTo
+$bound = $getPrivateData->bindTo($user, User::class);
+```
+
+### Closure::fromCallable
+```php
+<?php
+class Calculator
+{
+ public function add(int $a, int $b): int
+ {
+ return $a + $b;
+ }
+}
+
+$calc = new Calculator();
+
+// Преобразование метода в Closure
+$closure = Closure::fromCallable([$calc, 'add']);
+echo $closure(2, 3); // 5
+
+// First-class callable syntax (PHP 8.1+)
+$closure = $calc->add(...);
+echo $closure(2, 3); // 5
+```
+
+### First-class callables (PHP 8.1+)
+```php
+<?php
+class Formatter
+{
+ public function format(string $value): string
+ {
+ return strtoupper($value);
+ }
+
+ public static function staticFormat(string $value): string
+ {
+ return strtolower($value);
+ }
+}
+
+$formatter = new Formatter();
+
+// Получение замыкания из метода
+$format = $formatter->format(...);
+$staticFormat = Formatter::staticFormat(...);
+$strlen = strlen(...);
+
+// Использование
+$names = ['John', 'Jane'];
+$upper = array_map($format, $names);
+$lower = array_map($staticFormat, $names);
+$lengths = array_map($strlen, $names);
+```
+
+## Invokable объекты
+
+### Магический метод __invoke
+```php
+<?php
+class Greeter
+{
+ public function __construct(
+ private string $greeting = 'Hello'
+ ) {}
+
+ public function __invoke(string $name): string
+ {
+ return "{$this->greeting}, {$name}!";
+ }
+}
+
+$greeter = new Greeter('Hi');
+echo $greeter('World'); // Hi, World!
+
+// Можно использовать как callable
+$names = ['John', 'Jane'];
+$greetings = array_map(new Greeter('Hello'), $names);
+```
+
+### Паттерн Handler/Action
+```php
+<?php
+interface Handler
+{
+ public function __invoke(Request $request): Response;
+}
+
+class CreateUserHandler implements Handler
+{
+ public function __construct(
+ private UserRepository $repository,
+ private Validator $validator,
+ ) {}
+
+ public function __invoke(Request $request): Response
+ {
+ $data = $request->all();
+
+ if (!$this->validator->validate($data)) {
+ return new ValidationErrorResponse($this->validator->errors());
+ }
+
+ $user = $this->repository->create($data);
+
+ return new JsonResponse($user, 201);
+ }
+}
+
+// Использование
+$handler = new CreateUserHandler($repository, $validator);
+$response = $handler($request);
+```
+
+## Паттерны использования
+
+### Callbacks и hooks
+```php
+<?php
+class Pipeline
+{
+ private array $pipes = [];
+
+ public function pipe(callable $callback): self
+ {
+ $this->pipes[] = $callback;
+ return $this;
+ }
+
+ public function process(mixed $payload): mixed
+ {
+ foreach ($this->pipes as $pipe) {
+ $payload = $pipe($payload);
+ }
+ return $payload;
+ }
+}
+
+$pipeline = (new Pipeline())
+ ->pipe(fn($data) => array_filter($data))
+ ->pipe(fn($data) => array_map('trim', $data))
+ ->pipe(fn($data) => array_unique($data));
+
+$result = $pipeline->process($input);
+```
+
+### Lazy evaluation
+```php
+<?php
+class Lazy
+{
+ private mixed $value = null;
+ private bool $evaluated = false;
+
+ public function __construct(
+ private \Closure $initializer
+ ) {}
+
+ public function get(): mixed
+ {
+ if (!$this->evaluated) {
+ $this->value = ($this->initializer)();
+ $this->evaluated = true;
+ }
+ return $this->value;
+ }
+}
+
+// Использование
+$expensive = new Lazy(fn() => heavyComputation());
+
+// Вычисляется только при первом вызове
+$value = $expensive->get();
+$value = $expensive->get(); // Возвращает кэшированное
+```
+
+### Event listeners
+```php
+<?php
+class EventEmitter
+{
+ private array $listeners = [];
+
+ public function on(string $event, callable $callback): void
+ {
+ $this->listeners[$event][] = $callback;
+ }
+
+ public function emit(string $event, mixed ...$args): void
+ {
+ foreach ($this->listeners[$event] ?? [] as $callback) {
+ $callback(...$args);
+ }
+ }
+}
+
+$emitter = new EventEmitter();
+
+$emitter->on('user.created', function (User $user) {
+ sendWelcomeEmail($user);
+});
+
+$emitter->on('user.created', fn(User $user) => logActivity($user));
+
+$emitter->emit('user.created', $newUser);
+```
+
+## Рекомендации для Claude Code
+
+1. **Arrow функции** - для простых однострочных выражений
+2. **Традиционные замыкания** - для сложной логики или захвата по ссылке
+3. **Типизация** - используйте callable или Closure в параметрах
+4. **First-class callables** - используйте `$obj->method(...)` синтаксис (PHP 8.1+)
+5. **Invokable** - для объектов-обработчиков с одной ответственностью
+6. **unset после &** - помните, что захват по ссылке может вызвать утечки
--- /dev/null
+# Yii2 Structure - Структура приложения
+
+## Структура директорий
+
+### Стандартная структура Yii2 Basic
+```
+project/
+├── assets/ # Asset bundles
+├── commands/ # Console команды
+├── config/ # Конфигурационные файлы
+│ ├── console.php # Конфиг консоли
+│ ├── db.php # Подключение к БД
+│ ├── params.php # Параметры приложения
+│ └── web.php # Конфиг веб-приложения
+├── controllers/ # Контроллеры
+├── mail/ # Шаблоны писем
+├── models/ # Модели
+├── runtime/ # Временные файлы
+├── views/ # Представления
+│ ├── layouts/ # Макеты
+│ └── site/ # Views для SiteController
+├── web/ # Document root
+│ ├── assets/ # Опубликованные assets
+│ ├── css/ # CSS файлы
+│ └── index.php # Entry script
+├── widgets/ # Виджеты
+└── tests/ # Тесты
+```
+
+### Структура Yii2 Advanced
+```
+project/
+├── backend/ # Административная часть
+│ ├── assets/
+│ ├── config/
+│ ├── controllers/
+│ ├── models/
+│ ├── runtime/
+│ ├── views/
+│ └── web/
+├── common/ # Общий код
+│ ├── config/
+│ ├── mail/
+│ ├── models/
+│ └── tests/
+├── console/ # Консольные команды
+│ ├── config/
+│ ├── controllers/
+│ ├── migrations/
+│ └── runtime/
+├── frontend/ # Клиентская часть
+│ ├── assets/
+│ ├── config/
+│ ├── controllers/
+│ ├── models/
+│ ├── runtime/
+│ ├── views/
+│ └── web/
+├── environments/ # Окружения
+│ ├── dev/
+│ └── prod/
+└── vendor/ # Зависимости Composer
+```
+
+## Модульная структура
+
+### Организация для больших проектов
+```
+project/
+├── api/ # REST API модуль
+│ ├── config/
+│ ├── controllers/
+│ │ ├── v1/
+│ │ │ ├── UserController.php
+│ │ │ └── OrderController.php
+│ │ └── v2/
+│ ├── models/
+│ └── modules/
+├── modules/ # Модули приложения
+│ ├── admin/
+│ │ ├── Module.php
+│ │ ├── controllers/
+│ │ ├── models/
+│ │ └── views/
+│ ├── user/
+│ │ ├── Module.php
+│ │ ├── controllers/
+│ │ ├── models/
+│ │ └── views/
+│ └── payment/
+├── services/ # Сервисные классы
+│ ├── payment/
+│ │ ├── PaymentService.php
+│ │ └── StripeService.php
+│ └── notification/
+│ └── EmailService.php
+├── repositories/ # Репозитории
+│ └── UserRepository.php
+├── components/ # Компоненты приложения
+│ ├── AuthManager.php
+│ └── Notifier.php
+├── helpers/ # Вспомогательные классы
+│ ├── DateHelper.php
+│ └── StringHelper.php
+├── validators/ # Кастомные валидаторы
+│ └── PhoneValidator.php
+└── behaviors/ # Behaviors
+ ├── TimestampBehavior.php
+ └── BlameableBehavior.php
+```
+
+## Конфигурация
+
+### config/web.php (Basic)
+```php
+<?php
+$params = require __DIR__ . '/params.php';
+$db = require __DIR__ . '/db.php';
+
+$config = [
+ 'id' => 'basic',
+ 'name' => 'My Application',
+ 'basePath' => dirname(__DIR__),
+ 'bootstrap' => ['log'],
+ 'language' => 'ru-RU',
+ 'sourceLanguage' => 'en-US',
+ 'timeZone' => 'Europe/Moscow',
+ 'aliases' => [
+ '@bower' => '@vendor/bower-asset',
+ '@npm' => '@vendor/npm-asset',
+ ],
+ 'components' => [
+ 'request' => [
+ 'cookieValidationKey' => 'your-secret-key-here',
+ 'parsers' => [
+ 'application/json' => \yii\web\JsonParser::class,
+ ],
+ ],
+ 'cache' => [
+ 'class' => \yii\caching\FileCache::class,
+ ],
+ 'user' => [
+ 'identityClass' => \app\models\User::class,
+ 'enableAutoLogin' => true,
+ 'loginUrl' => ['site/login'],
+ ],
+ 'errorHandler' => [
+ 'errorAction' => 'site/error',
+ ],
+ 'mailer' => [
+ 'class' => \yii\symfonymailer\Mailer::class,
+ 'viewPath' => '@app/mail',
+ 'useFileTransport' => true,
+ ],
+ 'log' => [
+ 'traceLevel' => YII_DEBUG ? 3 : 0,
+ 'targets' => [
+ [
+ 'class' => \yii\log\FileTarget::class,
+ 'levels' => ['error', 'warning'],
+ ],
+ ],
+ ],
+ 'db' => $db,
+ 'urlManager' => [
+ 'enablePrettyUrl' => true,
+ 'showScriptName' => false,
+ 'rules' => [
+ '' => 'site/index',
+ '<controller:\w+>/<action:\w+>/<id:\d+>' => '<controller>/<action>',
+ '<controller:\w+>/<action:\w+>' => '<controller>/<action>',
+ ],
+ ],
+ ],
+ 'params' => $params,
+];
+
+if (YII_ENV_DEV) {
+ $config['bootstrap'][] = 'debug';
+ $config['modules']['debug'] = [
+ 'class' => \yii\debug\Module::class,
+ 'allowedIPs' => ['127.0.0.1', '::1'],
+ ];
+
+ $config['bootstrap'][] = 'gii';
+ $config['modules']['gii'] = [
+ 'class' => \yii\gii\Module::class,
+ 'allowedIPs' => ['127.0.0.1', '::1'],
+ ];
+}
+
+return $config;
+```
+
+### config/db.php
+```php
+<?php
+return [
+ 'class' => \yii\db\Connection::class,
+ 'dsn' => 'pgsql:host=localhost;dbname=myapp',
+ 'username' => 'myapp',
+ 'password' => getenv('DB_PASSWORD'),
+ 'charset' => 'utf8',
+ 'schemaMap' => [
+ 'pgsql' => [
+ 'class' => \yii\db\pgsql\Schema::class,
+ 'defaultSchema' => 'public',
+ ],
+ ],
+ 'enableSchemaCache' => !YII_DEBUG,
+ 'schemaCacheDuration' => 3600,
+ 'schemaCache' => 'cache',
+];
+```
+
+### config/params.php
+```php
+<?php
+return [
+ 'adminEmail' => 'admin@example.com',
+ 'supportEmail' => 'support@example.com',
+ 'senderEmail' => 'noreply@example.com',
+ 'senderName' => 'My Application',
+ 'user.passwordResetTokenExpire' => 3600,
+ 'user.passwordMinLength' => 8,
+ 'pagination' => [
+ 'defaultPageSize' => 20,
+ 'pageSizeLimit' => [1, 100],
+ ],
+];
+```
+
+## Окружения
+
+### Переменные окружения
+```php
+<?php
+// web/index.php
+defined('YII_DEBUG') or define('YII_DEBUG', getenv('YII_DEBUG') === 'true');
+defined('YII_ENV') or define('YII_ENV', getenv('YII_ENV') ?: 'prod');
+
+require __DIR__ . '/../vendor/autoload.php';
+require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';
+
+$config = require __DIR__ . '/../config/web.php';
+
+(new yii\web\Application($config))->run();
+```
+
+### Конфигурация по окружению
+```php
+<?php
+// config/web.php
+$config = [
+ // ... базовая конфигурация
+];
+
+// Настройки для разработки
+if (YII_ENV_DEV) {
+ $config['components']['cache'] = [
+ 'class' => \yii\caching\DummyCache::class,
+ ];
+ $config['components']['mailer']['useFileTransport'] = true;
+}
+
+// Настройки для продакшена
+if (YII_ENV_PROD) {
+ $config['components']['cache'] = [
+ 'class' => \yii\redis\Cache::class,
+ 'redis' => [
+ 'hostname' => getenv('REDIS_HOST'),
+ 'port' => 6379,
+ 'database' => 0,
+ ],
+ ];
+ $config['components']['log']['targets'][] = [
+ 'class' => \yii\log\DbTarget::class,
+ 'levels' => ['error'],
+ ];
+}
+
+return $config;
+```
+
+## Модули
+
+### Создание модуля
+```php
+<?php
+// modules/admin/Module.php
+namespace app\modules\admin;
+
+class Module extends \yii\base\Module
+{
+ public $controllerNamespace = 'app\modules\admin\controllers';
+
+ public string $defaultRoute = 'dashboard';
+
+ public function init(): void
+ {
+ parent::init();
+
+ // Кастомная конфигурация модуля
+ \Yii::configure($this, require __DIR__ . '/config/main.php');
+ }
+
+ public function beforeAction($action): bool
+ {
+ if (!parent::beforeAction($action)) {
+ return false;
+ }
+
+ // Проверка прав доступа к модулю
+ if (\Yii::$app->user->isGuest) {
+ \Yii::$app->response->redirect(['/site/login']);
+ return false;
+ }
+
+ return true;
+ }
+}
+```
+
+### Регистрация модуля
+```php
+<?php
+// config/web.php
+return [
+ 'modules' => [
+ 'admin' => [
+ 'class' => \app\modules\admin\Module::class,
+ 'layout' => 'admin',
+ ],
+ 'api' => [
+ 'class' => \app\modules\api\Module::class,
+ ],
+ ],
+];
+```
+
+### Вложенные модули
+```php
+<?php
+// modules/admin/Module.php
+public function init(): void
+{
+ parent::init();
+
+ $this->modules = [
+ 'users' => [
+ 'class' => \app\modules\admin\modules\users\Module::class,
+ ],
+ 'reports' => [
+ 'class' => \app\modules\admin\modules\reports\Module::class,
+ ],
+ ];
+}
+```
+
+## Компоненты приложения
+
+### Создание кастомного компонента
+```php
+<?php
+// components/Notifier.php
+namespace app\components;
+
+use yii\base\Component;
+
+class Notifier extends Component
+{
+ public string $defaultChannel = 'email';
+ public array $channels = [];
+
+ public function init(): void
+ {
+ parent::init();
+
+ // Инициализация каналов
+ foreach ($this->channels as $name => $config) {
+ $this->channels[$name] = \Yii::createObject($config);
+ }
+ }
+
+ public function send(string $message, ?string $channel = null): bool
+ {
+ $channel ??= $this->defaultChannel;
+
+ if (!isset($this->channels[$channel])) {
+ throw new \InvalidArgumentException("Channel '$channel' not found");
+ }
+
+ return $this->channels[$channel]->send($message);
+ }
+}
+```
+
+### Регистрация компонента
+```php
+<?php
+// config/web.php
+return [
+ 'components' => [
+ 'notifier' => [
+ 'class' => \app\components\Notifier::class,
+ 'defaultChannel' => 'telegram',
+ 'channels' => [
+ 'email' => [
+ 'class' => \app\components\channels\EmailChannel::class,
+ ],
+ 'telegram' => [
+ 'class' => \app\components\channels\TelegramChannel::class,
+ 'botToken' => getenv('TELEGRAM_BOT_TOKEN'),
+ ],
+ ],
+ ],
+ ],
+];
+
+// Использование
+Yii::$app->notifier->send('Hello!');
+Yii::$app->notifier->send('Hello!', 'email');
+```
+
+## Сервисные классы
+
+### Структура сервиса
+```php
+<?php
+// services/payment/PaymentService.php
+namespace app\services\payment;
+
+use app\models\Order;
+use app\models\Payment;
+use yii\base\Component;
+use yii\base\InvalidConfigException;
+
+class PaymentService extends Component
+{
+ public string $defaultGateway = 'stripe';
+
+ private array $gateways = [];
+
+ /**
+ * Создание платежа
+ */
+ public function createPayment(Order $order, string $gateway = null): Payment
+ {
+ $gateway ??= $this->defaultGateway;
+
+ $payment = new Payment([
+ 'order_id' => $order->id,
+ 'amount' => $order->total,
+ 'gateway' => $gateway,
+ 'status' => Payment::STATUS_PENDING,
+ ]);
+
+ if (!$payment->save()) {
+ throw new \RuntimeException('Failed to create payment');
+ }
+
+ return $payment;
+ }
+
+ /**
+ * Обработка платежа
+ */
+ public function processPayment(Payment $payment): bool
+ {
+ $gateway = $this->getGateway($payment->gateway);
+
+ $transaction = \Yii::$app->db->beginTransaction();
+
+ try {
+ $result = $gateway->charge($payment);
+
+ $payment->status = $result->isSuccess()
+ ? Payment::STATUS_SUCCESS
+ : Payment::STATUS_FAILED;
+ $payment->transaction_id = $result->getTransactionId();
+ $payment->save(false);
+
+ if ($result->isSuccess()) {
+ $payment->order->markAsPaid();
+ }
+
+ $transaction->commit();
+
+ return $result->isSuccess();
+ } catch (\Throwable $e) {
+ $transaction->rollBack();
+ \Yii::error($e->getMessage(), __METHOD__);
+ throw $e;
+ }
+ }
+
+ private function getGateway(string $name): PaymentGatewayInterface
+ {
+ if (!isset($this->gateways[$name])) {
+ throw new InvalidConfigException("Gateway '$name' not configured");
+ }
+
+ return $this->gateways[$name];
+ }
+}
+```
+
+## Консольные команды
+
+### Создание команды
+```php
+<?php
+// commands/CleanupController.php
+namespace app\commands;
+
+use yii\console\Controller;
+use yii\console\ExitCode;
+use yii\helpers\Console;
+
+class CleanupController extends Controller
+{
+ /**
+ * @var int Количество дней для хранения
+ */
+ public int $days = 30;
+
+ public function options($actionID): array
+ {
+ return array_merge(parent::options($actionID), [
+ 'days',
+ ]);
+ }
+
+ public function optionAliases(): array
+ {
+ return array_merge(parent::optionAliases(), [
+ 'd' => 'days',
+ ]);
+ }
+
+ /**
+ * Очистка устаревших логов
+ *
+ * @return int Exit code
+ */
+ public function actionLogs(): int
+ {
+ $this->stdout("Cleaning logs older than {$this->days} days...\n", Console::FG_YELLOW);
+
+ $cutoff = date('Y-m-d', strtotime("-{$this->days} days"));
+
+ $count = \Yii::$app->db->createCommand()
+ ->delete('{{%log}}', ['<', 'created_at', $cutoff])
+ ->execute();
+
+ $this->stdout("Deleted $count log entries.\n", Console::FG_GREEN);
+
+ return ExitCode::OK;
+ }
+
+ /**
+ * Очистка временных файлов
+ */
+ public function actionTemp(): int
+ {
+ $path = \Yii::getAlias('@runtime/temp');
+
+ if (!is_dir($path)) {
+ $this->stderr("Directory not found: $path\n", Console::FG_RED);
+ return ExitCode::UNSPECIFIED_ERROR;
+ }
+
+ $files = glob($path . '/*');
+ $deleted = 0;
+
+ foreach ($files as $file) {
+ if (filemtime($file) < strtotime("-{$this->days} days")) {
+ unlink($file);
+ $deleted++;
+ }
+ }
+
+ $this->stdout("Deleted $deleted temp files.\n", Console::FG_GREEN);
+
+ return ExitCode::OK;
+ }
+}
+```
+
+### Запуск команд
+```bash
+# Базовое использование
+php yii cleanup/logs
+
+# С параметрами
+php yii cleanup/logs --days=7
+php yii cleanup/logs -d 7
+
+# Справка
+php yii help cleanup
+```
+
+## Alias и пути
+
+### Стандартные alias
+```php
+<?php
+// Встроенные alias
+Yii::getAlias('@app'); // /path/to/project
+Yii::getAlias('@runtime'); // /path/to/project/runtime
+Yii::getAlias('@webroot'); // /path/to/project/web
+Yii::getAlias('@web'); // /
+Yii::getAlias('@vendor'); // /path/to/project/vendor
+
+// Кастомные alias
+Yii::setAlias('@uploads', '@webroot/uploads');
+Yii::setAlias('@storage', '/var/storage/app');
+
+// В конфигурации
+return [
+ 'aliases' => [
+ '@bower' => '@vendor/bower-asset',
+ '@npm' => '@vendor/npm-asset',
+ '@uploads' => '@webroot/uploads',
+ '@storage' => '/var/storage/app',
+ ],
+];
+```
+
+## Events и Behaviors
+
+### Глобальные события
+```php
+<?php
+// config/web.php
+return [
+ 'on beforeRequest' => function ($event) {
+ // Логика перед обработкой запроса
+ \Yii::info('Request started', 'application');
+ },
+ 'on afterRequest' => function ($event) {
+ // Логика после обработки запроса
+ },
+];
+```
+
+### Behaviors в конфигурации
+```php
+<?php
+// config/web.php
+return [
+ 'as access' => [
+ 'class' => \yii\filters\AccessControl::class,
+ 'rules' => [
+ [
+ 'allow' => true,
+ 'roles' => ['@'],
+ ],
+ ],
+ 'except' => ['site/login', 'site/error'],
+ ],
+];
+```
+
+## Логирование
+
+### Конфигурация логов
+```php
+<?php
+// config/web.php
+return [
+ 'components' => [
+ 'log' => [
+ 'traceLevel' => YII_DEBUG ? 3 : 0,
+ 'targets' => [
+ // Файловый лог ошибок
+ [
+ 'class' => \yii\log\FileTarget::class,
+ 'levels' => ['error', 'warning'],
+ 'logFile' => '@runtime/logs/error.log',
+ 'maxFileSize' => 10240, // 10MB
+ 'maxLogFiles' => 10,
+ ],
+ // Файловый лог приложения
+ [
+ 'class' => \yii\log\FileTarget::class,
+ 'levels' => ['info'],
+ 'categories' => ['application'],
+ 'logFile' => '@runtime/logs/app.log',
+ ],
+ // Лог в БД
+ [
+ 'class' => \yii\log\DbTarget::class,
+ 'levels' => ['error'],
+ 'logTable' => '{{%log}}',
+ ],
+ // Email для критических ошибок
+ [
+ 'class' => \yii\log\EmailTarget::class,
+ 'levels' => ['error'],
+ 'categories' => ['yii\db\*'],
+ 'message' => [
+ 'from' => ['log@example.com'],
+ 'to' => ['admin@example.com'],
+ 'subject' => 'Database error',
+ ],
+ ],
+ ],
+ ],
+ ],
+];
+```
+
+### Использование логов
+```php
+<?php
+// Разные уровни
+Yii::error('Critical error', 'application');
+Yii::warning('Warning message', 'application');
+Yii::info('Info message', 'application');
+Yii::debug('Debug data', 'application');
+
+// С контекстом
+Yii::info([
+ 'message' => 'User logged in',
+ 'user_id' => $user->id,
+ 'ip' => Yii::$app->request->userIP,
+], 'auth');
+
+// Профилирование
+Yii::beginProfile('slow-operation');
+// ... операция
+Yii::endProfile('slow-operation');
+```
+
+## Рекомендации для Claude Code
+
+1. **Следуйте структуре Yii2** - используйте стандартные директории
+2. **Модульность** - разбивайте большие приложения на модули
+3. **Компоненты** - выносите переиспользуемую логику в компоненты
+4. **Сервисы** - изолируйте бизнес-логику от контроллеров
+5. **Конфигурация** - используйте переменные окружения для секретов
+6. **Логирование** - настраивайте разные targets для разных уровней
--- /dev/null
+# Yii2 Models - Модели и ActiveRecord
+
+## Структура модели
+
+### Порядок элементов в модели
+```php
+<?php
+namespace app\models;
+
+use yii\db\ActiveRecord;
+use yii\behaviors\TimestampBehavior;
+use yii\behaviors\BlameableBehavior;
+
+class User extends ActiveRecord
+{
+ // 1. Константы
+ public const STATUS_INACTIVE = 0;
+ public const STATUS_ACTIVE = 1;
+ public const STATUS_BLOCKED = 2;
+
+ public const ROLE_USER = 'user';
+ public const ROLE_MODERATOR = 'moderator';
+ public const ROLE_ADMIN = 'admin';
+
+ // 2. Публичные свойства (не из БД)
+ public ?string $password = null;
+ public ?string $passwordConfirm = null;
+
+ // 3. Приватные свойства
+ private ?Profile $_profile = null;
+
+ // 4. tableName()
+ public static function tableName(): string
+ {
+ return '{{%users}}';
+ }
+
+ // 5. behaviors()
+ public function behaviors(): array
+ {
+ return [
+ TimestampBehavior::class,
+ [
+ 'class' => BlameableBehavior::class,
+ 'createdByAttribute' => 'created_by',
+ 'updatedByAttribute' => 'updated_by',
+ ],
+ ];
+ }
+
+ // 6. rules()
+ public function rules(): array
+ {
+ return [
+ [['email', 'name'], 'required'],
+ ['email', 'email'],
+ ['email', 'unique'],
+ ['status', 'default', 'value' => self::STATUS_INACTIVE],
+ ['status', 'in', 'range' => [self::STATUS_INACTIVE, self::STATUS_ACTIVE, self::STATUS_BLOCKED]],
+ ['role', 'default', 'value' => self::ROLE_USER],
+ [['password', 'passwordConfirm'], 'required', 'on' => 'create'],
+ ['passwordConfirm', 'compare', 'compareAttribute' => 'password'],
+ ];
+ }
+
+ // 7. attributeLabels()
+ public function attributeLabels(): array
+ {
+ return [
+ 'id' => 'ID',
+ 'email' => 'Email',
+ 'name' => 'Имя',
+ 'status' => 'Статус',
+ 'role' => 'Роль',
+ 'created_at' => 'Дата создания',
+ ];
+ }
+
+ // 8. Связи (relations)
+ public function getProfile(): ActiveQuery
+ {
+ return $this->hasOne(Profile::class, ['user_id' => 'id']);
+ }
+
+ public function getPosts(): ActiveQuery
+ {
+ return $this->hasMany(Post::class, ['user_id' => 'id']);
+ }
+
+ public function getComments(): ActiveQuery
+ {
+ return $this->hasMany(Comment::class, ['user_id' => 'id'])
+ ->via('posts');
+ }
+
+ // 9. Scopes (статические методы запросов)
+ public static function find(): UserQuery
+ {
+ return new UserQuery(static::class);
+ }
+
+ // 10. Events
+ public function beforeSave($insert): bool
+ {
+ if (!parent::beforeSave($insert)) {
+ return false;
+ }
+
+ if ($this->password) {
+ $this->password_hash = \Yii::$app->security->generatePasswordHash($this->password);
+ }
+
+ return true;
+ }
+
+ public function afterSave($insert, $changedAttributes): void
+ {
+ parent::afterSave($insert, $changedAttributes);
+
+ if ($insert) {
+ $this->createDefaultProfile();
+ }
+ }
+
+ // 11. Публичные методы
+ public function getFullName(): string
+ {
+ return trim("{$this->first_name} {$this->last_name}");
+ }
+
+ public function isActive(): bool
+ {
+ return $this->status === self::STATUS_ACTIVE;
+ }
+
+ public function activate(): bool
+ {
+ $this->status = self::STATUS_ACTIVE;
+ return $this->save(false, ['status']);
+ }
+
+ // 12. Приватные методы
+ private function createDefaultProfile(): void
+ {
+ $profile = new Profile(['user_id' => $this->id]);
+ $profile->save(false);
+ }
+}
+```
+
+## Именование
+
+### Модели и таблицы
+```php
+<?php
+// ПРАВИЛЬНО - имя класса в единственном числе, CamelCase
+class User extends ActiveRecord {}
+class OrderItem extends ActiveRecord {}
+class ProductCategory extends ActiveRecord {}
+
+// Таблица - множественное число, snake_case
+public static function tableName(): string
+{
+ return '{{%users}}'; // users
+ return '{{%order_items}}'; // order_items
+}
+
+// НЕПРАВИЛЬНО
+class Users extends ActiveRecord {} // множественное число
+class orderItem extends ActiveRecord {} // не CamelCase
+```
+
+## Связи (Relations)
+
+### hasOne
+```php
+<?php
+class User extends ActiveRecord
+{
+ // Один к одному
+ public function getProfile(): ActiveQuery
+ {
+ return $this->hasOne(Profile::class, ['user_id' => 'id']);
+ }
+
+ // С условием
+ public function getActiveSubscription(): ActiveQuery
+ {
+ return $this->hasOne(Subscription::class, ['user_id' => 'id'])
+ ->andWhere(['status' => Subscription::STATUS_ACTIVE]);
+ }
+
+ // Обратная связь (inverse)
+ public function getAccount(): ActiveQuery
+ {
+ return $this->hasOne(Account::class, ['user_id' => 'id'])
+ ->inverseOf('user');
+ }
+}
+```
+
+### hasMany
+```php
+<?php
+class User extends ActiveRecord
+{
+ // Один ко многим
+ public function getPosts(): ActiveQuery
+ {
+ return $this->hasMany(Post::class, ['user_id' => 'id']);
+ }
+
+ // С сортировкой
+ public function getRecentPosts(): ActiveQuery
+ {
+ return $this->hasMany(Post::class, ['user_id' => 'id'])
+ ->orderBy(['created_at' => SORT_DESC])
+ ->limit(5);
+ }
+
+ // С условием
+ public function getPublishedPosts(): ActiveQuery
+ {
+ return $this->hasMany(Post::class, ['user_id' => 'id'])
+ ->andWhere(['status' => Post::STATUS_PUBLISHED]);
+ }
+
+ // onCondition для join-запросов
+ public function getActiveOrders(): ActiveQuery
+ {
+ return $this->hasMany(Order::class, ['user_id' => 'id'])
+ ->onCondition(['status' => Order::STATUS_ACTIVE]);
+ }
+}
+```
+
+### Many-to-Many через связующую таблицу
+```php
+<?php
+class User extends ActiveRecord
+{
+ // Через промежуточную модель
+ public function getRoles(): ActiveQuery
+ {
+ return $this->hasMany(Role::class, ['id' => 'role_id'])
+ ->viaTable('{{%user_roles}}', ['user_id' => 'id']);
+ }
+
+ // Через via() с моделью
+ public function getUserRoles(): ActiveQuery
+ {
+ return $this->hasMany(UserRole::class, ['user_id' => 'id']);
+ }
+
+ public function getRolesViaModel(): ActiveQuery
+ {
+ return $this->hasMany(Role::class, ['id' => 'role_id'])
+ ->via('userRoles');
+ }
+
+ // С доп. данными из связующей таблицы
+ public function getRolesWithPivot(): ActiveQuery
+ {
+ return $this->hasMany(Role::class, ['id' => 'role_id'])
+ ->viaTable('{{%user_roles}}', ['user_id' => 'id'])
+ ->select(['roles.*', 'user_roles.assigned_at']);
+ }
+}
+
+// Связующая модель
+class UserRole extends ActiveRecord
+{
+ public static function tableName(): string
+ {
+ return '{{%user_roles}}';
+ }
+
+ public function getUser(): ActiveQuery
+ {
+ return $this->hasOne(User::class, ['id' => 'user_id']);
+ }
+
+ public function getRole(): ActiveQuery
+ {
+ return $this->hasOne(Role::class, ['id' => 'role_id']);
+ }
+}
+```
+
+## Валидации
+
+### Встроенные валидаторы
+```php
+<?php
+public function rules(): array
+{
+ return [
+ // Обязательные поля
+ [['email', 'name'], 'required'],
+ [['email'], 'required', 'message' => 'Email обязателен для заполнения'],
+
+ // Типы данных
+ ['age', 'integer', 'min' => 0, 'max' => 150],
+ ['price', 'number', 'min' => 0],
+ ['is_active', 'boolean'],
+ ['birthday', 'date', 'format' => 'php:Y-m-d'],
+
+ // Строки
+ ['name', 'string', 'max' => 255],
+ ['bio', 'string', 'max' => 1000],
+ ['slug', 'match', 'pattern' => '/^[a-z0-9-]+$/'],
+
+ // Email и URL
+ ['email', 'email'],
+ ['website', 'url', 'defaultScheme' => 'https'],
+
+ // Уникальность
+ ['email', 'unique'],
+ ['slug', 'unique', 'targetAttribute' => ['slug', 'category_id']],
+
+ // Существование
+ ['category_id', 'exist', 'targetClass' => Category::class, 'targetAttribute' => 'id'],
+
+ // Диапазоны
+ ['status', 'in', 'range' => [self::STATUS_DRAFT, self::STATUS_PUBLISHED]],
+ ['role', 'in', 'range' => array_keys(self::getRoleList())],
+
+ // Сравнение
+ ['password_confirm', 'compare', 'compareAttribute' => 'password'],
+ ['end_date', 'compare', 'compareAttribute' => 'start_date', 'operator' => '>='],
+
+ // Значения по умолчанию
+ ['status', 'default', 'value' => self::STATUS_DRAFT],
+ ['created_at', 'default', 'value' => time()],
+
+ // Фильтры
+ ['email', 'filter', 'filter' => 'trim'],
+ ['email', 'filter', 'filter' => 'strtolower'],
+ ['name', 'filter', 'filter' => fn($value) => strip_tags($value)],
+
+ // Безопасные атрибуты
+ [['description', 'meta'], 'safe'],
+
+ // Файлы
+ ['avatar', 'file', 'extensions' => ['png', 'jpg', 'jpeg'], 'maxSize' => 5 * 1024 * 1024],
+ ['documents', 'file', 'extensions' => ['pdf', 'doc'], 'maxFiles' => 5],
+ ['image', 'image', 'minWidth' => 100, 'minHeight' => 100],
+ ];
+}
+```
+
+### Условные валидации
+```php
+<?php
+public function rules(): array
+{
+ return [
+ // when условие
+ ['phone', 'required', 'when' => function ($model) {
+ return $model->contact_method === 'phone';
+ }, 'whenClient' => "function(attribute, value) {
+ return $('#user-contact_method').val() === 'phone';
+ }"],
+
+ // skipOnEmpty
+ ['website', 'url', 'skipOnEmpty' => true],
+
+ // skipOnError
+ ['email', 'unique', 'skipOnError' => true],
+
+ // on сценарий
+ ['password', 'required', 'on' => 'create'],
+ ['password', 'string', 'min' => 8, 'on' => ['create', 'update-password']],
+
+ // except сценарий
+ ['email', 'required', 'except' => 'import'],
+ ];
+}
+```
+
+### Кастомные валидаторы
+```php
+<?php
+// Inline валидатор
+public function rules(): array
+{
+ return [
+ ['phone', 'validatePhone'],
+ ['code', function ($attribute, $params, $validator) {
+ if (!preg_match('/^[A-Z]{2}\d{4}$/', $this->$attribute)) {
+ $this->addError($attribute, 'Неверный формат кода');
+ }
+ }],
+ ];
+}
+
+public function validatePhone(string $attribute, ?array $params): void
+{
+ $phone = preg_replace('/\D/', '', $this->$attribute);
+
+ if (strlen($phone) !== 11) {
+ $this->addError($attribute, 'Телефон должен содержать 11 цифр');
+ }
+}
+
+// Класс валидатора
+// validators/PhoneValidator.php
+namespace app\validators;
+
+use yii\validators\Validator;
+
+class PhoneValidator extends Validator
+{
+ public string $pattern = '/^7\d{10}$/';
+ public string $message = 'Неверный формат телефона';
+
+ public function validateAttribute($model, $attribute): void
+ {
+ $value = preg_replace('/\D/', '', $model->$attribute);
+
+ if (!preg_match($this->pattern, $value)) {
+ $this->addError($model, $attribute, $this->message);
+ }
+ }
+
+ protected function validateValue($value): ?array
+ {
+ $value = preg_replace('/\D/', '', $value);
+
+ if (!preg_match($this->pattern, $value)) {
+ return [$this->message, []];
+ }
+
+ return null;
+ }
+}
+
+// Использование
+public function rules(): array
+{
+ return [
+ ['phone', PhoneValidator::class],
+ ];
+}
+```
+
+## Behaviors
+
+### Встроенные behaviors
+```php
+<?php
+use yii\behaviors\TimestampBehavior;
+use yii\behaviors\BlameableBehavior;
+use yii\behaviors\AttributeBehavior;
+use yii\behaviors\SluggableBehavior;
+
+public function behaviors(): array
+{
+ return [
+ // Автоматические метки времени
+ TimestampBehavior::class,
+
+ // С настройками
+ [
+ 'class' => TimestampBehavior::class,
+ 'createdAtAttribute' => 'created_at',
+ 'updatedAtAttribute' => 'updated_at',
+ 'value' => new \yii\db\Expression('NOW()'),
+ ],
+
+ // Автор записи
+ [
+ 'class' => BlameableBehavior::class,
+ 'createdByAttribute' => 'created_by',
+ 'updatedByAttribute' => 'updated_by',
+ ],
+
+ // Slug из названия
+ [
+ 'class' => SluggableBehavior::class,
+ 'attribute' => 'title',
+ 'slugAttribute' => 'slug',
+ 'ensureUnique' => true,
+ 'immutable' => true,
+ ],
+
+ // Кастомный атрибут
+ [
+ 'class' => AttributeBehavior::class,
+ 'attributes' => [
+ ActiveRecord::EVENT_BEFORE_INSERT => 'token',
+ ],
+ 'value' => fn() => \Yii::$app->security->generateRandomString(32),
+ ],
+ ];
+}
+```
+
+### Кастомный behavior
+```php
+<?php
+// behaviors/StatusBehavior.php
+namespace app\behaviors;
+
+use yii\base\Behavior;
+use yii\db\ActiveRecord;
+
+class StatusBehavior extends Behavior
+{
+ public string $statusAttribute = 'status';
+ public string $statusChangedAtAttribute = 'status_changed_at';
+
+ public function events(): array
+ {
+ return [
+ ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate',
+ ];
+ }
+
+ public function beforeUpdate(): void
+ {
+ $owner = $this->owner;
+
+ if ($owner->isAttributeChanged($this->statusAttribute)) {
+ $owner->{$this->statusChangedAtAttribute} = time();
+ }
+ }
+
+ public function changeStatus(int $status): bool
+ {
+ $this->owner->{$this->statusAttribute} = $status;
+ return $this->owner->save(false, [$this->statusAttribute, $this->statusChangedAtAttribute]);
+ }
+}
+
+// Использование
+$model->changeStatus(User::STATUS_ACTIVE);
+```
+
+## Сценарии (Scenarios)
+
+```php
+<?php
+class User extends ActiveRecord
+{
+ public const SCENARIO_CREATE = 'create';
+ public const SCENARIO_UPDATE = 'update';
+ public const SCENARIO_PROFILE = 'profile';
+
+ public function scenarios(): array
+ {
+ return [
+ self::SCENARIO_CREATE => ['email', 'password', 'name', 'role'],
+ self::SCENARIO_UPDATE => ['email', 'name', 'role'],
+ self::SCENARIO_PROFILE => ['name', 'bio', 'avatar'],
+ ];
+ }
+
+ public function rules(): array
+ {
+ return [
+ [['email', 'name'], 'required'],
+ ['password', 'required', 'on' => self::SCENARIO_CREATE],
+ ['password', 'string', 'min' => 8],
+ ['bio', 'string', 'max' => 500],
+ ];
+ }
+}
+
+// Использование
+$user = new User(['scenario' => User::SCENARIO_CREATE]);
+$user->load($data);
+$user->save();
+
+$user->scenario = User::SCENARIO_PROFILE;
+$user->load($profileData);
+$user->save();
+```
+
+## ActiveQuery и Scopes
+
+### Кастомный ActiveQuery
+```php
+<?php
+// models/query/UserQuery.php
+namespace app\models\query;
+
+use yii\db\ActiveQuery;
+
+class UserQuery extends ActiveQuery
+{
+ public function active(): self
+ {
+ return $this->andWhere(['status' => User::STATUS_ACTIVE]);
+ }
+
+ public function admins(): self
+ {
+ return $this->andWhere(['role' => User::ROLE_ADMIN]);
+ }
+
+ public function createdAfter(string $date): self
+ {
+ return $this->andWhere(['>=', 'created_at', $date]);
+ }
+
+ public function withPosts(): self
+ {
+ return $this->with('posts');
+ }
+
+ public function withProfile(): self
+ {
+ return $this->with('profile');
+ }
+
+ // Переопределение all() и one() для типизации
+ public function all($db = null): array
+ {
+ return parent::all($db);
+ }
+
+ public function one($db = null): ?User
+ {
+ return parent::one($db);
+ }
+}
+
+// В модели
+class User extends ActiveRecord
+{
+ public static function find(): UserQuery
+ {
+ return new UserQuery(static::class);
+ }
+}
+
+// Использование
+$users = User::find()
+ ->active()
+ ->admins()
+ ->createdAfter('2024-01-01')
+ ->withProfile()
+ ->all();
+```
+
+### Default scope
+```php
+<?php
+class Article extends ActiveRecord
+{
+ public static function find(): ArticleQuery
+ {
+ return (new ArticleQuery(static::class))
+ ->andWhere(['deleted_at' => null]); // Soft delete
+ }
+
+ // Запрос без default scope
+ public static function findWithDeleted(): ArticleQuery
+ {
+ return new ArticleQuery(static::class);
+ }
+}
+```
+
+## Events
+
+### Использование событий
+```php
+<?php
+class Order extends ActiveRecord
+{
+ public const EVENT_STATUS_CHANGED = 'statusChanged';
+ public const EVENT_PAID = 'paid';
+
+ public function init(): void
+ {
+ parent::init();
+
+ $this->on(self::EVENT_STATUS_CHANGED, [$this, 'onStatusChanged']);
+ $this->on(self::EVENT_PAID, [OrderService::class, 'handlePayment']);
+ }
+
+ public function beforeSave($insert): bool
+ {
+ if (!parent::beforeSave($insert)) {
+ return false;
+ }
+
+ if (!$insert && $this->isAttributeChanged('status')) {
+ $this->trigger(self::EVENT_STATUS_CHANGED, new StatusChangedEvent([
+ 'oldStatus' => $this->getOldAttribute('status'),
+ 'newStatus' => $this->status,
+ ]));
+ }
+
+ return true;
+ }
+
+ public function afterSave($insert, $changedAttributes): void
+ {
+ parent::afterSave($insert, $changedAttributes);
+
+ if (isset($changedAttributes['status']) && $this->status === self::STATUS_PAID) {
+ $this->trigger(self::EVENT_PAID);
+ }
+ }
+
+ protected function onStatusChanged(StatusChangedEvent $event): void
+ {
+ \Yii::info("Order {$this->id} status changed: {$event->oldStatus} -> {$event->newStatus}");
+ }
+}
+
+// Кастомный Event
+class StatusChangedEvent extends \yii\base\Event
+{
+ public ?int $oldStatus = null;
+ public ?int $newStatus = null;
+}
+```
+
+## Query Interface
+
+### Эффективные запросы
+```php
+<?php
+// Базовые запросы
+$user = User::findOne(1);
+$user = User::findOne(['email' => 'user@example.com']);
+$users = User::findAll(['status' => User::STATUS_ACTIVE]);
+
+// Query Builder
+$users = User::find()
+ ->where(['status' => User::STATUS_ACTIVE])
+ ->andWhere(['>=', 'created_at', '2024-01-01'])
+ ->orderBy(['created_at' => SORT_DESC])
+ ->limit(10)
+ ->all();
+
+// Сложные условия
+$users = User::find()
+ ->where(['or',
+ ['status' => User::STATUS_ACTIVE],
+ ['and',
+ ['status' => User::STATUS_INACTIVE],
+ ['role' => User::ROLE_ADMIN],
+ ],
+ ])
+ ->all();
+
+// LIKE поиск
+$users = User::find()
+ ->where(['like', 'name', $search])
+ ->orWhere(['like', 'email', $search])
+ ->all();
+
+// IN условие
+$users = User::find()
+ ->where(['id' => [1, 2, 3, 4, 5]])
+ ->all();
+
+// BETWEEN
+$orders = Order::find()
+ ->where(['between', 'created_at', $startDate, $endDate])
+ ->all();
+```
+
+### Eager Loading (избегаем N+1)
+```php
+<?php
+// НЕПРАВИЛЬНО - N+1 запросов
+$posts = Post::find()->all();
+foreach ($posts as $post) {
+ echo $post->user->name; // Запрос для каждого поста
+}
+
+// ПРАВИЛЬНО - 2 запроса
+$posts = Post::find()
+ ->with('user')
+ ->all();
+foreach ($posts as $post) {
+ echo $post->user->name; // Без дополнительных запросов
+}
+
+// Множественные связи
+$posts = Post::find()
+ ->with(['user', 'comments', 'tags'])
+ ->all();
+
+// Вложенные связи
+$posts = Post::find()
+ ->with(['user.profile', 'comments.user'])
+ ->all();
+
+// С условиями
+$posts = Post::find()
+ ->with([
+ 'comments' => function ($query) {
+ $query->andWhere(['status' => Comment::STATUS_APPROVED])
+ ->orderBy(['created_at' => SORT_DESC]);
+ },
+ ])
+ ->all();
+
+// joinWith для фильтрации
+$posts = Post::find()
+ ->joinWith('user')
+ ->where(['users.status' => User::STATUS_ACTIVE])
+ ->all();
+```
+
+### Агрегация
+```php
+<?php
+// Подсчёт
+$count = User::find()->where(['status' => User::STATUS_ACTIVE])->count();
+
+// Сумма
+$total = Order::find()->where(['user_id' => $userId])->sum('amount');
+
+// Среднее
+$avg = Product::find()->where(['category_id' => $categoryId])->average('price');
+
+// Минимум/Максимум
+$min = Product::find()->min('price');
+$max = Product::find()->max('price');
+
+// Группировка
+$stats = Order::find()
+ ->select(['status', 'COUNT(*) as count', 'SUM(amount) as total'])
+ ->groupBy('status')
+ ->asArray()
+ ->all();
+```
+
+### Batch обработка
+```php
+<?php
+// Для больших объёмов данных
+foreach (User::find()->batch(100) as $users) {
+ foreach ($users as $user) {
+ // Обработка пачками по 100
+ }
+}
+
+// each() - по одной записи, но эффективно
+foreach (User::find()->each(100) as $user) {
+ // Обработка по одному
+}
+
+// С eager loading
+foreach (User::find()->with('profile')->each(100) as $user) {
+ echo $user->profile->bio;
+}
+```
+
+## CRUD операции
+
+### Создание
+```php
+<?php
+// Способ 1 - через свойства
+$user = new User();
+$user->email = 'user@example.com';
+$user->name = 'John';
+$user->save();
+
+// Способ 2 - через конструктор
+$user = new User([
+ 'email' => 'user@example.com',
+ 'name' => 'John',
+]);
+$user->save();
+
+// Способ 3 - load из формы
+$user = new User();
+if ($user->load(\Yii::$app->request->post()) && $user->save()) {
+ return $this->redirect(['view', 'id' => $user->id]);
+}
+
+// Без валидации (осторожно!)
+$user->save(false);
+
+// Только определённые атрибуты
+$user->save(true, ['name', 'email']);
+```
+
+### Обновление
+```php
+<?php
+// Способ 1 - load + save
+$user = User::findOne($id);
+$user->name = 'New Name';
+$user->save();
+
+// Способ 2 - updateAttributes (без событий!)
+$user->updateAttributes(['name' => 'New Name']);
+
+// Способ 3 - updateAll (массовое обновление)
+User::updateAll(
+ ['status' => User::STATUS_INACTIVE],
+ ['<', 'last_login_at', strtotime('-1 year')]
+);
+
+// updateAllCounters
+Post::updateAllCounters(
+ ['view_count' => 1],
+ ['id' => $postId]
+);
+```
+
+### Удаление
+```php
+<?php
+// Удаление записи
+$user = User::findOne($id);
+$user->delete();
+
+// Массовое удаление
+User::deleteAll(['status' => User::STATUS_DELETED]);
+
+// Soft delete
+public function softDelete(): bool
+{
+ $this->deleted_at = time();
+ return $this->save(false, ['deleted_at']);
+}
+```
+
+## Транзакции
+
+```php
+<?php
+// Способ 1 - через Connection
+$transaction = \Yii::$app->db->beginTransaction();
+
+try {
+ $order = new Order($orderData);
+ if (!$order->save()) {
+ throw new \RuntimeException('Order save failed');
+ }
+
+ foreach ($items as $itemData) {
+ $item = new OrderItem($itemData);
+ $item->order_id = $order->id;
+ if (!$item->save()) {
+ throw new \RuntimeException('Item save failed');
+ }
+ }
+
+ $transaction->commit();
+} catch (\Throwable $e) {
+ $transaction->rollBack();
+ throw $e;
+}
+
+// Способ 2 - через callback
+\Yii::$app->db->transaction(function ($db) use ($orderData, $items) {
+ $order = new Order($orderData);
+ $order->save();
+
+ foreach ($items as $itemData) {
+ $item = new OrderItem($itemData);
+ $item->order_id = $order->id;
+ $item->save();
+ }
+});
+
+// Уровни изоляции
+$transaction = \Yii::$app->db->beginTransaction(
+ \yii\db\Transaction::SERIALIZABLE
+);
+```
+
+## Рекомендации для Claude Code
+
+1. **Тонкие модели** - выносите сложную бизнес-логику в сервисы
+2. **Используйте behaviors** - для повторяющейся логики (timestamps, slugs)
+3. **Кастомные ActiveQuery** - для переиспользуемых scope-ов
+4. **Eager loading** - всегда используйте with() для связей в циклах
+5. **Валидация на уровне БД** - добавляйте constraints в миграциях
+6. **Транзакции** - оборачивайте связанные операции
--- /dev/null
+# Yii2 Controllers - Контроллеры
+
+## Структура контроллера
+
+### Базовый контроллер
+```php
+<?php
+namespace app\controllers;
+
+use app\models\User;
+use yii\web\Controller;
+use yii\web\NotFoundHttpException;
+use yii\filters\AccessControl;
+use yii\filters\VerbFilter;
+use yii\data\ActiveDataProvider;
+
+class UserController extends Controller
+{
+ // 1. Behaviors (фильтры)
+ public function behaviors(): array
+ {
+ return [
+ 'access' => [
+ 'class' => AccessControl::class,
+ 'rules' => [
+ [
+ 'actions' => ['index', 'view'],
+ 'allow' => true,
+ ],
+ [
+ 'actions' => ['create', 'update', 'delete'],
+ 'allow' => true,
+ 'roles' => ['@'],
+ ],
+ ],
+ ],
+ 'verbs' => [
+ 'class' => VerbFilter::class,
+ 'actions' => [
+ 'delete' => ['POST'],
+ ],
+ ],
+ ];
+ }
+
+ // 2. Actions
+ public function actionIndex(): string
+ {
+ $dataProvider = new ActiveDataProvider([
+ 'query' => User::find()->active(),
+ 'pagination' => [
+ 'pageSize' => 20,
+ ],
+ 'sort' => [
+ 'defaultOrder' => ['created_at' => SORT_DESC],
+ ],
+ ]);
+
+ return $this->render('index', [
+ 'dataProvider' => $dataProvider,
+ ]);
+ }
+
+ public function actionView(int $id): string
+ {
+ $model = $this->findModel($id);
+
+ return $this->render('view', [
+ 'model' => $model,
+ ]);
+ }
+
+ public function actionCreate(): string|\yii\web\Response
+ {
+ $model = new User(['scenario' => User::SCENARIO_CREATE]);
+
+ if ($model->load(\Yii::$app->request->post()) && $model->save()) {
+ \Yii::$app->session->setFlash('success', 'Пользователь успешно создан');
+ return $this->redirect(['view', 'id' => $model->id]);
+ }
+
+ return $this->render('create', [
+ 'model' => $model,
+ ]);
+ }
+
+ public function actionUpdate(int $id): string|\yii\web\Response
+ {
+ $model = $this->findModel($id);
+ $model->scenario = User::SCENARIO_UPDATE;
+
+ if ($model->load(\Yii::$app->request->post()) && $model->save()) {
+ \Yii::$app->session->setFlash('success', 'Пользователь успешно обновлён');
+ return $this->redirect(['view', 'id' => $model->id]);
+ }
+
+ return $this->render('update', [
+ 'model' => $model,
+ ]);
+ }
+
+ public function actionDelete(int $id): \yii\web\Response
+ {
+ $this->findModel($id)->delete();
+ \Yii::$app->session->setFlash('success', 'Пользователь удалён');
+
+ return $this->redirect(['index']);
+ }
+
+ // 3. Вспомогательные методы
+ protected function findModel(int $id): User
+ {
+ $model = User::findOne($id);
+
+ if ($model === null) {
+ throw new NotFoundHttpException('Пользователь не найден');
+ }
+
+ return $model;
+ }
+}
+```
+
+## Наследование контроллеров
+
+### BaseController
+```php
+<?php
+// controllers/BaseController.php
+namespace app\controllers;
+
+use yii\web\Controller;
+use yii\filters\AccessControl;
+
+abstract class BaseController extends Controller
+{
+ public function behaviors(): array
+ {
+ return [
+ 'access' => [
+ 'class' => AccessControl::class,
+ 'rules' => [
+ [
+ 'allow' => true,
+ 'roles' => ['@'],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ public function beforeAction($action): bool
+ {
+ if (!parent::beforeAction($action)) {
+ return false;
+ }
+
+ // Общая логика для всех actions
+ \Yii::$app->language = \Yii::$app->session->get('language', 'ru-RU');
+
+ return true;
+ }
+
+ protected function setFlash(string $type, string $message): void
+ {
+ \Yii::$app->session->setFlash($type, $message);
+ }
+}
+```
+
+### Модульные контроллеры
+```php
+<?php
+// modules/admin/controllers/BaseController.php
+namespace app\modules\admin\controllers;
+
+use yii\web\Controller;
+use yii\web\ForbiddenHttpException;
+
+class BaseController extends Controller
+{
+ public $layout = 'admin';
+
+ public function init(): void
+ {
+ parent::init();
+
+ if (\Yii::$app->user->isGuest || !\Yii::$app->user->identity->isAdmin()) {
+ throw new ForbiddenHttpException('Доступ запрещён');
+ }
+ }
+}
+
+// modules/admin/controllers/UserController.php
+namespace app\modules\admin\controllers;
+
+class UserController extends BaseController
+{
+ // Наследует layout и проверку прав
+}
+```
+
+## Behaviors (Фильтры)
+
+### AccessControl
+```php
+<?php
+public function behaviors(): array
+{
+ return [
+ 'access' => [
+ 'class' => AccessControl::class,
+ 'only' => ['create', 'update', 'delete'],
+ 'rules' => [
+ // Гости
+ [
+ 'actions' => ['login', 'signup'],
+ 'allow' => true,
+ 'roles' => ['?'],
+ ],
+ // Авторизованные
+ [
+ 'actions' => ['logout', 'profile'],
+ 'allow' => true,
+ 'roles' => ['@'],
+ ],
+ // По ролям RBAC
+ [
+ 'actions' => ['admin'],
+ 'allow' => true,
+ 'roles' => ['admin'],
+ ],
+ // По callback
+ [
+ 'actions' => ['update'],
+ 'allow' => true,
+ 'roles' => ['@'],
+ 'matchCallback' => function ($rule, $action) {
+ return \Yii::$app->user->id === \Yii::$app->request->get('id');
+ },
+ ],
+ // По IP
+ [
+ 'actions' => ['debug'],
+ 'allow' => true,
+ 'ips' => ['127.0.0.1', '192.168.*'],
+ ],
+ ],
+ 'denyCallback' => function ($rule, $action) {
+ throw new ForbiddenHttpException('Доступ запрещён');
+ },
+ ],
+ ];
+}
+```
+
+### VerbFilter
+```php
+<?php
+public function behaviors(): array
+{
+ return [
+ 'verbs' => [
+ 'class' => VerbFilter::class,
+ 'actions' => [
+ 'index' => ['GET'],
+ 'view' => ['GET'],
+ 'create' => ['GET', 'POST'],
+ 'update' => ['GET', 'POST', 'PUT'],
+ 'delete' => ['POST', 'DELETE'],
+ ],
+ ],
+ ];
+}
+```
+
+### ContentNegotiator
+```php
+<?php
+use yii\filters\ContentNegotiator;
+use yii\web\Response;
+
+public function behaviors(): array
+{
+ return [
+ 'contentNegotiator' => [
+ 'class' => ContentNegotiator::class,
+ 'formats' => [
+ 'application/json' => Response::FORMAT_JSON,
+ 'application/xml' => Response::FORMAT_XML,
+ ],
+ ],
+ ];
+}
+```
+
+### Cors
+```php
+<?php
+use yii\filters\Cors;
+
+public function behaviors(): array
+{
+ return [
+ 'cors' => [
+ 'class' => Cors::class,
+ 'cors' => [
+ 'Origin' => ['http://localhost:3000', 'https://example.com'],
+ 'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'DELETE'],
+ 'Access-Control-Request-Headers' => ['*'],
+ 'Access-Control-Allow-Credentials' => true,
+ 'Access-Control-Max-Age' => 86400,
+ ],
+ ],
+ ];
+}
+```
+
+### RateLimiter
+```php
+<?php
+use yii\filters\RateLimiter;
+
+public function behaviors(): array
+{
+ return [
+ 'rateLimiter' => [
+ 'class' => RateLimiter::class,
+ 'enableRateLimitHeaders' => true,
+ ],
+ ];
+}
+
+// Модель должна реализовать RateLimitInterface
+class User extends ActiveRecord implements \yii\filters\RateLimitInterface
+{
+ public function getRateLimit($request, $action): array
+ {
+ return [100, 600]; // 100 запросов за 600 секунд
+ }
+
+ public function loadAllowance($request, $action): array
+ {
+ return [$this->allowance, $this->allowance_updated_at];
+ }
+
+ public function saveAllowance($request, $action, $allowance, $timestamp): void
+ {
+ $this->allowance = $allowance;
+ $this->allowance_updated_at = $timestamp;
+ $this->save(false);
+ }
+}
+```
+
+## Actions
+
+### Inline Actions
+```php
+<?php
+class SiteController extends Controller
+{
+ // Стандартный action
+ public function actionIndex(): string
+ {
+ return $this->render('index');
+ }
+
+ // С параметрами из GET
+ public function actionView(int $id, string $format = 'html'): string
+ {
+ // /site/view?id=1&format=json
+ return $this->render('view', ['id' => $id, 'format' => $format]);
+ }
+
+ // С nullable параметром
+ public function actionSearch(?string $query = null): string
+ {
+ if ($query === null) {
+ return $this->render('search-form');
+ }
+
+ $results = $this->searchService->search($query);
+ return $this->render('search-results', ['results' => $results]);
+ }
+}
+```
+
+### Standalone Actions
+```php
+<?php
+// actions/CreateAction.php
+namespace app\actions;
+
+use yii\base\Action;
+use yii\db\ActiveRecord;
+
+class CreateAction extends Action
+{
+ public string $modelClass;
+ public string $scenario = 'default';
+ public string $view = 'create';
+
+ public function run(): string|\yii\web\Response
+ {
+ /** @var ActiveRecord $model */
+ $model = new $this->modelClass(['scenario' => $this->scenario]);
+
+ if ($model->load(\Yii::$app->request->post()) && $model->save()) {
+ \Yii::$app->session->setFlash('success', 'Запись создана');
+ return $this->controller->redirect(['view', 'id' => $model->id]);
+ }
+
+ return $this->controller->render($this->view, ['model' => $model]);
+ }
+}
+
+// Использование в контроллере
+class PostController extends Controller
+{
+ public function actions(): array
+ {
+ return [
+ 'create' => [
+ 'class' => \app\actions\CreateAction::class,
+ 'modelClass' => Post::class,
+ 'scenario' => Post::SCENARIO_CREATE,
+ ],
+ 'error' => [
+ 'class' => \yii\web\ErrorAction::class,
+ ],
+ 'captcha' => [
+ 'class' => \yii\captcha\CaptchaAction::class,
+ 'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
+ ],
+ ];
+ }
+}
+```
+
+### ViewAction для статических страниц
+```php
+<?php
+public function actions(): array
+{
+ return [
+ 'page' => [
+ 'class' => \yii\web\ViewAction::class,
+ 'viewPrefix' => 'pages',
+ ],
+ ];
+}
+
+// URL: /site/page?view=about -> views/site/pages/about.php
+```
+
+## Request и Response
+
+### Работа с Request
+```php
+<?php
+public function actionProcess(): \yii\web\Response
+{
+ $request = \Yii::$app->request;
+
+ // GET параметры
+ $id = $request->get('id');
+ $page = $request->get('page', 1); // со значением по умолчанию
+
+ // POST параметры
+ $name = $request->post('name');
+ $data = $request->post(); // все POST данные
+
+ // Любой параметр
+ $value = $request->getQueryParam('key') ?? $request->getBodyParam('key');
+
+ // JSON body
+ if ($request->isAjax || $request->contentType === 'application/json') {
+ $data = $request->bodyParams;
+ }
+
+ // Проверка метода
+ if ($request->isPost) {
+ // ...
+ }
+ if ($request->isAjax) {
+ // ...
+ }
+ if ($request->isPjax) {
+ // ...
+ }
+
+ // Информация о запросе
+ $url = $request->absoluteUrl;
+ $method = $request->method;
+ $ip = $request->userIP;
+ $host = $request->userHost;
+ $headers = $request->headers;
+ $cookies = $request->cookies;
+
+ return $this->asJson(['success' => true]);
+}
+```
+
+### Работа с Response
+```php
+<?php
+public function actionDownload(): \yii\web\Response
+{
+ $response = \Yii::$app->response;
+
+ // JSON ответ
+ $response->format = Response::FORMAT_JSON;
+ $response->data = ['status' => 'ok', 'data' => $data];
+ return $response;
+
+ // Или через хелпер
+ return $this->asJson(['status' => 'ok']);
+
+ // XML ответ
+ return $this->asXml(['status' => 'ok']);
+
+ // Отправка файла
+ return $response->sendFile('/path/to/file.pdf', 'download.pdf');
+
+ // Отправка содержимого как файл
+ return $response->sendContentAsFile($content, 'report.csv', [
+ 'mimeType' => 'text/csv',
+ ]);
+
+ // Stream
+ return $response->sendStreamAsFile($stream, 'large-file.zip');
+
+ // Установка заголовков
+ $response->headers->set('X-Custom-Header', 'value');
+ $response->headers->add('Cache-Control', 'no-cache');
+
+ // Cookies
+ $response->cookies->add(new \yii\web\Cookie([
+ 'name' => 'token',
+ 'value' => $token,
+ 'expire' => time() + 3600,
+ 'httpOnly' => true,
+ 'secure' => true,
+ ]));
+
+ // Статус код
+ $response->statusCode = 201;
+
+ return $response;
+}
+```
+
+### Редиректы
+```php
+<?php
+public function actionRedirects(): \yii\web\Response
+{
+ // На action
+ return $this->redirect(['view', 'id' => 1]);
+
+ // На URL
+ return $this->redirect('https://example.com');
+
+ // Назад
+ return $this->goBack();
+
+ // На главную
+ return $this->goHome();
+
+ // С кодом
+ return $this->redirect(['index'], 301); // Permanent redirect
+
+ // Refresh
+ return $this->refresh();
+}
+```
+
+## AJAX и JSON
+
+### AJAX обработка
+```php
+<?php
+public function actionAjaxUpdate(int $id): array
+{
+ \Yii::$app->response->format = Response::FORMAT_JSON;
+
+ if (!\Yii::$app->request->isAjax) {
+ throw new BadRequestHttpException('Только AJAX запросы');
+ }
+
+ $model = $this->findModel($id);
+
+ if ($model->load(\Yii::$app->request->post()) && $model->save()) {
+ return [
+ 'success' => true,
+ 'message' => 'Сохранено',
+ 'data' => $model->attributes,
+ ];
+ }
+
+ return [
+ 'success' => false,
+ 'errors' => $model->errors,
+ ];
+}
+
+public function actionValidate(): array
+{
+ \Yii::$app->response->format = Response::FORMAT_JSON;
+
+ $model = new User();
+ $model->load(\Yii::$app->request->post());
+
+ return \yii\widgets\ActiveForm::validate($model);
+}
+```
+
+### Pjax поддержка
+```php
+<?php
+public function actionIndex(): string
+{
+ $dataProvider = new ActiveDataProvider([
+ 'query' => Post::find(),
+ ]);
+
+ // Pjax автоматически обрабатывается
+ return $this->render('index', [
+ 'dataProvider' => $dataProvider,
+ ]);
+}
+
+// В view
+<?php \yii\widgets\Pjax::begin(['id' => 'posts-pjax']); ?>
+ <?= \yii\grid\GridView::widget([
+ 'dataProvider' => $dataProvider,
+ ]) ?>
+<?php \yii\widgets\Pjax::end(); ?>
+```
+
+## REST API контроллеры
+
+### ActiveController
+```php
+<?php
+// controllers/api/v1/UserController.php
+namespace app\controllers\api\v1;
+
+use yii\rest\ActiveController;
+use yii\filters\auth\HttpBearerAuth;
+
+class UserController extends ActiveController
+{
+ public $modelClass = \app\models\User::class;
+
+ // Сериализация
+ public $serializer = [
+ 'class' => \yii\rest\Serializer::class,
+ 'collectionEnvelope' => 'items',
+ ];
+
+ public function behaviors(): array
+ {
+ $behaviors = parent::behaviors();
+
+ // Аутентификация
+ $behaviors['authenticator'] = [
+ 'class' => HttpBearerAuth::class,
+ 'except' => ['index', 'view'],
+ ];
+
+ // Rate limiting
+ $behaviors['rateLimiter'] = [
+ 'class' => \yii\filters\RateLimiter::class,
+ ];
+
+ return $behaviors;
+ }
+
+ public function actions(): array
+ {
+ $actions = parent::actions();
+
+ // Кастомизация действий
+ unset($actions['delete']); // Отключить удаление
+
+ $actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider'];
+
+ return $actions;
+ }
+
+ public function prepareDataProvider(): ActiveDataProvider
+ {
+ return new ActiveDataProvider([
+ 'query' => User::find()->active(),
+ 'pagination' => [
+ 'pageSize' => 20,
+ ],
+ ]);
+ }
+
+ // Кастомный action
+ public function actionMe(): array
+ {
+ return \Yii::$app->user->identity->toArray();
+ }
+
+ protected function verbs(): array
+ {
+ return [
+ 'index' => ['GET', 'HEAD'],
+ 'view' => ['GET', 'HEAD'],
+ 'create' => ['POST'],
+ 'update' => ['PUT', 'PATCH'],
+ 'delete' => ['DELETE'],
+ 'me' => ['GET'],
+ ];
+ }
+}
+```
+
+### Кастомный REST контроллер
+```php
+<?php
+namespace app\controllers\api\v1;
+
+use yii\rest\Controller;
+use yii\filters\auth\CompositeAuth;
+use yii\filters\auth\HttpBearerAuth;
+use yii\filters\auth\QueryParamAuth;
+
+class BaseApiController extends Controller
+{
+ public function behaviors(): array
+ {
+ $behaviors = parent::behaviors();
+
+ $behaviors['authenticator'] = [
+ 'class' => CompositeAuth::class,
+ 'authMethods' => [
+ HttpBearerAuth::class,
+ [
+ 'class' => QueryParamAuth::class,
+ 'tokenParam' => 'access_token',
+ ],
+ ],
+ ];
+
+ $behaviors['contentNegotiator']['formats'] = [
+ 'application/json' => Response::FORMAT_JSON,
+ ];
+
+ return $behaviors;
+ }
+
+ protected function success(mixed $data = null, int $statusCode = 200): array
+ {
+ \Yii::$app->response->statusCode = $statusCode;
+
+ return [
+ 'success' => true,
+ 'data' => $data,
+ ];
+ }
+
+ protected function error(string $message, array $errors = [], int $statusCode = 400): array
+ {
+ \Yii::$app->response->statusCode = $statusCode;
+
+ return [
+ 'success' => false,
+ 'message' => $message,
+ 'errors' => $errors,
+ ];
+ }
+}
+
+class OrderController extends BaseApiController
+{
+ public function actionCreate(): array
+ {
+ $model = new Order();
+
+ if ($model->load(\Yii::$app->request->bodyParams, '') && $model->save()) {
+ return $this->success($model->toArray(), 201);
+ }
+
+ return $this->error('Ошибка валидации', $model->errors, 422);
+ }
+}
+```
+
+## Обработка ошибок
+
+### ErrorHandler
+```php
+<?php
+class SiteController extends Controller
+{
+ public function actions(): array
+ {
+ return [
+ 'error' => [
+ 'class' => \yii\web\ErrorAction::class,
+ 'view' => 'error',
+ ],
+ ];
+ }
+}
+
+// views/site/error.php
+<?php
+use yii\helpers\Html;
+
+$this->title = $name;
+?>
+<div class="site-error">
+ <h1><?= Html::encode($this->title) ?></h1>
+ <div class="alert alert-danger">
+ <?= nl2br(Html::encode($message)) ?>
+ </div>
+ <?php if (YII_DEBUG): ?>
+ <pre><?= Html::encode($exception->getTraceAsString()) ?></pre>
+ <?php endif; ?>
+</div>
+```
+
+### Кастомная обработка исключений
+```php
+<?php
+class BaseController extends Controller
+{
+ public function beforeAction($action): bool
+ {
+ try {
+ return parent::beforeAction($action);
+ } catch (\Throwable $e) {
+ $this->handleException($e);
+ return false;
+ }
+ }
+
+ protected function handleException(\Throwable $e): void
+ {
+ \Yii::error($e->getMessage(), __METHOD__);
+
+ if (\Yii::$app->request->isAjax) {
+ \Yii::$app->response->format = Response::FORMAT_JSON;
+ \Yii::$app->response->data = [
+ 'success' => false,
+ 'message' => YII_DEBUG ? $e->getMessage() : 'Произошла ошибка',
+ ];
+ \Yii::$app->response->statusCode = $this->getStatusCode($e);
+ \Yii::$app->response->send();
+ }
+ }
+
+ private function getStatusCode(\Throwable $e): int
+ {
+ if ($e instanceof \yii\web\HttpException) {
+ return $e->statusCode;
+ }
+ return 500;
+ }
+}
+```
+
+## Sessions и Cookies
+
+### Sessions
+```php
+<?php
+public function actionSession(): void
+{
+ $session = \Yii::$app->session;
+
+ // Запись
+ $session->set('user_preferences', ['theme' => 'dark']);
+ $session['cart'] = $cartItems;
+
+ // Чтение
+ $preferences = $session->get('user_preferences', []);
+ $cart = $session['cart'] ?? [];
+
+ // Проверка
+ if ($session->has('user_preferences')) {
+ // ...
+ }
+
+ // Удаление
+ $session->remove('cart');
+
+ // Flash сообщения
+ $session->setFlash('success', 'Операция выполнена успешно');
+ $session->setFlash('error', 'Произошла ошибка');
+ $session->addFlash('info', 'Информация 1');
+ $session->addFlash('info', 'Информация 2');
+
+ // В view
+ // Yii::$app->session->getFlash('success')
+ // Yii::$app->session->getAllFlashes()
+}
+```
+
+### Cookies
+```php
+<?php
+public function actionCookies(): void
+{
+ $cookies = \Yii::$app->request->cookies;
+ $responseCookies = \Yii::$app->response->cookies;
+
+ // Чтение
+ $token = $cookies->getValue('remember_token');
+ if ($cookies->has('remember_token')) {
+ $cookie = $cookies->get('remember_token');
+ }
+
+ // Запись
+ $responseCookies->add(new \yii\web\Cookie([
+ 'name' => 'remember_token',
+ 'value' => $token,
+ 'expire' => time() + 86400 * 30, // 30 дней
+ 'httpOnly' => true,
+ 'secure' => !YII_DEBUG,
+ 'sameSite' => \yii\web\Cookie::SAME_SITE_LAX,
+ ]));
+
+ // Удаление
+ $responseCookies->remove('remember_token');
+}
+```
+
+## Layout и рендеринг
+
+### Управление layout
+```php
+<?php
+class AdminController extends Controller
+{
+ public $layout = 'admin'; // views/layouts/admin.php
+
+ public function actionDashboard(): string
+ {
+ // Использует layout 'admin'
+ return $this->render('dashboard');
+ }
+
+ public function actionPrint(int $id): string
+ {
+ // Без layout
+ $this->layout = false;
+ return $this->render('print', ['model' => $this->findModel($id)]);
+ }
+
+ public function actionPopup(): string
+ {
+ // Другой layout
+ $this->layout = 'popup';
+ return $this->render('popup-content');
+ }
+}
+```
+
+### Методы рендеринга
+```php
+<?php
+public function actionRender(): string
+{
+ // С layout
+ return $this->render('view', ['model' => $model]);
+
+ // Без layout
+ return $this->renderPartial('_item', ['item' => $item]);
+
+ // Для AJAX (без layout, с asset bundles)
+ return $this->renderAjax('_form', ['model' => $model]);
+
+ // Из другой директории
+ return $this->render('@app/modules/admin/views/user/view', ['model' => $model]);
+
+ // Содержимое в layout
+ return $this->renderContent('<h1>Hello</h1>');
+
+ // Файл
+ return $this->renderFile('@app/views/site/test.php', ['var' => $value]);
+}
+```
+
+## Рекомендации для Claude Code
+
+1. **Тонкие контроллеры** - логику выносите в модели и сервисы
+2. **RESTful** - следуйте REST конвенциям для API
+3. **Фильтры** - используйте behaviors для access control и validation
+4. **Типизация** - указывайте типы параметров и возвращаемых значений
+5. **DRY** - общий код выносите в базовые контроллеры и standalone actions
+6. **Безопасность** - всегда проверяйте права доступа через AccessControl
--- /dev/null
+# Yii2 Views - Представления и шаблоны
+
+## Структура views
+
+### Организация директорий
+```
+views/
+├── layouts/ # Макеты
+│ ├── main.php # Основной layout
+│ ├── admin.php # Админский layout
+│ └── blank.php # Пустой layout
+├── site/ # Views для SiteController
+│ ├── index.php
+│ ├── login.php
+│ ├── error.php
+│ └── _search.php # Partial (начинается с _)
+├── user/ # Views для UserController
+│ ├── index.php
+│ ├── view.php
+│ ├── create.php
+│ ├── update.php
+│ └── _form.php # Partial формы
+└── widgets/ # Views для виджетов
+ └── menu.php
+```
+
+## Layouts
+
+### Базовый layout
+```php
+<?php
+// views/layouts/main.php
+use app\assets\AppAsset;
+use yii\bootstrap5\Html;
+use yii\bootstrap5\Nav;
+use yii\bootstrap5\NavBar;
+use yii\bootstrap5\Breadcrumbs;
+use yii\widgets\Alert;
+
+/** @var yii\web\View $this */
+/** @var string $content */
+
+AppAsset::register($this);
+
+$this->registerCsrfMetaTags();
+$this->registerMetaTag(['charset' => Yii::$app->charset], 'charset');
+$this->registerMetaTag(['name' => 'viewport', 'content' => 'width=device-width, initial-scale=1']);
+$this->registerLinkTag(['rel' => 'icon', 'type' => 'image/x-icon', 'href' => '/favicon.ico']);
+?>
+<?php $this->beginPage() ?>
+<!DOCTYPE html>
+<html lang="<?= Yii::$app->language ?>" class="h-100">
+<head>
+ <title><?= Html::encode($this->title) ?></title>
+ <?php $this->head() ?>
+</head>
+<body class="d-flex flex-column h-100">
+<?php $this->beginBody() ?>
+
+<header>
+ <?php
+ NavBar::begin([
+ 'brandLabel' => Yii::$app->name,
+ 'brandUrl' => Yii::$app->homeUrl,
+ 'options' => ['class' => 'navbar navbar-expand-md navbar-dark bg-dark fixed-top'],
+ ]);
+ echo Nav::widget([
+ 'options' => ['class' => 'navbar-nav ms-auto'],
+ 'items' => [
+ ['label' => 'Главная', 'url' => ['/site/index']],
+ ['label' => 'О нас', 'url' => ['/site/about']],
+ ['label' => 'Контакты', 'url' => ['/site/contact']],
+ Yii::$app->user->isGuest
+ ? ['label' => 'Войти', 'url' => ['/site/login']]
+ : '<li class="nav-item">'
+ . Html::beginForm(['/site/logout'], 'post', ['class' => 'd-flex'])
+ . Html::submitButton(
+ 'Выйти (' . Yii::$app->user->identity->username . ')',
+ ['class' => 'btn btn-link nav-link']
+ )
+ . Html::endForm()
+ . '</li>',
+ ],
+ ]);
+ NavBar::end();
+ ?>
+</header>
+
+<main role="main" class="flex-shrink-0">
+ <div class="container">
+ <?= Breadcrumbs::widget([
+ 'links' => $this->params['breadcrumbs'] ?? [],
+ ]) ?>
+ <?php foreach (Yii::$app->session->getAllFlashes() as $type => $messages): ?>
+ <?php foreach ((array)$messages as $message): ?>
+ <?= Alert::widget([
+ 'options' => ['class' => "alert alert-{$type}"],
+ 'body' => Html::encode($message),
+ ]) ?>
+ <?php endforeach ?>
+ <?php endforeach ?>
+ <?= $content ?>
+ </div>
+</main>
+
+<footer class="footer mt-auto py-3 text-muted">
+ <div class="container">
+ <p class="float-start">© <?= Html::encode(Yii::$app->name) ?> <?= date('Y') ?></p>
+ <p class="float-end"><?= Yii::powered() ?></p>
+ </div>
+</footer>
+
+<?php $this->endBody() ?>
+</body>
+</html>
+<?php $this->endPage() ?>
+```
+
+### Вложенные layouts
+```php
+<?php
+// views/layouts/admin.php
+/** @var yii\web\View $this */
+/** @var string $content */
+
+$this->beginContent('@app/views/layouts/main.php');
+?>
+
+<div class="row">
+ <div class="col-md-3">
+ <?= $this->render('_sidebar') ?>
+ </div>
+ <div class="col-md-9">
+ <?= $content ?>
+ </div>
+</div>
+
+<?php $this->endContent() ?>
+```
+
+## View файлы
+
+### Базовый view
+```php
+<?php
+// views/user/index.php
+use yii\helpers\Html;
+use yii\grid\GridView;
+
+/** @var yii\web\View $this */
+/** @var yii\data\ActiveDataProvider $dataProvider */
+/** @var app\models\UserSearch $searchModel */
+
+$this->title = 'Пользователи';
+$this->params['breadcrumbs'][] = $this->title;
+?>
+
+<div class="user-index">
+ <h1><?= Html::encode($this->title) ?></h1>
+
+ <p>
+ <?= Html::a('Создать пользователя', ['create'], ['class' => 'btn btn-success']) ?>
+ </p>
+
+ <?= $this->render('_search', ['model' => $searchModel]) ?>
+
+ <?= GridView::widget([
+ 'dataProvider' => $dataProvider,
+ 'filterModel' => $searchModel,
+ 'columns' => [
+ ['class' => 'yii\grid\SerialColumn'],
+ 'id',
+ 'username',
+ 'email:email',
+ 'status',
+ 'created_at:datetime',
+ [
+ 'class' => 'yii\grid\ActionColumn',
+ 'template' => '{view} {update} {delete}',
+ ],
+ ],
+ ]) ?>
+</div>
+```
+
+### View с формой
+```php
+<?php
+// views/user/create.php
+use yii\helpers\Html;
+
+/** @var yii\web\View $this */
+/** @var app\models\User $model */
+
+$this->title = 'Создание пользователя';
+$this->params['breadcrumbs'][] = ['label' => 'Пользователи', 'url' => ['index']];
+$this->params['breadcrumbs'][] = $this->title;
+?>
+
+<div class="user-create">
+ <h1><?= Html::encode($this->title) ?></h1>
+
+ <?= $this->render('_form', ['model' => $model]) ?>
+</div>
+```
+
+### Partial (_form.php)
+```php
+<?php
+// views/user/_form.php
+use yii\helpers\Html;
+use yii\bootstrap5\ActiveForm;
+
+/** @var yii\web\View $this */
+/** @var app\models\User $model */
+/** @var yii\bootstrap5\ActiveForm $form */
+?>
+
+<div class="user-form">
+ <?php $form = ActiveForm::begin([
+ 'id' => 'user-form',
+ 'enableAjaxValidation' => true,
+ 'options' => ['enctype' => 'multipart/form-data'],
+ ]) ?>
+
+ <?= $form->field($model, 'username')->textInput(['maxlength' => true]) ?>
+
+ <?= $form->field($model, 'email')->input('email') ?>
+
+ <?= $form->field($model, 'password')->passwordInput() ?>
+
+ <?= $form->field($model, 'status')->dropDownList(
+ User::getStatusList(),
+ ['prompt' => 'Выберите статус']
+ ) ?>
+
+ <?= $form->field($model, 'role')->radioList(User::getRoleList()) ?>
+
+ <?= $form->field($model, 'is_active')->checkbox() ?>
+
+ <?= $form->field($model, 'avatar')->fileInput(['accept' => 'image/*']) ?>
+
+ <?= $form->field($model, 'bio')->textarea(['rows' => 6]) ?>
+
+ <div class="form-group">
+ <?= Html::submitButton(
+ $model->isNewRecord ? 'Создать' : 'Сохранить',
+ ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']
+ ) ?>
+ </div>
+
+ <?php ActiveForm::end() ?>
+</div>
+```
+
+## Html Helper
+
+### Основные методы
+```php
+<?php
+use yii\helpers\Html;
+
+// Экранирование
+echo Html::encode($unsafeText);
+echo Html::decode($encodedText);
+
+// Теги
+echo Html::tag('p', 'Параграф', ['class' => 'text-muted']);
+echo Html::tag('div', $content, ['id' => 'container', 'data-id' => 123]);
+echo Html::beginTag('div', ['class' => 'wrapper']);
+echo Html::endTag('div');
+
+// Ссылки
+echo Html::a('Текст ссылки', ['controller/action', 'id' => 1]);
+echo Html::a('Внешняя', 'https://example.com', ['target' => '_blank']);
+echo Html::mailto('Email', 'user@example.com');
+
+// Изображения
+echo Html::img('/images/logo.png', ['alt' => 'Логотип', 'class' => 'img-fluid']);
+
+// Списки
+echo Html::ul(['Первый', 'Второй', 'Третий'], ['class' => 'list-group']);
+echo Html::ol($items, ['class' => 'list-group']);
+
+// CSS и JS
+echo Html::cssFile('/css/style.css');
+echo Html::jsFile('/js/app.js');
+echo Html::style('.red { color: red; }');
+echo Html::script('alert("Hello");');
+```
+
+### Формы
+```php
+<?php
+use yii\helpers\Html;
+
+// Форма
+echo Html::beginForm(['site/search'], 'get', ['class' => 'form-inline']);
+echo Html::endForm();
+
+// Поля формы
+echo Html::textInput('name', $value, ['class' => 'form-control']);
+echo Html::passwordInput('password', '', ['class' => 'form-control']);
+echo Html::textarea('message', $text, ['rows' => 5]);
+echo Html::hiddenInput('token', $token);
+
+// Select
+echo Html::dropDownList('status', $selected, $items, ['prompt' => 'Выберите']);
+echo Html::listBox('roles', $selected, $items, ['multiple' => true, 'size' => 5]);
+
+// Radio и Checkbox
+echo Html::radio('gender', false, ['label' => 'Мужской', 'value' => 'male']);
+echo Html::checkbox('agree', true, ['label' => 'Согласен']);
+echo Html::radioList('gender', 'male', ['male' => 'Мужской', 'female' => 'Женский']);
+echo Html::checkboxList('hobbies', [], ['sport' => 'Спорт', 'music' => 'Музыка']);
+
+// Файл
+echo Html::fileInput('document', null, ['accept' => '.pdf,.doc']);
+
+// Кнопки
+echo Html::submitButton('Отправить', ['class' => 'btn btn-primary']);
+echo Html::resetButton('Сбросить', ['class' => 'btn btn-secondary']);
+echo Html::button('Кнопка', ['class' => 'btn btn-info', 'onclick' => 'doSomething()']);
+
+// CSRF
+echo Html::csrfMetaTags();
+```
+
+### Active поля (с моделью)
+```php
+<?php
+use yii\helpers\Html;
+
+// Active поля связаны с моделью
+echo Html::activeTextInput($model, 'username', ['class' => 'form-control']);
+echo Html::activePasswordInput($model, 'password');
+echo Html::activeTextarea($model, 'bio', ['rows' => 5]);
+echo Html::activeDropDownList($model, 'status', $statusList);
+echo Html::activeCheckbox($model, 'is_active');
+echo Html::activeRadioList($model, 'role', $roleList);
+echo Html::activeFileInput($model, 'avatar');
+echo Html::activeHiddenInput($model, 'id');
+
+// Label и Error
+echo Html::activeLabel($model, 'username');
+echo Html::error($model, 'username', ['class' => 'text-danger']);
+```
+
+## ActiveForm
+
+### Базовое использование
+```php
+<?php
+use yii\bootstrap5\ActiveForm;
+use yii\bootstrap5\Html;
+
+$form = ActiveForm::begin([
+ 'id' => 'contact-form',
+ 'action' => ['site/contact'],
+ 'method' => 'post',
+ 'options' => [
+ 'class' => 'form-horizontal',
+ 'enctype' => 'multipart/form-data',
+ ],
+ 'fieldConfig' => [
+ 'template' => "{label}\n{input}\n{hint}\n{error}",
+ 'labelOptions' => ['class' => 'form-label'],
+ 'inputOptions' => ['class' => 'form-control'],
+ 'errorOptions' => ['class' => 'invalid-feedback'],
+ 'hintOptions' => ['class' => 'form-text text-muted'],
+ ],
+]);
+?>
+
+<?= $form->field($model, 'name')->textInput(['autofocus' => true]) ?>
+
+<?= $form->field($model, 'email')->input('email') ?>
+
+<?= $form->field($model, 'subject')->textInput(['maxlength' => true]) ?>
+
+<?= $form->field($model, 'body')->textarea(['rows' => 6]) ?>
+
+<?= $form->field($model, 'verifyCode')->widget(\yii\captcha\Captcha::class) ?>
+
+<div class="form-group">
+ <?= Html::submitButton('Отправить', ['class' => 'btn btn-primary']) ?>
+</div>
+
+<?php ActiveForm::end() ?>
+```
+
+### Расширенные поля формы
+```php
+<?php
+// Различные типы полей
+<?= $form->field($model, 'birthday')->input('date') ?>
+<?= $form->field($model, 'time')->input('time') ?>
+<?= $form->field($model, 'datetime')->input('datetime-local') ?>
+<?= $form->field($model, 'color')->input('color') ?>
+<?= $form->field($model, 'range')->input('range', ['min' => 0, 'max' => 100]) ?>
+
+// Dropdown с группами
+<?= $form->field($model, 'category')->dropDownList([
+ 'Фрукты' => [
+ 'apple' => 'Яблоко',
+ 'orange' => 'Апельсин',
+ ],
+ 'Овощи' => [
+ 'tomato' => 'Помидор',
+ 'potato' => 'Картофель',
+ ],
+]) ?>
+
+// Radio и Checkbox списки
+<?= $form->field($model, 'role')->radioList([
+ 'user' => 'Пользователь',
+ 'admin' => 'Администратор',
+], [
+ 'item' => function ($index, $label, $name, $checked, $value) {
+ return Html::radio($name, $checked, [
+ 'value' => $value,
+ 'label' => $label,
+ 'class' => 'form-check-input',
+ ]);
+ },
+]) ?>
+
+<?= $form->field($model, 'permissions')->checkboxList($permissionList, [
+ 'separator' => '<br>',
+]) ?>
+
+// Inline форма
+<?= $form->field($model, 'agree', ['options' => ['class' => 'form-check']])
+ ->checkbox(['class' => 'form-check-input']) ?>
+
+// Кастомный template
+<?= $form->field($model, 'price', [
+ 'template' => '<div class="input-group">{label}<span class="input-group-text">$</span>{input}{error}</div>',
+])->textInput() ?>
+
+// С hint
+<?= $form->field($model, 'password')
+ ->passwordInput()
+ ->hint('Минимум 8 символов') ?>
+
+// С label
+<?= $form->field($model, 'email')
+ ->input('email')
+ ->label('Электронная почта') ?>
+
+// Без label
+<?= $form->field($model, 'search', ['options' => ['class' => 'mb-0']])
+ ->textInput(['placeholder' => 'Поиск...'])
+ ->label(false) ?>
+```
+
+### AJAX валидация
+```php
+<?php
+// В контроллере
+public function actionCreate()
+{
+ $model = new User();
+
+ if (Yii::$app->request->isAjax && $model->load(Yii::$app->request->post())) {
+ Yii::$app->response->format = Response::FORMAT_JSON;
+ return ActiveForm::validate($model);
+ }
+
+ if ($model->load(Yii::$app->request->post()) && $model->save()) {
+ return $this->redirect(['view', 'id' => $model->id]);
+ }
+
+ return $this->render('create', ['model' => $model]);
+}
+
+// В view
+$form = ActiveForm::begin([
+ 'id' => 'user-form',
+ 'enableAjaxValidation' => true,
+ 'validationUrl' => ['validate'],
+]);
+```
+
+## Url Helper
+
+### Создание URL
+```php
+<?php
+use yii\helpers\Url;
+
+// Относительный URL
+echo Url::to(['site/index']); // /site/index
+echo Url::to(['user/view', 'id' => 1]); // /user/view?id=1
+echo Url::to(['post/index', 'page' => 2]); // /post/index?page=2
+
+// Абсолютный URL
+echo Url::to(['site/index'], true); // http://example.com/site/index
+echo Url::to(['site/index'], 'https'); // https://example.com/site/index
+
+// Текущий URL
+echo Url::to(''); // Текущий URL
+echo Url::current(); // Текущий URL
+echo Url::current(['page' => 2]); // Текущий URL + параметр
+
+// Внешний URL
+echo Url::to('https://example.com');
+
+// Домашняя страница
+echo Url::home();
+echo Url::home(true); // Абсолютный
+
+// Предыдущий URL
+echo Url::previous();
+
+// Запоминание URL
+Url::remember();
+Url::remember(['user/profile'], 'profile');
+echo Url::previous('profile');
+
+// Канонический URL
+$this->registerLinkTag(['rel' => 'canonical', 'href' => Url::canonical()]);
+
+// Base URL
+echo Url::base(); // /
+echo Url::base(true); // http://example.com
+```
+
+## Регистрация скриптов и стилей
+
+### В View
+```php
+<?php
+/** @var yii\web\View $this */
+
+// CSS файлы
+$this->registerCssFile('/css/custom.css', ['depends' => [\app\assets\AppAsset::class]]);
+$this->registerCssFile('https://cdn.example.com/lib.css');
+
+// Inline CSS
+$this->registerCss('.highlight { background: yellow; }');
+
+// JS файлы
+$this->registerJsFile('/js/custom.js', [
+ 'depends' => [\yii\web\JqueryAsset::class],
+ 'position' => \yii\web\View::POS_END,
+]);
+
+// Inline JS
+$this->registerJs('
+ $(function() {
+ $(".delete-btn").click(function() {
+ return confirm("Удалить?");
+ });
+ });
+', \yii\web\View::POS_READY);
+
+// JS в начале body
+$this->registerJs('console.log("loaded");', \yii\web\View::POS_BEGIN);
+
+// JS в конце body
+$this->registerJs('initApp();', \yii\web\View::POS_END);
+
+// Мета-теги
+$this->registerMetaTag(['name' => 'description', 'content' => 'Описание страницы']);
+$this->registerMetaTag(['name' => 'keywords', 'content' => 'ключевые, слова']);
+$this->registerMetaTag(['property' => 'og:title', 'content' => $this->title]);
+
+// Link теги
+$this->registerLinkTag(['rel' => 'alternate', 'hreflang' => 'en', 'href' => $enUrl]);
+```
+
+### Asset Bundles
+```php
+<?php
+// assets/AppAsset.php
+namespace app\assets;
+
+use yii\web\AssetBundle;
+
+class AppAsset extends AssetBundle
+{
+ public $basePath = '@webroot';
+ public $baseUrl = '@web';
+
+ public $css = [
+ 'css/site.css',
+ ];
+
+ public $js = [
+ 'js/app.js',
+ ];
+
+ public $depends = [
+ \yii\web\YiiAsset::class,
+ \yii\bootstrap5\BootstrapAsset::class,
+ ];
+
+ // Опции публикации
+ public $publishOptions = [
+ 'forceCopy' => YII_DEBUG,
+ ];
+}
+
+// В layout или view
+AppAsset::register($this);
+```
+
+## Блоки контента
+
+### Определение блоков
+```php
+<?php
+// В view
+$this->beginBlock('sidebar');
+?>
+<div class="sidebar">
+ <h3>Боковая панель</h3>
+ <ul>
+ <li>Пункт 1</li>
+ <li>Пункт 2</li>
+ </ul>
+</div>
+<?php
+$this->endBlock();
+?>
+
+// В layout
+<?php if (isset($this->blocks['sidebar'])): ?>
+ <aside><?= $this->blocks['sidebar'] ?></aside>
+<?php else: ?>
+ <aside><?= $this->render('_default_sidebar') ?></aside>
+<?php endif; ?>
+```
+
+### Content placeholders
+```php
+<?php
+// В layout
+<!DOCTYPE html>
+<html>
+<head>
+ <?php $this->beginContent('@app/views/layouts/_header.php'); ?>
+ <?php $this->endContent(); ?>
+</head>
+<body>
+ <?= $content ?>
+
+ <?php $this->beginContent('@app/views/layouts/_footer.php'); ?>
+ <?php $this->endContent(); ?>
+</body>
+</html>
+```
+
+## ViewRenderer и темы
+
+### Темы
+```php
+<?php
+// config/web.php
+return [
+ 'components' => [
+ 'view' => [
+ 'theme' => [
+ 'basePath' => '@app/themes/basic',
+ 'baseUrl' => '@web/themes/basic',
+ 'pathMap' => [
+ '@app/views' => '@app/themes/basic/views',
+ '@app/modules' => '@app/themes/basic/modules',
+ '@app/widgets' => '@app/themes/basic/widgets',
+ ],
+ ],
+ ],
+ ],
+];
+```
+
+### View Events
+```php
+<?php
+// В контроллере или config
+\Yii::$app->view->on(\yii\base\View::EVENT_BEGIN_PAGE, function ($event) {
+ // Перед началом страницы
+});
+
+\Yii::$app->view->on(\yii\base\View::EVENT_END_PAGE, function ($event) {
+ // После окончания страницы
+});
+
+\Yii::$app->view->on(\yii\base\View::EVENT_BEFORE_RENDER, function ($event) {
+ // Перед рендерингом view
+ $event->viewFile; // Путь к файлу
+});
+
+\Yii::$app->view->on(\yii\base\View::EVENT_AFTER_RENDER, function ($event) {
+ // После рендеринга
+ $event->output; // Результат рендеринга
+});
+```
+
+## Рекомендации для Claude Code
+
+1. **Escaping** - всегда используйте `Html::encode()` для пользовательских данных
+2. **Partials** - выносите повторяющиеся элементы в partials с префиксом `_`
+3. **Asset Bundles** - группируйте CSS/JS в asset bundles
+4. **PHPDoc** - документируйте переменные в начале view
+5. **Layouts** - используйте вложенные layouts для модульности
+6. **ActiveForm** - предпочитайте ActiveForm для форм с валидацией
--- /dev/null
+# Yii2 Routing - Маршрутизация
+
+## Конфигурация UrlManager
+
+### Базовая настройка
+```php
+<?php
+// config/web.php
+return [
+ 'components' => [
+ 'urlManager' => [
+ 'class' => \yii\web\UrlManager::class,
+ 'enablePrettyUrl' => true,
+ 'showScriptName' => false, // Убрать index.php из URL
+ 'enableStrictParsing' => false,
+ 'suffix' => '', // Суффикс URL (.html, /)
+ 'rules' => [
+ // Правила маршрутизации
+ ],
+ ],
+ ],
+];
+```
+
+### Nginx конфигурация
+```nginx
+location / {
+ try_files $uri $uri/ /index.php$is_args$args;
+}
+
+location ~ \.php$ {
+ include fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+ fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
+}
+
+location ~ /\.(ht|svn|git) {
+ deny all;
+}
+```
+
+## Правила маршрутизации
+
+### Базовые правила
+```php
+<?php
+'rules' => [
+ // Точное соответствие
+ '' => 'site/index', // / -> site/index
+ 'about' => 'site/about', // /about -> site/about
+
+ // С параметрами
+ 'post/<id:\d+>' => 'post/view', // /post/123 -> post/view?id=123
+ 'user/<username:\w+>' => 'user/profile', // /user/john -> user/profile?username=john
+
+ // Несколько параметров
+ 'posts/<year:\d{4}>/<month:\d{2}>' => 'post/archive', // /posts/2024/01
+
+ // Опциональные параметры
+ 'posts/<page:\d+>' => 'post/index',
+ 'posts' => 'post/index', // page необязателен
+
+ // Wildcard
+ 'doc/<path:.*>' => 'doc/view', // /doc/any/path/here
+],
+```
+
+### RESTful правила
+```php
+<?php
+'rules' => [
+ // Полный REST набор
+ [
+ 'class' => \yii\rest\UrlRule::class,
+ 'controller' => 'api/user',
+ 'pluralize' => true,
+ ],
+ // Создаёт:
+ // GET /users -> api/user/index
+ // GET /users/123 -> api/user/view?id=123
+ // POST /users -> api/user/create
+ // PUT /users/123 -> api/user/update?id=123
+ // DELETE /users/123 -> api/user/delete?id=123
+ // HEAD /users -> api/user/index
+ // HEAD /users/123 -> api/user/view?id=123
+ // OPTIONS /users -> api/user/options
+ // OPTIONS /users/123 -> api/user/options
+
+ // Несколько контроллеров
+ [
+ 'class' => \yii\rest\UrlRule::class,
+ 'controller' => ['api/user', 'api/post', 'api/comment'],
+ ],
+
+ // С кастомными actions
+ [
+ 'class' => \yii\rest\UrlRule::class,
+ 'controller' => 'api/user',
+ 'extraPatterns' => [
+ 'POST login' => 'login', // POST /users/login
+ 'GET me' => 'me', // GET /users/me
+ 'PUT {id}/activate' => 'activate', // PUT /users/123/activate
+ ],
+ ],
+
+ // Только определённые actions
+ [
+ 'class' => \yii\rest\UrlRule::class,
+ 'controller' => 'api/user',
+ 'only' => ['index', 'view'],
+ ],
+
+ // Исключая actions
+ [
+ 'class' => \yii\rest\UrlRule::class,
+ 'controller' => 'api/user',
+ 'except' => ['delete'],
+ ],
+],
+```
+
+### Правила с HTTP методами
+```php
+<?php
+'rules' => [
+ // Конкретный метод
+ 'POST users' => 'user/create',
+ 'PUT users/<id:\d+>' => 'user/update',
+ 'DELETE users/<id:\d+>' => 'user/delete',
+
+ // Массив с verb
+ [
+ 'pattern' => 'users/<id:\d+>',
+ 'route' => 'user/view',
+ 'verb' => ['GET', 'HEAD'],
+ ],
+
+ // Разные routes для разных методов
+ [
+ 'pattern' => 'users',
+ 'route' => 'user/index',
+ 'verb' => 'GET',
+ ],
+ [
+ 'pattern' => 'users',
+ 'route' => 'user/create',
+ 'verb' => 'POST',
+ ],
+],
+```
+
+### Правила с хостом
+```php
+<?php
+'rules' => [
+ // По поддомену
+ [
+ 'pattern' => '',
+ 'route' => 'admin/dashboard/index',
+ 'host' => 'admin.<domain>',
+ ],
+ [
+ 'pattern' => '',
+ 'route' => 'api/v1/default/index',
+ 'host' => 'api.<domain>',
+ ],
+
+ // Параметр из хоста
+ [
+ 'pattern' => '',
+ 'route' => 'site/index',
+ 'host' => '<subdomain:\w+>.<domain>',
+ ],
+],
+```
+
+## Группировка правил
+
+### GroupUrlRule
+```php
+<?php
+'rules' => [
+ // Группа для API v1
+ [
+ 'class' => \yii\web\GroupUrlRule::class,
+ 'prefix' => 'api/v1',
+ 'rules' => [
+ 'users' => 'user/index',
+ 'users/<id:\d+>' => 'user/view',
+ 'posts' => 'post/index',
+ 'posts/<id:\d+>' => 'post/view',
+ ],
+ ],
+ // api/v1/users -> api/v1/user/index
+
+ // Группа для админки
+ [
+ 'class' => \yii\web\GroupUrlRule::class,
+ 'prefix' => 'admin',
+ 'routePrefix' => 'admin',
+ 'rules' => [
+ '' => 'dashboard/index',
+ 'users' => 'user/index',
+ 'settings' => 'settings/index',
+ ],
+ ],
+ // admin/users -> admin/user/index
+],
+```
+
+### Модульные правила
+```php
+<?php
+// config/web.php
+return [
+ 'modules' => [
+ 'api' => [
+ 'class' => \app\modules\api\Module::class,
+ ],
+ 'admin' => [
+ 'class' => \app\modules\admin\Module::class,
+ ],
+ ],
+ 'components' => [
+ 'urlManager' => [
+ 'rules' => [
+ // Правила для модулей
+ [
+ 'class' => \yii\web\GroupUrlRule::class,
+ 'prefix' => 'api/v1',
+ 'routePrefix' => 'api/v1',
+ 'rules' => [
+ [
+ 'class' => \yii\rest\UrlRule::class,
+ 'controller' => 'user',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+];
+```
+
+## Кастомные правила
+
+### Класс UrlRule
+```php
+<?php
+// components/SlugUrlRule.php
+namespace app\components;
+
+use yii\web\UrlRule;
+use yii\web\UrlRuleInterface;
+use app\models\Post;
+
+class SlugUrlRule extends UrlRule implements UrlRuleInterface
+{
+ public function parseRequest($manager, $request): array|false
+ {
+ $pathInfo = $request->pathInfo;
+
+ // Проверяем формат URL
+ if (preg_match('/^post\/([a-z0-9-]+)$/i', $pathInfo, $matches)) {
+ $slug = $matches[1];
+
+ // Ищем пост по slug
+ $post = Post::find()->where(['slug' => $slug])->one();
+
+ if ($post !== null) {
+ return ['post/view', ['id' => $post->id]];
+ }
+ }
+
+ return false;
+ }
+
+ public function createUrl($manager, $route, $params): string|false
+ {
+ if ($route === 'post/view' && isset($params['id'])) {
+ $post = Post::findOne($params['id']);
+
+ if ($post !== null) {
+ return 'post/' . $post->slug;
+ }
+ }
+
+ return false;
+ }
+}
+
+// Использование
+'rules' => [
+ [
+ 'class' => \app\components\SlugUrlRule::class,
+ ],
+],
+```
+
+### Категории и вложенные URL
+```php
+<?php
+// components/CategoryUrlRule.php
+namespace app\components;
+
+use yii\base\BaseObject;
+use yii\web\UrlRuleInterface;
+use app\models\Category;
+
+class CategoryUrlRule extends BaseObject implements UrlRuleInterface
+{
+ public function parseRequest($manager, $request): array|false
+ {
+ $pathInfo = $request->pathInfo;
+
+ // catalog/electronics/phones -> category/view?path=electronics/phones
+ if (preg_match('/^catalog\/(.+)$/', $pathInfo, $matches)) {
+ $path = $matches[1];
+
+ $category = Category::find()
+ ->where(['path' => $path])
+ ->one();
+
+ if ($category) {
+ return ['category/view', ['id' => $category->id]];
+ }
+ }
+
+ return false;
+ }
+
+ public function createUrl($manager, $route, $params): string|false
+ {
+ if ($route === 'category/view' && isset($params['id'])) {
+ $category = Category::findOne($params['id']);
+
+ if ($category) {
+ return 'catalog/' . $category->path;
+ }
+ }
+
+ return false;
+ }
+}
+```
+
+## Создание URL
+
+### В контроллере
+```php
+<?php
+use yii\helpers\Url;
+
+class SiteController extends Controller
+{
+ public function actionExample(): \yii\web\Response
+ {
+ // Абсолютный URL
+ $url = Url::to(['user/view', 'id' => 1], true);
+ // https://example.com/user/view?id=1
+
+ // С протоколом
+ $url = Url::to(['user/view', 'id' => 1], 'https');
+
+ // Редирект
+ return $this->redirect(['user/profile', 'username' => 'john']);
+
+ // toRoute (аналог Url::to с массивом)
+ $url = Url::toRoute(['post/index', 'page' => 2]);
+ }
+}
+```
+
+### В представлениях
+```php
+<?php
+use yii\helpers\Url;
+use yii\helpers\Html;
+
+// Ссылки
+echo Html::a('Профиль', ['user/profile', 'id' => $user->id]);
+echo Html::a('Главная', Url::home());
+echo Html::a('Назад', Url::previous());
+
+// Формы
+echo Html::beginForm(['site/search'], 'get');
+echo Html::beginForm(Url::to(['user/update', 'id' => $id]), 'post');
+
+// Изображения с URL
+echo Html::img(Url::to('@web/images/logo.png', true));
+
+// Текущий URL с изменением параметров
+echo Url::current(['page' => 2]);
+echo Url::current(['sort' => 'name'], true);
+
+// Канонический URL
+$this->registerLinkTag(['rel' => 'canonical', 'href' => Url::canonical()]);
+```
+
+## Нормализация URL
+
+### UrlNormalizer
+```php
+<?php
+// config/web.php
+return [
+ 'components' => [
+ 'urlManager' => [
+ 'enablePrettyUrl' => true,
+ 'showScriptName' => false,
+ 'normalizer' => [
+ 'class' => \yii\web\UrlNormalizer::class,
+ // Нормализация trailing slash
+ 'normalizeTrailingSlash' => true,
+ // Действие: REDIRECT_TEMPORARY (302) или REDIRECT_PERMANENT (301)
+ 'action' => \yii\web\UrlNormalizer::ACTION_REDIRECT_PERMANENT,
+ ],
+ 'rules' => [
+ // ...
+ ],
+ ],
+ ],
+];
+```
+
+### Отключение нормализации для правил
+```php
+<?php
+'rules' => [
+ [
+ 'pattern' => 'api/<action>',
+ 'route' => 'api/default/<action>',
+ 'normalizer' => false, // Отключить нормализацию
+ ],
+ [
+ 'pattern' => 'legacy/<path:.*>',
+ 'route' => 'legacy/redirect',
+ 'normalizer' => [
+ 'normalizeTrailingSlash' => false,
+ ],
+ ],
+],
+```
+
+## Кэширование правил
+
+```php
+<?php
+// config/web.php
+return [
+ 'components' => [
+ 'urlManager' => [
+ 'enablePrettyUrl' => true,
+ 'showScriptName' => false,
+ 'cache' => 'cache', // Использовать компонент cache
+ 'rules' => [
+ // Правила кэшируются
+ ],
+ ],
+ ],
+];
+
+// Очистка кэша правил
+Yii::$app->urlManager->cache->delete('UrlManager');
+```
+
+## Языковые URL
+
+### Локализованные URL
+```php
+<?php
+// components/LocaleUrlManager.php
+namespace app\components;
+
+use yii\web\UrlManager;
+
+class LocaleUrlManager extends UrlManager
+{
+ public array $languages = ['ru', 'en', 'de'];
+ public string $defaultLanguage = 'ru';
+
+ public function createUrl($params): string
+ {
+ $url = parent::createUrl($params);
+
+ $language = \Yii::$app->language;
+
+ if ($language !== $this->defaultLanguage) {
+ $url = '/' . substr($language, 0, 2) . $url;
+ }
+
+ return $url;
+ }
+
+ public function parseRequest($request): array|false
+ {
+ $pathInfo = $request->pathInfo;
+
+ // Извлекаем язык из URL
+ if (preg_match('/^(en|de)\/(.*)$/', $pathInfo, $matches)) {
+ \Yii::$app->language = $matches[1];
+ $request->pathInfo = $matches[2];
+ } else {
+ \Yii::$app->language = $this->defaultLanguage;
+ }
+
+ return parent::parseRequest($request);
+ }
+}
+
+// config/web.php
+return [
+ 'components' => [
+ 'urlManager' => [
+ 'class' => \app\components\LocaleUrlManager::class,
+ 'enablePrettyUrl' => true,
+ 'showScriptName' => false,
+ 'languages' => ['ru', 'en', 'de'],
+ 'defaultLanguage' => 'ru',
+ ],
+ ],
+];
+```
+
+### hreflang теги
+```php
+<?php
+// В layout или view
+$languages = ['ru' => 'ru-RU', 'en' => 'en-US', 'de' => 'de-DE'];
+
+foreach ($languages as $lang => $locale) {
+ $this->registerLinkTag([
+ 'rel' => 'alternate',
+ 'hreflang' => $locale,
+ 'href' => Url::to(array_merge(
+ [$this->context->route],
+ Yii::$app->request->queryParams,
+ ['language' => $lang]
+ ), true),
+ ]);
+}
+```
+
+## Версионирование API
+
+```php
+<?php
+// config/web.php
+return [
+ 'components' => [
+ 'urlManager' => [
+ 'rules' => [
+ // API v1
+ [
+ 'class' => \yii\rest\UrlRule::class,
+ 'controller' => ['v1/user' => 'api/v1/user'],
+ 'prefix' => 'api',
+ ],
+ // API v2
+ [
+ 'class' => \yii\rest\UrlRule::class,
+ 'controller' => ['v2/user' => 'api/v2/user'],
+ 'prefix' => 'api',
+ ],
+ ],
+ ],
+ ],
+];
+
+// Или через модули
+'modules' => [
+ 'v1' => [
+ 'class' => \app\modules\api\v1\Module::class,
+ ],
+ 'v2' => [
+ 'class' => \app\modules\api\v2\Module::class,
+ ],
+],
+```
+
+## Отладка маршрутов
+
+```php
+<?php
+// Посмотреть все правила
+$rules = Yii::$app->urlManager->rules;
+foreach ($rules as $rule) {
+ echo get_class($rule) . ': ' . $rule->name . "\n";
+}
+
+// Проверить парсинг URL
+$request = new \yii\web\Request();
+$request->pathInfo = 'user/123';
+$result = Yii::$app->urlManager->parseRequest($request);
+var_dump($result); // ['user/view', ['id' => '123']]
+
+// Проверить создание URL
+$url = Yii::$app->urlManager->createUrl(['user/view', 'id' => 123]);
+echo $url; // /user/123
+```
+
+## Рекомендации для Claude Code
+
+1. **Pretty URLs** - всегда включайте enablePrettyUrl для чистых URL
+2. **REST правила** - используйте `yii\rest\UrlRule` для API
+3. **Группировка** - используйте `GroupUrlRule` для модулей
+4. **Кэширование** - включайте кэш для production
+5. **Нормализация** - используйте UrlNormalizer для консистентности
+6. **Версионирование** - добавляйте версию в API URL (/api/v1/)
--- /dev/null
+# Yii2 Migrations - Миграции базы данных
+
+## Основы миграций
+
+### Создание миграции
+```bash
+# Базовая миграция
+php yii migrate/create create_user_table
+
+# С namespace
+php yii migrate/create app\\migrations\\CreateUserTable --namespace=app\\migrations
+
+# В модуле
+php yii migrate/create create_order_table --migrationPath=@app/modules/shop/migrations
+```
+
+### Структура файла миграции
+```php
+<?php
+// migrations/m241201_120000_create_user_table.php
+use yii\db\Migration;
+
+class m241201_120000_create_user_table extends Migration
+{
+ public function safeUp(): bool
+ {
+ $this->createTable('{{%users}}', [
+ 'id' => $this->primaryKey(),
+ 'username' => $this->string(255)->notNull()->unique(),
+ 'email' => $this->string(255)->notNull()->unique(),
+ 'password_hash' => $this->string(255)->notNull(),
+ 'status' => $this->smallInteger()->notNull()->defaultValue(0),
+ 'created_at' => $this->integer()->notNull(),
+ 'updated_at' => $this->integer()->notNull(),
+ ]);
+
+ $this->createIndex('idx-users-status', '{{%users}}', 'status');
+
+ return true;
+ }
+
+ public function safeDown(): bool
+ {
+ $this->dropTable('{{%users}}');
+
+ return true;
+ }
+}
+```
+
+## Типы колонок
+
+### Базовые типы
+```php
+<?php
+public function safeUp(): bool
+{
+ $this->createTable('{{%products}}', [
+ // Целые числа
+ 'id' => $this->primaryKey(),
+ 'tiny_int' => $this->tinyInteger(), // TINYINT
+ 'small_int' => $this->smallInteger(), // SMALLINT
+ 'medium_int' => $this->integer(), // INTEGER
+ 'big_int' => $this->bigInteger(), // BIGINT
+
+ // С размером
+ 'count' => $this->integer(11)->notNull(),
+
+ // Auto increment
+ 'id2' => $this->bigPrimaryKey(), // BIGSERIAL
+
+ // Числа с плавающей точкой
+ 'price' => $this->decimal(10, 2), // DECIMAL(10,2)
+ 'rating' => $this->float(), // FLOAT
+ 'score' => $this->double(), // DOUBLE
+
+ // Денежные
+ 'amount' => $this->money(10, 2), // DECIMAL(10,2)
+
+ // Строки
+ 'code' => $this->char(10), // CHAR(10)
+ 'name' => $this->string(255), // VARCHAR(255)
+ 'description' => $this->text(), // TEXT
+
+ // Дата и время
+ 'birth_date' => $this->date(), // DATE
+ 'start_time' => $this->time(), // TIME
+ 'created_at' => $this->dateTime(), // DATETIME
+ 'updated_at' => $this->timestamp(), // TIMESTAMP
+
+ // Бинарные
+ 'data' => $this->binary(), // BLOB
+
+ // Булевы
+ 'is_active' => $this->boolean(), // BOOLEAN
+
+ // JSON (PostgreSQL, MySQL 5.7+)
+ 'metadata' => $this->json(), // JSON
+
+ // UUID (PostgreSQL)
+ 'uuid' => 'uuid DEFAULT gen_random_uuid()',
+ ]);
+
+ return true;
+}
+```
+
+### Модификаторы колонок
+```php
+<?php
+$this->createTable('{{%orders}}', [
+ 'id' => $this->primaryKey(),
+
+ // NOT NULL
+ 'user_id' => $this->integer()->notNull(),
+
+ // DEFAULT
+ 'status' => $this->smallInteger()->notNull()->defaultValue(0),
+ 'created_at' => $this->timestamp()->defaultExpression('CURRENT_TIMESTAMP'),
+
+ // UNIQUE
+ 'order_number' => $this->string(50)->notNull()->unique(),
+
+ // COMMENT
+ 'notes' => $this->text()->comment('Примечания к заказу'),
+
+ // NULL
+ 'deleted_at' => $this->timestamp()->null(),
+
+ // UNSIGNED (MySQL)
+ 'quantity' => $this->integer()->unsigned()->notNull(),
+
+ // Комбинации
+ 'email' => $this->string(255)
+ ->notNull()
+ ->unique()
+ ->comment('Email пользователя'),
+]);
+```
+
+## Работа с таблицами
+
+### Создание таблицы
+```php
+<?php
+public function safeUp(): bool
+{
+ // Простое создание
+ $this->createTable('{{%posts}}', [
+ 'id' => $this->primaryKey(),
+ 'title' => $this->string(255)->notNull(),
+ 'content' => $this->text(),
+ 'user_id' => $this->integer()->notNull(),
+ 'created_at' => $this->timestamp()->defaultExpression('CURRENT_TIMESTAMP'),
+ ]);
+
+ // С опциями таблицы (MySQL)
+ $tableOptions = null;
+ if ($this->db->driverName === 'mysql') {
+ $tableOptions = 'CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB';
+ }
+
+ $this->createTable('{{%comments}}', [
+ 'id' => $this->primaryKey(),
+ 'content' => $this->text()->notNull(),
+ ], $tableOptions);
+
+ return true;
+}
+```
+
+### Изменение таблицы
+```php
+<?php
+public function safeUp(): bool
+{
+ // Добавление колонки
+ $this->addColumn('{{%users}}', 'phone', $this->string(20)->after('email'));
+
+ // Добавление колонки в начало (MySQL)
+ $this->addColumn('{{%users}}', 'uuid', $this->string(36)->first());
+
+ // Удаление колонки
+ $this->dropColumn('{{%users}}', 'old_field');
+
+ // Переименование колонки
+ $this->renameColumn('{{%users}}', 'name', 'full_name');
+
+ // Изменение типа колонки
+ $this->alterColumn('{{%users}}', 'status', $this->integer()->notNull()->defaultValue(1));
+
+ // Переименование таблицы
+ $this->renameTable('{{%old_name}}', '{{%new_name}}');
+
+ // Удаление таблицы
+ $this->dropTable('{{%temp_table}}');
+
+ // Очистка таблицы
+ $this->truncateTable('{{%logs}}');
+
+ return true;
+}
+```
+
+## Индексы
+
+### Создание индексов
+```php
+<?php
+public function safeUp(): bool
+{
+ // Обычный индекс
+ $this->createIndex(
+ 'idx-posts-user_id',
+ '{{%posts}}',
+ 'user_id'
+ );
+
+ // Уникальный индекс
+ $this->createIndex(
+ 'idx-users-email',
+ '{{%users}}',
+ 'email',
+ true // unique
+ );
+
+ // Составной индекс
+ $this->createIndex(
+ 'idx-posts-user_status',
+ '{{%posts}}',
+ ['user_id', 'status']
+ );
+
+ // Частичный индекс (PostgreSQL)
+ $this->execute('CREATE INDEX idx_active_users ON {{%users}} (email) WHERE status = 1');
+
+ // GIN индекс для JSONB (PostgreSQL)
+ $this->execute('CREATE INDEX idx_metadata ON {{%products}} USING GIN (metadata)');
+
+ // Полнотекстовый индекс (PostgreSQL)
+ $this->execute('CREATE INDEX idx_search ON {{%posts}} USING GIN (to_tsvector(\'russian\', title || \' \' || content))');
+
+ return true;
+}
+
+public function safeDown(): bool
+{
+ $this->dropIndex('idx-posts-user_id', '{{%posts}}');
+ $this->dropIndex('idx-users-email', '{{%users}}');
+ $this->dropIndex('idx-posts-user_status', '{{%posts}}');
+
+ return true;
+}
+```
+
+## Внешние ключи
+
+### Создание FK
+```php
+<?php
+public function safeUp(): bool
+{
+ $this->createTable('{{%posts}}', [
+ 'id' => $this->primaryKey(),
+ 'user_id' => $this->integer()->notNull(),
+ 'category_id' => $this->integer(),
+ 'title' => $this->string(255)->notNull(),
+ ]);
+
+ // Внешний ключ с каскадным удалением
+ $this->addForeignKey(
+ 'fk-posts-user_id', // Имя ключа
+ '{{%posts}}', // Таблица
+ 'user_id', // Колонка
+ '{{%users}}', // Связанная таблица
+ 'id', // Связанная колонка
+ 'CASCADE', // ON DELETE
+ 'CASCADE' // ON UPDATE
+ );
+
+ // FK с SET NULL при удалении
+ $this->addForeignKey(
+ 'fk-posts-category_id',
+ '{{%posts}}',
+ 'category_id',
+ '{{%categories}}',
+ 'id',
+ 'SET NULL',
+ 'CASCADE'
+ );
+
+ // FK с RESTRICT
+ $this->addForeignKey(
+ 'fk-orders-user_id',
+ '{{%orders}}',
+ 'user_id',
+ '{{%users}}',
+ 'id',
+ 'RESTRICT',
+ 'RESTRICT'
+ );
+
+ return true;
+}
+
+public function safeDown(): bool
+{
+ // Сначала удаляем FK, потом таблицу
+ $this->dropForeignKey('fk-posts-user_id', '{{%posts}}');
+ $this->dropForeignKey('fk-posts-category_id', '{{%posts}}');
+ $this->dropTable('{{%posts}}');
+
+ return true;
+}
+```
+
+## Данные
+
+### Вставка данных
+```php
+<?php
+public function safeUp(): bool
+{
+ // Одна запись
+ $this->insert('{{%users}}', [
+ 'username' => 'admin',
+ 'email' => 'admin@example.com',
+ 'password_hash' => Yii::$app->security->generatePasswordHash('admin'),
+ 'status' => 1,
+ 'created_at' => time(),
+ 'updated_at' => time(),
+ ]);
+
+ // Массовая вставка
+ $this->batchInsert('{{%roles}}', ['name', 'description'], [
+ ['admin', 'Администратор'],
+ ['moderator', 'Модератор'],
+ ['user', 'Пользователь'],
+ ]);
+
+ return true;
+}
+```
+
+### Обновление и удаление
+```php
+<?php
+public function safeUp(): bool
+{
+ // Обновление
+ $this->update(
+ '{{%users}}',
+ ['status' => 1], // Новые значения
+ ['status' => 0] // Условие
+ );
+
+ // Удаление
+ $this->delete('{{%users}}', ['status' => -1]);
+
+ // Upsert (INSERT ON CONFLICT)
+ $this->upsert(
+ '{{%settings}}',
+ [
+ 'key' => 'site_name',
+ 'value' => 'My Site',
+ ],
+ [
+ 'value' => 'My Site', // UPDATE при конфликте
+ ]
+ );
+
+ return true;
+}
+```
+
+## Raw SQL
+
+### Выполнение SQL
+```php
+<?php
+public function safeUp(): bool
+{
+ // Простой SQL
+ $this->execute('ALTER TABLE {{%users}} ADD COLUMN age INTEGER');
+
+ // С параметрами (PostgreSQL специфичный синтаксис)
+ $this->execute('
+ CREATE OR REPLACE FUNCTION update_modified_column()
+ RETURNS TRIGGER AS $$
+ BEGIN
+ NEW.updated_at = NOW();
+ RETURN NEW;
+ END;
+ $$ language \'plpgsql\'
+ ');
+
+ // Триггер
+ $this->execute('
+ CREATE TRIGGER update_users_modtime
+ BEFORE UPDATE ON {{%users}}
+ FOR EACH ROW
+ EXECUTE FUNCTION update_modified_column()
+ ');
+
+ // Представление
+ $this->execute('
+ CREATE OR REPLACE VIEW {{%active_users}} AS
+ SELECT * FROM {{%users}} WHERE status = 1
+ ');
+
+ // Материализованное представление (PostgreSQL)
+ $this->execute('
+ CREATE MATERIALIZED VIEW {{%user_stats}} AS
+ SELECT user_id, COUNT(*) as posts_count
+ FROM {{%posts}}
+ GROUP BY user_id
+ ');
+
+ return true;
+}
+
+public function safeDown(): bool
+{
+ $this->execute('DROP TRIGGER IF EXISTS update_users_modtime ON {{%users}}');
+ $this->execute('DROP FUNCTION IF EXISTS update_modified_column()');
+ $this->execute('DROP VIEW IF EXISTS {{%active_users}}');
+ $this->execute('DROP MATERIALIZED VIEW IF EXISTS {{%user_stats}}');
+
+ return true;
+}
+```
+
+## Транзакции
+
+### Безопасные миграции
+```php
+<?php
+// safeUp/safeDown автоматически оборачиваются в транзакцию
+public function safeUp(): bool
+{
+ $this->createTable('{{%orders}}', [
+ 'id' => $this->primaryKey(),
+ // ...
+ ]);
+
+ $this->createTable('{{%order_items}}', [
+ 'id' => $this->primaryKey(),
+ // ...
+ ]);
+
+ // Если что-то упадёт - откатится всё
+ return true;
+}
+
+// up/down - без транзакции
+public function up(): void
+{
+ // Для операций, которые нельзя в транзакции
+ // (CREATE INDEX CONCURRENTLY в PostgreSQL)
+}
+```
+
+### Ручное управление транзакциями
+```php
+<?php
+public function up(): void
+{
+ // Операция вне транзакции
+ $this->execute('CREATE INDEX CONCURRENTLY idx_large ON {{%large_table}} (column)');
+
+ // Операции в транзакции
+ $transaction = $this->db->beginTransaction();
+ try {
+ $this->insert('{{%settings}}', ['key' => 'value']);
+ $this->update('{{%config}}', ['updated' => true]);
+ $transaction->commit();
+ } catch (\Exception $e) {
+ $transaction->rollBack();
+ throw $e;
+ }
+}
+```
+
+## Команды миграций
+
+### Основные команды
+```bash
+# Применить все новые миграции
+php yii migrate
+
+# Применить N миграций
+php yii migrate 3
+
+# Откатить последнюю миграцию
+php yii migrate/down
+
+# Откатить N миграций
+php yii migrate/down 3
+
+# Откатить все миграции
+php yii migrate/down all
+
+# Переприменить последние N миграций
+php yii migrate/redo 3
+
+# Показать статус миграций
+php yii migrate/history
+php yii migrate/history 10
+
+# Показать непримененные миграции
+php yii migrate/new
+php yii migrate/new 10
+
+# Пометить миграцию как примененную (без выполнения)
+php yii migrate/mark m241201_120000_create_user_table
+
+# Применить без подтверждения
+php yii migrate --interactive=0
+```
+
+### Namespace миграции
+```bash
+# Создать миграцию с namespace
+php yii migrate/create create_user_table --namespace=app\\migrations
+
+# Применить миграции из namespace
+php yii migrate --migrationNamespaces=app\\migrations
+
+# config/console.php
+return [
+ 'controllerMap' => [
+ 'migrate' => [
+ 'class' => \yii\console\controllers\MigrateController::class,
+ 'migrationNamespaces' => [
+ 'app\migrations',
+ 'app\modules\user\migrations',
+ ],
+ ],
+ ],
+];
+```
+
+## Множественные базы данных
+
+### Миграции для разных БД
+```php
+<?php
+// config/console.php
+return [
+ 'controllerMap' => [
+ 'migrate' => [
+ 'class' => \yii\console\controllers\MigrateController::class,
+ 'migrationPath' => '@app/migrations',
+ 'db' => 'db',
+ ],
+ 'migrate-analytics' => [
+ 'class' => \yii\console\controllers\MigrateController::class,
+ 'migrationPath' => '@app/migrations/analytics',
+ 'migrationTable' => '{{%migration_analytics}}',
+ 'db' => 'dbAnalytics',
+ ],
+ ],
+];
+
+// Использование
+// php yii migrate - основная БД
+// php yii migrate-analytics - аналитика
+```
+
+## Best Practices
+
+### Именование
+```php
+<?php
+// ПРАВИЛЬНО - описательные имена
+m241201_120000_create_user_table
+m241201_120001_add_email_to_users
+m241201_120002_create_posts_table
+m241201_120003_add_foreign_key_to_posts
+
+// НЕПРАВИЛЬНО
+m241201_120000_migration1
+m241201_120001_update
+```
+
+### Идемпотентность
+```php
+<?php
+public function safeUp(): bool
+{
+ // Проверка существования таблицы
+ if ($this->db->schema->getTableSchema('{{%users}}') === null) {
+ $this->createTable('{{%users}}', [
+ 'id' => $this->primaryKey(),
+ // ...
+ ]);
+ }
+
+ // Проверка существования колонки
+ $table = $this->db->schema->getTableSchema('{{%users}}');
+ if (!isset($table->columns['phone'])) {
+ $this->addColumn('{{%users}}', 'phone', $this->string(20));
+ }
+
+ // Проверка существования индекса
+ $indexes = $this->db->schema->findIndexes('{{%users}}');
+ if (!isset($indexes['idx-users-email'])) {
+ $this->createIndex('idx-users-email', '{{%users}}', 'email', true);
+ }
+
+ return true;
+}
+```
+
+### Большие миграции
+```php
+<?php
+public function safeUp(): bool
+{
+ // Для больших таблиц - batch обработка
+ $batchSize = 1000;
+ $offset = 0;
+
+ while (true) {
+ $rows = (new \yii\db\Query())
+ ->from('{{%old_table}}')
+ ->limit($batchSize)
+ ->offset($offset)
+ ->all();
+
+ if (empty($rows)) {
+ break;
+ }
+
+ $this->batchInsert('{{%new_table}}', array_keys($rows[0]), $rows);
+ $offset += $batchSize;
+ }
+
+ return true;
+}
+```
+
+## Рекомендации для Claude Code
+
+1. **Используйте safeUp/safeDown** - для автоматических транзакций
+2. **Описательные имена** - миграция должна описывать что она делает
+3. **Обратимость** - всегда реализуйте safeDown для отката
+4. **Префикс таблиц** - используйте `{{%table}}` для поддержки префиксов
+5. **Индексы для FK** - всегда создавайте индексы для внешних ключей
+6. **Атомарность** - одна миграция = одно логическое изменение
--- /dev/null
+# Yii2 Testing - Тестирование
+
+## Настройка окружения
+
+### Codeception конфигурация
+```yaml
+# codeception.yml
+namespace: tests
+actor_suffix: Tester
+paths:
+ tests: tests
+ output: tests/_output
+ data: tests/_data
+ support: tests/_support
+settings:
+ shuffle: false
+ lint: true
+bootstrap: _bootstrap.php
+params:
+ - tests/params.php
+```
+
+### Конфигурация unit тестов
+```yaml
+# tests/unit.suite.yml
+suite_namespace: tests\unit
+actor: UnitTester
+modules:
+ enabled:
+ - Yii2:
+ part: [orm, fixtures]
+ configFile: 'config/test.php'
+ - Asserts
+```
+
+### Тестовая конфигурация приложения
+```php
+<?php
+// config/test.php
+$params = require __DIR__ . '/params.php';
+$db = require __DIR__ . '/test_db.php';
+
+return [
+ 'id' => 'basic-tests',
+ 'basePath' => dirname(__DIR__),
+ 'aliases' => [
+ '@bower' => '@vendor/bower-asset',
+ '@npm' => '@vendor/npm-asset',
+ ],
+ 'language' => 'en-US',
+ 'components' => [
+ 'db' => $db,
+ 'mailer' => [
+ 'class' => \yii\symfonymailer\Mailer::class,
+ 'useFileTransport' => true,
+ ],
+ 'urlManager' => [
+ 'showScriptName' => true,
+ ],
+ 'user' => [
+ 'identityClass' => \app\models\User::class,
+ ],
+ 'request' => [
+ 'cookieValidationKey' => 'test',
+ 'enableCsrfValidation' => false,
+ ],
+ ],
+ 'params' => $params,
+];
+```
+
+## Unit тесты
+
+### Базовый unit тест
+```php
+<?php
+// tests/unit/models/UserTest.php
+namespace tests\unit\models;
+
+use app\models\User;
+use Codeception\Test\Unit;
+
+class UserTest extends Unit
+{
+ protected UnitTester $tester;
+
+ // Вызывается перед каждым тестом
+ protected function _before(): void
+ {
+ // Подготовка
+ }
+
+ // Вызывается после каждого теста
+ protected function _after(): void
+ {
+ // Очистка
+ }
+
+ public function testValidation(): void
+ {
+ $user = new User();
+
+ // Пустая модель невалидна
+ $this->assertFalse($user->validate());
+ $this->assertArrayHasKey('email', $user->errors);
+ $this->assertArrayHasKey('username', $user->errors);
+
+ // Заполняем обязательные поля
+ $user->email = 'test@example.com';
+ $user->username = 'testuser';
+ $user->password = 'password123';
+
+ $this->assertTrue($user->validate());
+ }
+
+ public function testEmailValidation(): void
+ {
+ $user = new User();
+ $user->username = 'testuser';
+ $user->password = 'password123';
+
+ // Невалидный email
+ $user->email = 'invalid-email';
+ $this->assertFalse($user->validate(['email']));
+
+ // Валидный email
+ $user->email = 'test@example.com';
+ $this->assertTrue($user->validate(['email']));
+ }
+
+ public function testPasswordHashing(): void
+ {
+ $user = new User();
+ $user->setPassword('mypassword');
+
+ $this->assertNotEmpty($user->password_hash);
+ $this->assertTrue($user->validatePassword('mypassword'));
+ $this->assertFalse($user->validatePassword('wrongpassword'));
+ }
+
+ /**
+ * @dataProvider statusProvider
+ */
+ public function testStatusValidation(int $status, bool $expected): void
+ {
+ $user = new User([
+ 'email' => 'test@example.com',
+ 'username' => 'testuser',
+ 'status' => $status,
+ ]);
+
+ $this->assertEquals($expected, $user->validate(['status']));
+ }
+
+ public static function statusProvider(): array
+ {
+ return [
+ 'active status' => [User::STATUS_ACTIVE, true],
+ 'inactive status' => [User::STATUS_INACTIVE, true],
+ 'invalid status' => [999, false],
+ ];
+ }
+}
+```
+
+### Тестирование с fixtures
+```php
+<?php
+// tests/unit/models/PostTest.php
+namespace tests\unit\models;
+
+use app\models\Post;
+use app\models\User;
+use tests\fixtures\PostFixture;
+use tests\fixtures\UserFixture;
+use Codeception\Test\Unit;
+
+class PostTest extends Unit
+{
+ public function _fixtures(): array
+ {
+ return [
+ 'users' => UserFixture::class,
+ 'posts' => PostFixture::class,
+ ];
+ }
+
+ public function testFindPublished(): void
+ {
+ $posts = Post::find()->published()->all();
+
+ $this->assertCount(2, $posts);
+ foreach ($posts as $post) {
+ $this->assertEquals(Post::STATUS_PUBLISHED, $post->status);
+ }
+ }
+
+ public function testBelongsToUser(): void
+ {
+ /** @var Post $post */
+ $post = $this->tester->grabFixture('posts', 'post1');
+
+ $this->assertInstanceOf(User::class, $post->user);
+ $this->assertEquals('admin', $post->user->username);
+ }
+}
+```
+
+### Fixture файлы
+```php
+<?php
+// tests/fixtures/UserFixture.php
+namespace tests\fixtures;
+
+use yii\test\ActiveFixture;
+
+class UserFixture extends ActiveFixture
+{
+ public $modelClass = \app\models\User::class;
+ public $dataFile = '@tests/_data/user.php';
+ public $depends = [];
+}
+
+// tests/_data/user.php
+return [
+ 'admin' => [
+ 'id' => 1,
+ 'username' => 'admin',
+ 'email' => 'admin@example.com',
+ 'password_hash' => '$2y$13$...',
+ 'status' => 1,
+ 'created_at' => 1234567890,
+ 'updated_at' => 1234567890,
+ ],
+ 'user' => [
+ 'id' => 2,
+ 'username' => 'user',
+ 'email' => 'user@example.com',
+ 'password_hash' => '$2y$13$...',
+ 'status' => 1,
+ 'created_at' => 1234567890,
+ 'updated_at' => 1234567890,
+ ],
+];
+```
+
+## Функциональные тесты
+
+### Тестирование контроллеров
+```php
+<?php
+// tests/functional/SiteCest.php
+namespace tests\functional;
+
+use tests\FunctionalTester;
+
+class SiteCest
+{
+ public function _before(FunctionalTester $I): void
+ {
+ // Перед каждым тестом
+ }
+
+ public function checkHomePage(FunctionalTester $I): void
+ {
+ $I->amOnPage('/');
+ $I->see('Welcome');
+ $I->seeResponseCodeIs(200);
+ }
+
+ public function checkAboutPage(FunctionalTester $I): void
+ {
+ $I->amOnRoute('site/about');
+ $I->see('About');
+ $I->seeResponseCodeIs(200);
+ }
+
+ public function checkLoginForm(FunctionalTester $I): void
+ {
+ $I->amOnRoute('site/login');
+ $I->see('Login', 'h1');
+ $I->seeElement('input', ['name' => 'LoginForm[username]']);
+ $I->seeElement('input', ['name' => 'LoginForm[password]']);
+ }
+
+ public function checkLoginWithEmptyCredentials(FunctionalTester $I): void
+ {
+ $I->amOnRoute('site/login');
+ $I->submitForm('#login-form', []);
+ $I->seeValidationError('Username cannot be blank.');
+ $I->seeValidationError('Password cannot be blank.');
+ }
+
+ public function checkLoginWithWrongCredentials(FunctionalTester $I): void
+ {
+ $I->amOnRoute('site/login');
+ $I->submitForm('#login-form', [
+ 'LoginForm[username]' => 'wrong',
+ 'LoginForm[password]' => 'wrong',
+ ]);
+ $I->seeValidationError('Incorrect username or password.');
+ }
+}
+```
+
+### Тестирование форм
+```php
+<?php
+// tests/functional/ContactCest.php
+namespace tests\functional;
+
+use tests\FunctionalTester;
+
+class ContactCest
+{
+ public function _before(FunctionalTester $I): void
+ {
+ $I->amOnRoute('site/contact');
+ }
+
+ public function checkContactFormSubmit(FunctionalTester $I): void
+ {
+ $I->submitForm('#contact-form', [
+ 'ContactForm[name]' => 'John Doe',
+ 'ContactForm[email]' => 'john@example.com',
+ 'ContactForm[subject]' => 'Test Subject',
+ 'ContactForm[body]' => 'Test message body',
+ 'ContactForm[verifyCode]' => 'testme',
+ ]);
+
+ $I->seeEmailIsSent();
+ $I->see('Thank you for contacting us');
+ }
+
+ public function checkContactFormValidation(FunctionalTester $I): void
+ {
+ $I->submitForm('#contact-form', [
+ 'ContactForm[name]' => '',
+ 'ContactForm[email]' => 'invalid-email',
+ 'ContactForm[subject]' => '',
+ 'ContactForm[body]' => '',
+ ]);
+
+ $I->seeValidationError('Name cannot be blank.');
+ $I->seeValidationError('Email is not a valid email address.');
+ $I->seeValidationError('Subject cannot be blank.');
+ $I->seeValidationError('Body cannot be blank.');
+ }
+}
+```
+
+### Тестирование с аутентификацией
+```php
+<?php
+// tests/functional/UserCest.php
+namespace tests\functional;
+
+use app\models\User;
+use tests\FunctionalTester;
+use tests\fixtures\UserFixture;
+
+class UserCest
+{
+ public function _fixtures(): array
+ {
+ return [
+ 'users' => UserFixture::class,
+ ];
+ }
+
+ public function checkProfilePageRequiresAuth(FunctionalTester $I): void
+ {
+ $I->amOnRoute('user/profile');
+ $I->seeCurrentUrlEquals('/site/login');
+ }
+
+ public function checkProfilePageAsUser(FunctionalTester $I): void
+ {
+ // Логин пользователя
+ $I->amLoggedInAs(User::findOne(['username' => 'user']));
+
+ $I->amOnRoute('user/profile');
+ $I->see('Profile');
+ $I->see('user@example.com');
+ }
+
+ public function checkAdminPageAccessDenied(FunctionalTester $I): void
+ {
+ $I->amLoggedInAs(User::findOne(['username' => 'user']));
+
+ $I->amOnRoute('admin/dashboard');
+ $I->seeResponseCodeIs(403);
+ }
+
+ public function checkAdminPageAsAdmin(FunctionalTester $I): void
+ {
+ $I->amLoggedInAs(User::findOne(['username' => 'admin']));
+
+ $I->amOnRoute('admin/dashboard');
+ $I->seeResponseCodeIs(200);
+ $I->see('Dashboard');
+ }
+}
+```
+
+## API тесты
+
+### Тестирование REST API
+```php
+<?php
+// tests/api/UserCest.php
+namespace tests\api;
+
+use tests\ApiTester;
+use tests\fixtures\UserFixture;
+use Codeception\Util\HttpCode;
+
+class UserCest
+{
+ public function _fixtures(): array
+ {
+ return [
+ 'users' => UserFixture::class,
+ ];
+ }
+
+ public function getUsers(ApiTester $I): void
+ {
+ $I->sendGet('/api/users');
+ $I->seeResponseCodeIs(HttpCode::OK);
+ $I->seeResponseIsJson();
+ $I->seeResponseMatchesJsonType([
+ 'id' => 'integer',
+ 'username' => 'string',
+ 'email' => 'string:email',
+ ]);
+ }
+
+ public function getUser(ApiTester $I): void
+ {
+ $I->sendGet('/api/users/1');
+ $I->seeResponseCodeIs(HttpCode::OK);
+ $I->seeResponseContainsJson([
+ 'id' => 1,
+ 'username' => 'admin',
+ ]);
+ }
+
+ public function getUserNotFound(ApiTester $I): void
+ {
+ $I->sendGet('/api/users/999');
+ $I->seeResponseCodeIs(HttpCode::NOT_FOUND);
+ }
+
+ public function createUser(ApiTester $I): void
+ {
+ $I->haveHttpHeader('Content-Type', 'application/json');
+ $I->sendPost('/api/users', [
+ 'username' => 'newuser',
+ 'email' => 'newuser@example.com',
+ 'password' => 'password123',
+ ]);
+
+ $I->seeResponseCodeIs(HttpCode::CREATED);
+ $I->seeResponseContainsJson([
+ 'username' => 'newuser',
+ 'email' => 'newuser@example.com',
+ ]);
+ }
+
+ public function createUserValidationError(ApiTester $I): void
+ {
+ $I->haveHttpHeader('Content-Type', 'application/json');
+ $I->sendPost('/api/users', [
+ 'username' => '',
+ 'email' => 'invalid-email',
+ ]);
+
+ $I->seeResponseCodeIs(HttpCode::UNPROCESSABLE_ENTITY);
+ $I->seeResponseContainsJson([
+ 'field' => 'username',
+ 'message' => 'Username cannot be blank.',
+ ]);
+ }
+
+ public function updateUser(ApiTester $I): void
+ {
+ $I->amBearerAuthenticated('valid-token');
+ $I->haveHttpHeader('Content-Type', 'application/json');
+ $I->sendPut('/api/users/1', [
+ 'username' => 'updated',
+ ]);
+
+ $I->seeResponseCodeIs(HttpCode::OK);
+ $I->seeResponseContainsJson([
+ 'username' => 'updated',
+ ]);
+ }
+
+ public function deleteUser(ApiTester $I): void
+ {
+ $I->amBearerAuthenticated('admin-token');
+ $I->sendDelete('/api/users/2');
+ $I->seeResponseCodeIs(HttpCode::NO_CONTENT);
+ }
+}
+```
+
+## Тестирование с моками
+
+### Мокирование компонентов
+```php
+<?php
+namespace tests\unit\services;
+
+use app\services\PaymentService;
+use app\components\PaymentGateway;
+use Codeception\Test\Unit;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class PaymentServiceTest extends Unit
+{
+ private PaymentService $service;
+ private MockObject $gateway;
+
+ protected function _before(): void
+ {
+ // Создаём мок
+ $this->gateway = $this->createMock(PaymentGateway::class);
+
+ // Конфигурируем мок
+ $this->gateway->method('charge')
+ ->willReturn(['success' => true, 'transaction_id' => 'TX123']);
+
+ $this->service = new PaymentService($this->gateway);
+ }
+
+ public function testProcessPayment(): void
+ {
+ // Ожидаем один вызов charge
+ $this->gateway->expects($this->once())
+ ->method('charge')
+ ->with(
+ $this->equalTo(100.00),
+ $this->equalTo('USD')
+ );
+
+ $result = $this->service->processPayment(100.00, 'USD');
+
+ $this->assertTrue($result['success']);
+ $this->assertEquals('TX123', $result['transaction_id']);
+ }
+
+ public function testPaymentFailure(): void
+ {
+ $this->gateway->method('charge')
+ ->willThrowException(new \Exception('Payment failed'));
+
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('Payment failed');
+
+ $this->service->processPayment(100.00, 'USD');
+ }
+}
+```
+
+### Мокирование Yii компонентов
+```php
+<?php
+namespace tests\unit\services;
+
+use app\services\NotificationService;
+use Codeception\Test\Unit;
+
+class NotificationServiceTest extends Unit
+{
+ public function testSendEmail(): void
+ {
+ // Мокируем mailer
+ $mailer = $this->createMock(\yii\mail\MailerInterface::class);
+ $message = $this->createMock(\yii\mail\MessageInterface::class);
+
+ $message->method('setTo')->willReturnSelf();
+ $message->method('setSubject')->willReturnSelf();
+ $message->method('setTextBody')->willReturnSelf();
+ $message->expects($this->once())->method('send')->willReturn(true);
+
+ $mailer->method('compose')->willReturn($message);
+
+ // Подменяем компонент
+ \Yii::$app->set('mailer', $mailer);
+
+ $service = new NotificationService();
+ $result = $service->sendEmail('test@example.com', 'Subject', 'Body');
+
+ $this->assertTrue($result);
+ }
+
+ protected function _after(): void
+ {
+ // Восстанавливаем оригинальный компонент
+ \Yii::$app->set('mailer', [
+ 'class' => \yii\symfonymailer\Mailer::class,
+ 'useFileTransport' => true,
+ ]);
+ }
+}
+```
+
+## Acceptance тесты
+
+### Selenium/WebDriver тесты
+```php
+<?php
+// tests/acceptance/LoginCest.php
+namespace tests\acceptance;
+
+use tests\AcceptanceTester;
+
+class LoginCest
+{
+ public function _before(AcceptanceTester $I): void
+ {
+ $I->amOnPage('/');
+ }
+
+ public function testLoginPage(AcceptanceTester $I): void
+ {
+ $I->click('Login');
+ $I->see('Login', 'h1');
+ }
+
+ public function testSuccessfulLogin(AcceptanceTester $I): void
+ {
+ $I->amOnPage('/site/login');
+ $I->fillField('Username', 'admin');
+ $I->fillField('Password', 'admin123');
+ $I->click('Login');
+
+ $I->waitForText('Welcome, admin');
+ $I->see('Logout');
+ }
+
+ public function testAjaxValidation(AcceptanceTester $I): void
+ {
+ $I->amOnPage('/site/login');
+ $I->fillField('Username', 'admin');
+ $I->click('body'); // Trigger blur
+
+ $I->waitForElement('.field-loginform-password.has-error');
+ $I->see('Password cannot be blank.');
+ }
+}
+```
+
+## Команды тестирования
+
+```bash
+# Все тесты
+vendor/bin/codecept run
+
+# Конкретный suite
+vendor/bin/codecept run unit
+vendor/bin/codecept run functional
+vendor/bin/codecept run api
+
+# Конкретный тест
+vendor/bin/codecept run unit/models/UserTest
+vendor/bin/codecept run unit/models/UserTest:testValidation
+
+# С покрытием кода
+vendor/bin/codecept run --coverage --coverage-html
+
+# Debug режим
+vendor/bin/codecept run unit -vvv
+
+# С отчётом
+vendor/bin/codecept run --html
+
+# Генерация fixtures
+vendor/bin/codecept generate:fixture User --namespace=tests\fixtures
+```
+
+## Рекомендации для Claude Code
+
+1. **Изоляция** - каждый тест должен быть независим
+2. **Fixtures** - используйте fixtures для тестовых данных
+3. **Моки** - мокируйте внешние зависимости
+4. **Покрытие** - стремитесь к 80%+ покрытию критического кода
+5. **CI/CD** - запускайте тесты в CI pipeline
+6. **Именование** - тесты должны описывать ожидаемое поведение
--- /dev/null
+# Yii2 Security - Безопасность
+
+## Аутентификация
+
+### Identity Interface
+```php
+<?php
+// models/User.php
+namespace app\models;
+
+use yii\db\ActiveRecord;
+use yii\web\IdentityInterface;
+
+class User extends ActiveRecord implements IdentityInterface
+{
+ public const STATUS_INACTIVE = 0;
+ public const STATUS_ACTIVE = 1;
+
+ public static function tableName(): string
+ {
+ return '{{%users}}';
+ }
+
+ // Поиск по ID
+ public static function findIdentity($id): ?static
+ {
+ return static::findOne(['id' => $id, 'status' => self::STATUS_ACTIVE]);
+ }
+
+ // Поиск по токену (для REST API)
+ public static function findIdentityByAccessToken($token, $type = null): ?static
+ {
+ return static::findOne(['access_token' => $token, 'status' => self::STATUS_ACTIVE]);
+ }
+
+ public function getId(): int
+ {
+ return $this->id;
+ }
+
+ public function getAuthKey(): string
+ {
+ return $this->auth_key;
+ }
+
+ public function validateAuthKey($authKey): bool
+ {
+ return $this->auth_key === $authKey;
+ }
+
+ // Дополнительные методы
+ public static function findByUsername(string $username): ?static
+ {
+ return static::findOne(['username' => $username, 'status' => self::STATUS_ACTIVE]);
+ }
+
+ public static function findByEmail(string $email): ?static
+ {
+ return static::findOne(['email' => $email, 'status' => self::STATUS_ACTIVE]);
+ }
+
+ public function validatePassword(string $password): bool
+ {
+ return \Yii::$app->security->validatePassword($password, $this->password_hash);
+ }
+
+ public function setPassword(string $password): void
+ {
+ $this->password_hash = \Yii::$app->security->generatePasswordHash($password);
+ }
+
+ public function generateAuthKey(): void
+ {
+ $this->auth_key = \Yii::$app->security->generateRandomString();
+ }
+
+ public function generateAccessToken(): void
+ {
+ $this->access_token = \Yii::$app->security->generateRandomString(32);
+ }
+
+ public function generatePasswordResetToken(): void
+ {
+ $this->password_reset_token = \Yii::$app->security->generateRandomString() . '_' . time();
+ }
+
+ public function removePasswordResetToken(): void
+ {
+ $this->password_reset_token = null;
+ }
+
+ public static function findByPasswordResetToken(string $token): ?static
+ {
+ if (!static::isPasswordResetTokenValid($token)) {
+ return null;
+ }
+
+ return static::findOne([
+ 'password_reset_token' => $token,
+ 'status' => self::STATUS_ACTIVE,
+ ]);
+ }
+
+ public static function isPasswordResetTokenValid(?string $token): bool
+ {
+ if ($token === null) {
+ return false;
+ }
+
+ $timestamp = (int) substr($token, strrpos($token, '_') + 1);
+ $expire = \Yii::$app->params['user.passwordResetTokenExpire'] ?? 3600;
+
+ return $timestamp + $expire >= time();
+ }
+}
+```
+
+### Конфигурация User компонента
+```php
+<?php
+// config/web.php
+return [
+ 'components' => [
+ 'user' => [
+ 'identityClass' => \app\models\User::class,
+ 'enableAutoLogin' => true,
+ 'loginUrl' => ['site/login'],
+ 'identityCookie' => [
+ 'name' => '_identity',
+ 'httpOnly' => true,
+ 'secure' => !YII_DEBUG,
+ 'sameSite' => \yii\web\Cookie::SAME_SITE_LAX,
+ ],
+ 'authTimeout' => 3600, // 1 час
+ 'absoluteAuthTimeout' => 86400, // 24 часа
+ ],
+ ],
+];
+```
+
+### Форма логина
+```php
+<?php
+// models/LoginForm.php
+namespace app\models;
+
+use yii\base\Model;
+
+class LoginForm extends Model
+{
+ public ?string $username = null;
+ public ?string $password = null;
+ public bool $rememberMe = false;
+
+ private ?User $_user = null;
+
+ public function rules(): array
+ {
+ return [
+ [['username', 'password'], 'required'],
+ ['rememberMe', 'boolean'],
+ ['password', 'validatePassword'],
+ ];
+ }
+
+ public function validatePassword(string $attribute): void
+ {
+ if (!$this->hasErrors()) {
+ $user = $this->getUser();
+
+ if ($user === null || !$user->validatePassword($this->password)) {
+ $this->addError($attribute, 'Неверный логин или пароль');
+ }
+ }
+ }
+
+ public function login(): bool
+ {
+ if (!$this->validate()) {
+ return false;
+ }
+
+ return \Yii::$app->user->login(
+ $this->getUser(),
+ $this->rememberMe ? 3600 * 24 * 30 : 0
+ );
+ }
+
+ protected function getUser(): ?User
+ {
+ if ($this->_user === null) {
+ $this->_user = User::findByUsername($this->username);
+ }
+
+ return $this->_user;
+ }
+}
+```
+
+## RBAC (Role-Based Access Control)
+
+### Конфигурация
+```php
+<?php
+// config/console.php и config/web.php
+return [
+ 'components' => [
+ 'authManager' => [
+ 'class' => \yii\rbac\DbManager::class,
+ 'cache' => 'cache',
+ ],
+ ],
+];
+```
+
+### Миграция для RBAC
+```bash
+php yii migrate --migrationPath=@yii/rbac/migrations
+```
+
+### Создание ролей и разрешений
+```php
+<?php
+// console/controllers/RbacController.php
+namespace console\controllers;
+
+use yii\console\Controller;
+
+class RbacController extends Controller
+{
+ public function actionInit(): void
+ {
+ $auth = \Yii::$app->authManager;
+ $auth->removeAll();
+
+ // Разрешения
+ $createPost = $auth->createPermission('createPost');
+ $createPost->description = 'Создание постов';
+ $auth->add($createPost);
+
+ $updatePost = $auth->createPermission('updatePost');
+ $updatePost->description = 'Редактирование постов';
+ $auth->add($updatePost);
+
+ $deletePost = $auth->createPermission('deletePost');
+ $deletePost->description = 'Удаление постов';
+ $auth->add($deletePost);
+
+ $manageUsers = $auth->createPermission('manageUsers');
+ $manageUsers->description = 'Управление пользователями';
+ $auth->add($manageUsers);
+
+ // Правило для проверки авторства
+ $authorRule = new \app\rbac\AuthorRule();
+ $auth->add($authorRule);
+
+ // Разрешение на редактирование своих постов
+ $updateOwnPost = $auth->createPermission('updateOwnPost');
+ $updateOwnPost->description = 'Редактирование своих постов';
+ $updateOwnPost->ruleName = $authorRule->name;
+ $auth->add($updateOwnPost);
+ $auth->addChild($updateOwnPost, $updatePost);
+
+ // Роли
+ $user = $auth->createRole('user');
+ $user->description = 'Пользователь';
+ $auth->add($user);
+ $auth->addChild($user, $createPost);
+ $auth->addChild($user, $updateOwnPost);
+
+ $moderator = $auth->createRole('moderator');
+ $moderator->description = 'Модератор';
+ $auth->add($moderator);
+ $auth->addChild($moderator, $user);
+ $auth->addChild($moderator, $updatePost);
+ $auth->addChild($moderator, $deletePost);
+
+ $admin = $auth->createRole('admin');
+ $admin->description = 'Администратор';
+ $auth->add($admin);
+ $auth->addChild($admin, $moderator);
+ $auth->addChild($admin, $manageUsers);
+
+ echo "RBAC инициализирован\n";
+ }
+}
+```
+
+### Правило (Rule)
+```php
+<?php
+// rbac/AuthorRule.php
+namespace app\rbac;
+
+use yii\rbac\Rule;
+use app\models\Post;
+
+class AuthorRule extends Rule
+{
+ public $name = 'isAuthor';
+
+ public function execute($user, $item, $params): bool
+ {
+ if (!isset($params['post'])) {
+ return false;
+ }
+
+ $post = $params['post'];
+
+ if ($post instanceof Post) {
+ return $post->user_id === $user;
+ }
+
+ return Post::find()
+ ->where(['id' => $post, 'user_id' => $user])
+ ->exists();
+ }
+}
+```
+
+### Использование RBAC
+```php
+<?php
+// В контроллере
+class PostController extends Controller
+{
+ public function behaviors(): array
+ {
+ return [
+ 'access' => [
+ 'class' => AccessControl::class,
+ 'rules' => [
+ [
+ 'actions' => ['index', 'view'],
+ 'allow' => true,
+ ],
+ [
+ 'actions' => ['create'],
+ 'allow' => true,
+ 'roles' => ['createPost'],
+ ],
+ [
+ 'actions' => ['update'],
+ 'allow' => true,
+ 'roles' => ['updatePost'],
+ ],
+ [
+ 'actions' => ['delete'],
+ 'allow' => true,
+ 'roles' => ['deletePost'],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ public function actionUpdate(int $id): string|\yii\web\Response
+ {
+ $post = Post::findOne($id);
+
+ // Проверка с параметрами (для правила)
+ if (!\Yii::$app->user->can('updatePost', ['post' => $post])) {
+ throw new ForbiddenHttpException('Нет доступа');
+ }
+
+ // ...
+ }
+}
+
+// Проверка разрешений
+if (\Yii::$app->user->can('createPost')) {
+ // Может создавать
+}
+
+// Назначение роли
+$auth = \Yii::$app->authManager;
+$userRole = $auth->getRole('user');
+$auth->assign($userRole, $userId);
+
+// Отзыв роли
+$auth->revoke($userRole, $userId);
+
+// Получение ролей пользователя
+$roles = $auth->getRolesByUser($userId);
+```
+
+## Защита от атак
+
+### CSRF Protection
+```php
+<?php
+// config/web.php
+return [
+ 'components' => [
+ 'request' => [
+ 'enableCsrfValidation' => true,
+ 'csrfParam' => '_csrf-frontend',
+ 'csrfCookie' => [
+ 'httpOnly' => true,
+ 'secure' => !YII_DEBUG,
+ ],
+ ],
+ ],
+];
+
+// В форме (автоматически добавляется ActiveForm)
+<?= Html::csrfMetaTags() ?>
+<?= Html::hiddenInput(\Yii::$app->request->csrfParam, \Yii::$app->request->csrfToken) ?>
+
+// Отключение CSRF для отдельных actions
+public function beforeAction($action): bool
+{
+ if ($action->id === 'webhook') {
+ $this->enableCsrfValidation = false;
+ }
+ return parent::beforeAction($action);
+}
+```
+
+### XSS Protection
+```php
+<?php
+use yii\helpers\Html;
+use yii\helpers\HtmlPurifier;
+
+// Экранирование вывода
+echo Html::encode($userInput);
+
+// Очистка HTML
+echo HtmlPurifier::process($htmlContent);
+
+// Настройка HtmlPurifier
+echo HtmlPurifier::process($htmlContent, [
+ 'HTML.Allowed' => 'p,b,i,u,a[href],ul,ol,li',
+ 'AutoFormat.RemoveEmpty' => true,
+]);
+
+// Content Security Policy
+\Yii::$app->response->headers->set('Content-Security-Policy',
+ "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
+);
+```
+
+### SQL Injection Protection
+```php
+<?php
+// ПРАВИЛЬНО - параметризованные запросы
+$users = User::find()
+ ->where(['status' => $status])
+ ->andWhere(['like', 'name', $search])
+ ->all();
+
+$users = \Yii::$app->db->createCommand(
+ 'SELECT * FROM users WHERE status = :status AND name LIKE :name',
+ [':status' => $status, ':name' => "%{$search}%"]
+)->queryAll();
+
+// НЕПРАВИЛЬНО - конкатенация
+$users = \Yii::$app->db->createCommand(
+ "SELECT * FROM users WHERE status = {$status}" // SQL Injection!
+)->queryAll();
+```
+
+### Защита от Mass Assignment
+```php
+<?php
+// В модели - только разрешённые атрибуты
+public function rules(): array
+{
+ return [
+ [['name', 'email'], 'required'],
+ [['name', 'email', 'bio'], 'safe'], // Можно массово присваивать
+ // role, status - нельзя массово присваивать
+ ];
+}
+
+// Или через сценарии
+public function scenarios(): array
+{
+ return [
+ self::SCENARIO_CREATE => ['name', 'email', 'password'],
+ self::SCENARIO_UPDATE => ['name', 'email', 'bio'],
+ self::SCENARIO_ADMIN => ['name', 'email', 'role', 'status'],
+ ];
+}
+
+// Явная загрузка
+$model->load($data); // Использует scenarios/rules
+$model->load($data, ''); // Без формы, напрямую
+
+// Явное присваивание (безопасно)
+$model->role = 'admin'; // Работает всегда
+```
+
+## Шифрование и хеширование
+
+### Работа с паролями
+```php
+<?php
+$security = \Yii::$app->security;
+
+// Хеширование пароля
+$hash = $security->generatePasswordHash('mypassword');
+
+// Проверка пароля
+$isValid = $security->validatePassword('mypassword', $hash);
+
+// С настройкой cost
+$hash = $security->generatePasswordHash('mypassword', 13);
+```
+
+### Генерация токенов
+```php
+<?php
+$security = \Yii::$app->security;
+
+// Случайная строка
+$token = $security->generateRandomString(32);
+
+// Случайные байты
+$bytes = $security->generateRandomKey(32);
+
+// UUID
+$uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+ random_int(0, 0xffff), random_int(0, 0xffff),
+ random_int(0, 0xffff),
+ random_int(0, 0x0fff) | 0x4000,
+ random_int(0, 0x3fff) | 0x8000,
+ random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff)
+);
+```
+
+### Шифрование данных
+```php
+<?php
+$security = \Yii::$app->security;
+$secretKey = \Yii::$app->params['encryptionKey'];
+
+// Шифрование
+$encrypted = $security->encryptByKey($sensitiveData, $secretKey);
+$encryptedBase64 = base64_encode($encrypted);
+
+// Дешифрование
+$decrypted = $security->decryptByKey(base64_decode($encryptedBase64), $secretKey);
+
+// С паролем
+$encrypted = $security->encryptByPassword($data, $password);
+$decrypted = $security->decryptByPassword($encrypted, $password);
+```
+
+### HMAC
+```php
+<?php
+$security = \Yii::$app->security;
+
+// Генерация HMAC
+$data = json_encode(['user_id' => 123, 'expires' => time() + 3600]);
+$hash = $security->hashData($data, $secretKey);
+
+// Проверка и извлечение
+$originalData = $security->validateData($hash, $secretKey);
+if ($originalData === false) {
+ throw new \Exception('Data tampered');
+}
+```
+
+## Безопасные headers
+
+```php
+<?php
+// config/web.php
+return [
+ 'components' => [
+ 'response' => [
+ 'on beforeSend' => function ($event) {
+ $response = $event->sender;
+ $headers = $response->headers;
+
+ // Security headers
+ $headers->set('X-Content-Type-Options', 'nosniff');
+ $headers->set('X-Frame-Options', 'SAMEORIGIN');
+ $headers->set('X-XSS-Protection', '1; mode=block');
+ $headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
+
+ // HSTS (только для production)
+ if (!YII_DEBUG) {
+ $headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
+ }
+
+ // CSP
+ $headers->set('Content-Security-Policy', implode('; ', [
+ "default-src 'self'",
+ "script-src 'self' 'unsafe-inline' cdn.example.com",
+ "style-src 'self' 'unsafe-inline' fonts.googleapis.com",
+ "font-src 'self' fonts.gstatic.com",
+ "img-src 'self' data: https:",
+ "connect-src 'self' api.example.com",
+ ]));
+ },
+ ],
+ ],
+];
+```
+
+## Rate Limiting
+
+```php
+<?php
+// behaviors/RateLimiter.php
+use yii\filters\RateLimiter;
+
+public function behaviors(): array
+{
+ return [
+ 'rateLimiter' => [
+ 'class' => RateLimiter::class,
+ 'enableRateLimitHeaders' => true,
+ 'errorMessage' => 'Слишком много запросов',
+ ],
+ ];
+}
+
+// Модель должна реализовать RateLimitInterface
+class User extends ActiveRecord implements \yii\filters\RateLimitInterface
+{
+ public function getRateLimit($request, $action): array
+ {
+ // 100 запросов за 60 секунд
+ return [100, 60];
+ }
+
+ public function loadAllowance($request, $action): array
+ {
+ return [$this->allowance, $this->allowance_updated_at];
+ }
+
+ public function saveAllowance($request, $action, $allowance, $timestamp): void
+ {
+ $this->updateAttributes([
+ 'allowance' => $allowance,
+ 'allowance_updated_at' => $timestamp,
+ ]);
+ }
+}
+```
+
+## Рекомендации для Claude Code
+
+1. **Валидация** - всегда валидируйте входные данные
+2. **Экранирование** - используйте Html::encode() для вывода
+3. **Параметризация** - никогда не конкатенируйте SQL
+4. **CSRF** - включайте для всех форм
+5. **RBAC** - используйте для контроля доступа
+6. **HTTPS** - включайте в production
+7. **Секреты** - храните в переменных окружения
--- /dev/null
+# Yii2 Performance - Производительность
+
+## Кэширование
+
+### Конфигурация кэша
+```php
+<?php
+// config/web.php
+return [
+ 'components' => [
+ // File cache (для разработки)
+ 'cache' => [
+ 'class' => \yii\caching\FileCache::class,
+ 'cachePath' => '@runtime/cache',
+ ],
+
+ // Redis cache (для production)
+ 'cache' => [
+ 'class' => \yii\redis\Cache::class,
+ 'redis' => [
+ 'hostname' => 'localhost',
+ 'port' => 6379,
+ 'database' => 0,
+ ],
+ 'defaultDuration' => 3600,
+ ],
+
+ // Memcached
+ 'cache' => [
+ 'class' => \yii\caching\MemCache::class,
+ 'servers' => [
+ ['host' => 'localhost', 'port' => 11211, 'weight' => 100],
+ ],
+ ],
+
+ // APCu
+ 'cache' => [
+ 'class' => \yii\caching\ApcCache::class,
+ ],
+ ],
+];
+```
+
+### Работа с кэшем
+```php
+<?php
+$cache = \Yii::$app->cache;
+
+// Базовые операции
+$cache->set('key', $value, 3600); // 1 час
+$value = $cache->get('key');
+$cache->delete('key');
+$cache->flush(); // Очистить весь кэш
+
+// Проверка существования
+if ($cache->exists('key')) {
+ // ...
+}
+
+// getOrSet - получить или вычислить
+$users = $cache->getOrSet('all-users', function () {
+ return User::find()->all();
+}, 3600);
+
+// С dependency
+$users = $cache->getOrSet('users-count', function () {
+ return User::find()->count();
+}, 3600, new \yii\caching\DbDependency([
+ 'sql' => 'SELECT MAX(updated_at) FROM users',
+]));
+
+// Множественные операции
+$values = $cache->multiGet(['key1', 'key2', 'key3']);
+$cache->multiSet([
+ 'key1' => $value1,
+ 'key2' => $value2,
+], 3600);
+
+// Теги (TagDependency)
+$dependency = new \yii\caching\TagDependency(['tags' => ['users']]);
+$cache->set('user-1', $user, 3600, $dependency);
+$cache->set('user-2', $user2, 3600, $dependency);
+
+// Инвалидация по тегу
+\yii\caching\TagDependency::invalidate($cache, ['users']);
+```
+
+### Кэширование запросов
+```php
+<?php
+// Кэширование отдельного запроса
+$users = User::find()
+ ->where(['status' => User::STATUS_ACTIVE])
+ ->cache(3600) // 1 час
+ ->all();
+
+// С dependency
+$posts = Post::find()
+ ->where(['status' => Post::STATUS_PUBLISHED])
+ ->cache(3600, new \yii\caching\DbDependency([
+ 'sql' => 'SELECT MAX(updated_at) FROM posts WHERE status = 1',
+ ]))
+ ->all();
+
+// Глобальное кэширование запросов
+\Yii::$app->db->cache(function ($db) {
+ $users = User::find()->all();
+ $posts = Post::find()->all();
+ return [$users, $posts];
+}, 3600);
+```
+
+### Schema Cache
+```php
+<?php
+// config/db.php
+return [
+ 'class' => \yii\db\Connection::class,
+ 'dsn' => 'pgsql:host=localhost;dbname=myapp',
+ 'enableSchemaCache' => true,
+ 'schemaCacheDuration' => 86400, // 24 часа
+ 'schemaCache' => 'cache',
+];
+
+// Очистка schema cache
+\Yii::$app->db->schema->refresh();
+\Yii::$app->cache->delete(\Yii::$app->db->schema->getCacheKey('tableName'));
+```
+
+### Fragment Caching
+```php
+<?php
+// views/site/index.php
+<?php if ($this->beginCache('sidebar', [
+ 'duration' => 3600,
+ 'dependency' => [
+ 'class' => \yii\caching\DbDependency::class,
+ 'sql' => 'SELECT MAX(updated_at) FROM posts',
+ ],
+ 'variations' => [
+ \Yii::$app->language,
+ \Yii::$app->user->id ?? 0,
+ ],
+])): ?>
+
+ <?= $this->render('_sidebar', ['posts' => $posts]) ?>
+
+<?php $this->endCache(); endif; ?>
+```
+
+### Page Caching
+```php
+<?php
+// controllers/SiteController.php
+public function behaviors(): array
+{
+ return [
+ 'pageCache' => [
+ 'class' => \yii\filters\PageCache::class,
+ 'only' => ['index', 'about'],
+ 'duration' => 3600,
+ 'variations' => [
+ \Yii::$app->language,
+ ],
+ 'dependency' => [
+ 'class' => \yii\caching\DbDependency::class,
+ 'sql' => 'SELECT MAX(updated_at) FROM pages',
+ ],
+ ],
+ ];
+}
+```
+
+### HTTP Caching
+```php
+<?php
+public function behaviors(): array
+{
+ return [
+ 'httpCache' => [
+ 'class' => \yii\filters\HttpCache::class,
+ 'only' => ['view'],
+ 'lastModified' => function ($action, $params) {
+ $post = Post::findOne(\Yii::$app->request->get('id'));
+ return $post ? $post->updated_at : null;
+ },
+ 'etagSeed' => function ($action, $params) {
+ $post = Post::findOne(\Yii::$app->request->get('id'));
+ return $post ? serialize([
+ $post->id,
+ $post->updated_at,
+ \Yii::$app->language,
+ ]) : null;
+ },
+ 'cacheControlHeader' => 'public, max-age=3600',
+ ],
+ ];
+}
+```
+
+## Оптимизация запросов
+
+### Eager Loading
+```php
+<?php
+// НЕПРАВИЛЬНО - N+1 запросов
+$posts = Post::find()->all();
+foreach ($posts as $post) {
+ echo $post->user->name; // Запрос для каждого поста
+ foreach ($post->comments as $comment) { // Ещё N запросов
+ echo $comment->text;
+ }
+}
+
+// ПРАВИЛЬНО - 3 запроса
+$posts = Post::find()
+ ->with(['user', 'comments'])
+ ->all();
+
+// Вложенные связи
+$posts = Post::find()
+ ->with(['user.profile', 'comments.user'])
+ ->all();
+
+// С условиями
+$posts = Post::find()
+ ->with([
+ 'comments' => function ($query) {
+ $query->andWhere(['status' => Comment::STATUS_APPROVED])
+ ->orderBy(['created_at' => SORT_DESC])
+ ->limit(5);
+ },
+ ])
+ ->all();
+
+// joinWith для фильтрации
+$posts = Post::find()
+ ->joinWith('user')
+ ->where(['users.status' => User::STATUS_ACTIVE])
+ ->all();
+```
+
+### Индексация
+```php
+<?php
+// Миграция - создание индексов
+$this->createIndex('idx-posts-user_id', '{{%posts}}', 'user_id');
+$this->createIndex('idx-posts-status', '{{%posts}}', 'status');
+$this->createIndex('idx-posts-created_at', '{{%posts}}', 'created_at');
+
+// Составной индекс
+$this->createIndex('idx-posts-user_status', '{{%posts}}', ['user_id', 'status']);
+
+// Частичный индекс (PostgreSQL)
+$this->execute('CREATE INDEX idx_active_posts ON posts (created_at) WHERE status = 1');
+
+// Покрывающий индекс
+$this->execute('CREATE INDEX idx_posts_covering ON posts (user_id) INCLUDE (title, status)');
+
+// Анализ запроса
+$sql = Post::find()
+ ->where(['status' => 1])
+ ->orderBy(['created_at' => SORT_DESC])
+ ->createCommand()
+ ->rawSql;
+
+// EXPLAIN
+$explain = \Yii::$app->db->createCommand("EXPLAIN ANALYZE {$sql}")->queryAll();
+```
+
+### Batch обработка
+```php
+<?php
+// Для больших наборов данных
+foreach (User::find()->batch(1000) as $users) {
+ foreach ($users as $user) {
+ // Обработка партиями по 1000
+ }
+}
+
+// each() для экономии памяти
+foreach (User::find()->each(100) as $user) {
+ // По одной записи, но эффективно
+}
+
+// С eager loading
+foreach (User::find()->with('profile')->each(100) as $user) {
+ echo $user->profile->bio;
+}
+```
+
+### Асинхронные запросы
+```php
+<?php
+// Параллельные запросы через cURL multi
+$urls = ['http://api1.com', 'http://api2.com', 'http://api3.com'];
+
+$mh = curl_multi_init();
+$handles = [];
+
+foreach ($urls as $i => $url) {
+ $handles[$i] = curl_init($url);
+ curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
+ curl_multi_add_handle($mh, $handles[$i]);
+}
+
+do {
+ curl_multi_exec($mh, $running);
+ curl_multi_select($mh);
+} while ($running > 0);
+
+$results = [];
+foreach ($handles as $i => $handle) {
+ $results[$i] = curl_multi_getcontent($handle);
+ curl_multi_remove_handle($mh, $handle);
+}
+
+curl_multi_close($mh);
+```
+
+## Профилирование
+
+### Debug Toolbar
+```php
+<?php
+// config/web.php
+if (YII_ENV_DEV) {
+ $config['bootstrap'][] = 'debug';
+ $config['modules']['debug'] = [
+ 'class' => \yii\debug\Module::class,
+ 'allowedIPs' => ['127.0.0.1', '::1'],
+ 'panels' => [
+ 'db' => ['class' => \yii\debug\panels\DbPanel::class],
+ 'profiling' => ['class' => \yii\debug\panels\ProfilingPanel::class],
+ ],
+ ];
+}
+```
+
+### Профилирование кода
+```php
+<?php
+// Начало профилирования
+\Yii::beginProfile('heavy-operation');
+
+// Тяжёлая операция
+$result = $this->heavyOperation();
+
+// Конец профилирования
+\Yii::endProfile('heavy-operation');
+
+// С категорией
+\Yii::beginProfile('db-query', 'database');
+$users = User::find()->all();
+\Yii::endProfile('db-query', 'database');
+
+// Логирование времени
+$start = microtime(true);
+$result = $this->process();
+$time = microtime(true) - $start;
+\Yii::info("Processing took {$time} seconds", 'performance');
+```
+
+### Логирование запросов
+```php
+<?php
+// config/web.php
+return [
+ 'components' => [
+ 'log' => [
+ 'targets' => [
+ [
+ 'class' => \yii\log\FileTarget::class,
+ 'categories' => ['yii\db\Command::*'],
+ 'logFile' => '@runtime/logs/sql.log',
+ 'logVars' => [],
+ ],
+ ],
+ ],
+ ],
+];
+```
+
+## Asset оптимизация
+
+### Asset Bundle compression
+```php
+<?php
+// config/console.php
+return [
+ 'controllerMap' => [
+ 'asset' => [
+ 'class' => \yii\console\controllers\AssetController::class,
+ 'bundles' => [
+ \app\assets\AppAsset::class,
+ ],
+ 'targets' => [
+ 'all' => [
+ 'class' => \yii\web\AssetBundle::class,
+ 'basePath' => '@webroot/assets',
+ 'baseUrl' => '@web/assets',
+ 'js' => 'js/all-{hash}.js',
+ 'css' => 'css/all-{hash}.css',
+ ],
+ ],
+ 'jsCompressor' => 'java -jar compiler.jar --js {from} --js_output_file {to}',
+ 'cssCompressor' => 'java -jar yuicompressor.jar --type css {from} -o {to}',
+ ],
+ ],
+];
+
+// Команда
+// php yii asset assets.php config/assets-prod.php
+```
+
+### Lazy loading assets
+```php
+<?php
+// assets/AppAsset.php
+class AppAsset extends AssetBundle
+{
+ public $basePath = '@webroot';
+ public $baseUrl = '@web';
+
+ public $css = [
+ 'css/site.css',
+ ];
+
+ public $js = [
+ 'js/app.js',
+ ];
+
+ // Асинхронная загрузка
+ public $jsOptions = [
+ 'defer' => true,
+ ];
+
+ public $cssOptions = [
+ 'media' => 'print',
+ 'onload' => "this.media='all'",
+ ];
+}
+```
+
+## Фоновые задачи
+
+### Очереди (yii2-queue)
+```php
+<?php
+// config/console.php
+return [
+ 'components' => [
+ 'queue' => [
+ 'class' => \yii\queue\redis\Queue::class,
+ 'redis' => 'redis',
+ 'channel' => 'queue',
+ ],
+ ],
+];
+
+// Задача
+class SendEmailJob extends \yii\base\BaseObject implements \yii\queue\JobInterface
+{
+ public string $to;
+ public string $subject;
+ public string $body;
+
+ public function execute($queue): void
+ {
+ \Yii::$app->mailer->compose()
+ ->setTo($this->to)
+ ->setSubject($this->subject)
+ ->setTextBody($this->body)
+ ->send();
+ }
+}
+
+// Добавление в очередь
+\Yii::$app->queue->push(new SendEmailJob([
+ 'to' => 'user@example.com',
+ 'subject' => 'Hello',
+ 'body' => 'World',
+]));
+
+// С задержкой
+\Yii::$app->queue->delay(3600)->push($job); // Через 1 час
+
+// Запуск воркера
+// php yii queue/listen
+```
+
+## Масштабирование
+
+### Database Sharding
+```php
+<?php
+// config/web.php
+return [
+ 'components' => [
+ 'db' => [
+ 'class' => \yii\db\Connection::class,
+ 'dsn' => 'pgsql:host=master;dbname=app',
+ 'username' => 'root',
+ 'password' => '',
+ 'slaveConfig' => [
+ 'username' => 'root',
+ 'password' => '',
+ ],
+ 'slaves' => [
+ ['dsn' => 'pgsql:host=slave1;dbname=app'],
+ ['dsn' => 'pgsql:host=slave2;dbname=app'],
+ ],
+ ],
+ ],
+];
+
+// Master для записи, slaves для чтения автоматически
+$user = User::findOne(1); // Читает из slave
+$user->name = 'New Name';
+$user->save(); // Пишет в master
+```
+
+### Session Storage
+```php
+<?php
+// Redis sessions
+return [
+ 'components' => [
+ 'session' => [
+ 'class' => \yii\redis\Session::class,
+ 'redis' => 'redis',
+ 'keyPrefix' => 'session:',
+ ],
+ ],
+];
+
+// Database sessions
+return [
+ 'components' => [
+ 'session' => [
+ 'class' => \yii\web\DbSession::class,
+ 'sessionTable' => '{{%session}}',
+ ],
+ ],
+];
+```
+
+## Мониторинг
+
+### Пользовательские метрики
+```php
+<?php
+// components/MetricsCollector.php
+class MetricsCollector
+{
+ public function recordRequestTime(float $time): void
+ {
+ \Yii::$app->redis->lpush('metrics:request_times', $time);
+ \Yii::$app->redis->ltrim('metrics:request_times', 0, 9999);
+ }
+
+ public function recordDbQuery(string $sql, float $time): void
+ {
+ if ($time > 0.1) { // Медленные запросы > 100ms
+ \Yii::warning("Slow query ({$time}s): {$sql}", 'performance');
+ }
+ }
+
+ public function getAverageResponseTime(): float
+ {
+ $times = \Yii::$app->redis->lrange('metrics:request_times', 0, -1);
+ return $times ? array_sum($times) / count($times) : 0;
+ }
+}
+```
+
+## Рекомендации для Claude Code
+
+1. **Кэширование** - кэшируйте дорогие операции
+2. **Eager loading** - используйте with() для связей
+3. **Индексы** - создавайте для часто используемых полей
+4. **Batch** - обрабатывайте большие данные партиями
+5. **Очереди** - выносите тяжёлые операции в фон
+6. **Профилирование** - используйте debug toolbar для анализа
--- /dev/null
+# Yii2 REST API - Построение API
+
+## Базовая конфигурация
+
+### ActiveController
+```php
+<?php
+// controllers/api/UserController.php
+namespace app\controllers\api;
+
+use yii\rest\ActiveController;
+use yii\filters\auth\HttpBearerAuth;
+use yii\filters\RateLimiter;
+
+class UserController extends ActiveController
+{
+ public $modelClass = \app\models\User::class;
+
+ public function behaviors(): array
+ {
+ $behaviors = parent::behaviors();
+
+ // Формат ответа
+ $behaviors['contentNegotiator']['formats']['application/json'] = \yii\web\Response::FORMAT_JSON;
+
+ // Аутентификация
+ $behaviors['authenticator'] = [
+ 'class' => HttpBearerAuth::class,
+ 'except' => ['index', 'view'], // Публичные actions
+ ];
+
+ // Rate limiting
+ $behaviors['rateLimiter'] = [
+ 'class' => RateLimiter::class,
+ 'enableRateLimitHeaders' => true,
+ ];
+
+ return $behaviors;
+ }
+
+ // Переопределение actions
+ public function actions(): array
+ {
+ $actions = parent::actions();
+
+ // Отключить delete
+ unset($actions['delete']);
+
+ // Кастомизация index
+ $actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider'];
+
+ return $actions;
+ }
+
+ public function prepareDataProvider(): \yii\data\ActiveDataProvider
+ {
+ return new \yii\data\ActiveDataProvider([
+ 'query' => \app\models\User::find()
+ ->where(['status' => \app\models\User::STATUS_ACTIVE]),
+ 'pagination' => [
+ 'pageSize' => 20,
+ ],
+ 'sort' => [
+ 'defaultOrder' => ['created_at' => SORT_DESC],
+ ],
+ ]);
+ }
+}
+```
+
+### Конфигурация приложения
+```php
+<?php
+// config/web.php
+return [
+ 'components' => [
+ 'urlManager' => [
+ 'enablePrettyUrl' => true,
+ 'showScriptName' => false,
+ 'rules' => [
+ [
+ 'class' => \yii\rest\UrlRule::class,
+ 'controller' => 'api/user',
+ 'pluralize' => true,
+ ],
+ ],
+ ],
+ 'request' => [
+ 'parsers' => [
+ 'application/json' => \yii\web\JsonParser::class,
+ ],
+ ],
+ 'response' => [
+ 'formatters' => [
+ \yii\web\Response::FORMAT_JSON => [
+ 'class' => \yii\web\JsonResponseFormatter::class,
+ 'prettyPrint' => YII_DEBUG,
+ 'encodeOptions' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
+ ],
+ ],
+ ],
+ ],
+];
+```
+
+## Сериализация
+
+### Кастомный Serializer
+```php
+<?php
+// components/ApiSerializer.php
+namespace app\components;
+
+use yii\rest\Serializer;
+
+class ApiSerializer extends Serializer
+{
+ public $collectionEnvelope = 'data';
+ public $linksEnvelope = 'links';
+ public $metaEnvelope = 'meta';
+
+ protected function serializeDataProvider($dataProvider): array
+ {
+ $models = $this->serializeModels($dataProvider->getModels());
+
+ $result = [
+ $this->collectionEnvelope => $models,
+ ];
+
+ if ($this->metaEnvelope !== false) {
+ $result[$this->metaEnvelope] = [
+ 'totalCount' => $dataProvider->getTotalCount(),
+ 'pageCount' => $dataProvider->getPagination()->getPageCount(),
+ 'currentPage' => $dataProvider->getPagination()->getPage() + 1,
+ 'perPage' => $dataProvider->getPagination()->getPageSize(),
+ ];
+ }
+
+ if ($this->linksEnvelope !== false) {
+ $result[$this->linksEnvelope] = $this->serializeLinks($dataProvider);
+ }
+
+ return $result;
+ }
+
+ protected function serializeLinks($dataProvider): array
+ {
+ $pagination = $dataProvider->getPagination();
+
+ return [
+ 'self' => $pagination->createUrl($pagination->getPage()),
+ 'first' => $pagination->createUrl(0),
+ 'last' => $pagination->createUrl($pagination->getPageCount() - 1),
+ 'prev' => $pagination->getPage() > 0
+ ? $pagination->createUrl($pagination->getPage() - 1)
+ : null,
+ 'next' => $pagination->getPage() < $pagination->getPageCount() - 1
+ ? $pagination->createUrl($pagination->getPage() + 1)
+ : null,
+ ];
+ }
+}
+
+// Использование в контроллере
+public $serializer = [
+ 'class' => \app\components\ApiSerializer::class,
+ 'collectionEnvelope' => 'items',
+];
+```
+
+### fields() и extraFields() в модели
+```php
+<?php
+// models/User.php
+class User extends ActiveRecord
+{
+ // Базовые поля (всегда возвращаются)
+ public function fields(): array
+ {
+ return [
+ 'id',
+ 'username',
+ 'email',
+ 'full_name' => function ($model) {
+ return $model->first_name . ' ' . $model->last_name;
+ },
+ 'avatar_url' => function ($model) {
+ return $model->getAvatarUrl();
+ },
+ 'created_at' => function ($model) {
+ return date('c', $model->created_at);
+ },
+ ];
+ }
+
+ // Дополнительные поля (запрашиваются через ?expand=)
+ public function extraFields(): array
+ {
+ return [
+ 'profile', // Связь
+ 'posts', // Связь
+ 'postsCount' => function ($model) {
+ return $model->getPosts()->count();
+ },
+ 'statistics' => function ($model) {
+ return [
+ 'posts' => $model->getPosts()->count(),
+ 'comments' => $model->getComments()->count(),
+ 'likes' => $model->getLikesReceived()->count(),
+ ];
+ },
+ ];
+ }
+}
+
+// Запрос: GET /api/users/1?expand=profile,postsCount
+```
+
+## Аутентификация
+
+### Bearer Token
+```php
+<?php
+// Модель с токеном
+class User extends ActiveRecord implements \yii\web\IdentityInterface
+{
+ public static function findIdentityByAccessToken($token, $type = null): ?self
+ {
+ // Проверка срока действия токена
+ $tokenData = \Yii::$app->security->validateData($token, \Yii::$app->params['tokenSecret']);
+
+ if ($tokenData === false) {
+ return null;
+ }
+
+ $data = json_decode($tokenData, true);
+
+ if ($data['expires'] < time()) {
+ return null;
+ }
+
+ return static::findOne(['id' => $data['user_id'], 'status' => self::STATUS_ACTIVE]);
+ }
+
+ public function generateAccessToken(int $duration = 3600): string
+ {
+ $data = json_encode([
+ 'user_id' => $this->id,
+ 'expires' => time() + $duration,
+ ]);
+
+ return \Yii::$app->security->hashData($data, \Yii::$app->params['tokenSecret']);
+ }
+}
+
+// controllers/api/AuthController.php
+class AuthController extends \yii\rest\Controller
+{
+ public function actionLogin(): array
+ {
+ $request = \Yii::$app->request;
+ $email = $request->post('email');
+ $password = $request->post('password');
+
+ $user = User::findByEmail($email);
+
+ if ($user === null || !$user->validatePassword($password)) {
+ throw new \yii\web\UnauthorizedHttpException('Invalid credentials');
+ }
+
+ return [
+ 'access_token' => $user->generateAccessToken(),
+ 'token_type' => 'Bearer',
+ 'expires_in' => 3600,
+ 'user' => $user,
+ ];
+ }
+
+ public function actionRefresh(): array
+ {
+ $user = \Yii::$app->user->identity;
+
+ return [
+ 'access_token' => $user->generateAccessToken(),
+ 'token_type' => 'Bearer',
+ 'expires_in' => 3600,
+ ];
+ }
+}
+```
+
+### OAuth2 / JWT
+```php
+<?php
+// С использованием JWT (firebase/php-jwt)
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+
+class JwtAuth
+{
+ private string $secret;
+ private string $algorithm = 'HS256';
+
+ public function __construct(string $secret)
+ {
+ $this->secret = $secret;
+ }
+
+ public function generateToken(User $user): string
+ {
+ $payload = [
+ 'iss' => \Yii::$app->request->hostInfo,
+ 'aud' => \Yii::$app->request->hostInfo,
+ 'iat' => time(),
+ 'exp' => time() + 3600,
+ 'sub' => $user->id,
+ 'data' => [
+ 'username' => $user->username,
+ 'email' => $user->email,
+ ],
+ ];
+
+ return JWT::encode($payload, $this->secret, $this->algorithm);
+ }
+
+ public function validateToken(string $token): ?object
+ {
+ try {
+ return JWT::decode($token, new Key($this->secret, $this->algorithm));
+ } catch (\Exception $e) {
+ return null;
+ }
+ }
+}
+
+// Аутентификатор
+class JwtBearerAuth extends \yii\filters\auth\AuthMethod
+{
+ public function authenticate($user, $request, $response): ?\yii\web\IdentityInterface
+ {
+ $authHeader = $request->getHeaders()->get('Authorization');
+
+ if ($authHeader !== null && preg_match('/^Bearer\s+(.*?)$/', $authHeader, $matches)) {
+ $token = $matches[1];
+ $jwt = new JwtAuth(\Yii::$app->params['jwtSecret']);
+ $payload = $jwt->validateToken($token);
+
+ if ($payload !== null) {
+ $identity = User::findOne($payload->sub);
+ if ($identity !== null) {
+ $user->login($identity);
+ return $identity;
+ }
+ }
+ }
+
+ return null;
+ }
+}
+```
+
+### Rate Limiting
+```php
+<?php
+// Модель пользователя с RateLimitInterface
+class User extends ActiveRecord implements
+ \yii\web\IdentityInterface,
+ \yii\filters\RateLimitInterface
+{
+ public function getRateLimit($request, $action): array
+ {
+ // [количество запросов, период в секундах]
+ return match ($action->id) {
+ 'create' => [10, 60], // 10 запросов в минуту
+ 'update' => [30, 60], // 30 запросов в минуту
+ default => [100, 60], // 100 запросов в минуту
+ };
+ }
+
+ public function loadAllowance($request, $action): array
+ {
+ $key = $this->getRateLimitKey($action);
+ $data = \Yii::$app->cache->get($key);
+
+ return $data ? $data : [$this->getRateLimit($request, $action)[0], time()];
+ }
+
+ public function saveAllowance($request, $action, $allowance, $timestamp): void
+ {
+ $key = $this->getRateLimitKey($action);
+ \Yii::$app->cache->set($key, [$allowance, $timestamp], 3600);
+ }
+
+ private function getRateLimitKey($action): string
+ {
+ return "rate_limit:{$this->id}:{$action->uniqueId}";
+ }
+}
+```
+
+## Кастомные Actions
+
+### Дополнительные endpoints
+```php
+<?php
+// controllers/api/UserController.php
+class UserController extends ActiveController
+{
+ public $modelClass = \app\models\User::class;
+
+ public function actions(): array
+ {
+ $actions = parent::actions();
+
+ // Кастомизация create
+ $actions['create']['scenario'] = \app\models\User::SCENARIO_API_CREATE;
+
+ return $actions;
+ }
+
+ // POST /api/users/{id}/follow
+ public function actionFollow(int $id): array
+ {
+ $user = $this->findModel($id);
+ $currentUser = \Yii::$app->user->identity;
+
+ if ($currentUser->isFollowing($user)) {
+ throw new \yii\web\BadRequestHttpException('Already following');
+ }
+
+ $currentUser->follow($user);
+
+ return [
+ 'success' => true,
+ 'message' => 'Now following ' . $user->username,
+ ];
+ }
+
+ // DELETE /api/users/{id}/follow
+ public function actionUnfollow(int $id): array
+ {
+ $user = $this->findModel($id);
+ $currentUser = \Yii::$app->user->identity;
+
+ if (!$currentUser->isFollowing($user)) {
+ throw new \yii\web\BadRequestHttpException('Not following');
+ }
+
+ $currentUser->unfollow($user);
+
+ return [
+ 'success' => true,
+ 'message' => 'Unfollowed ' . $user->username,
+ ];
+ }
+
+ // GET /api/users/{id}/posts
+ public function actionPosts(int $id): \yii\data\ActiveDataProvider
+ {
+ $user = $this->findModel($id);
+
+ return new \yii\data\ActiveDataProvider([
+ 'query' => $user->getPosts()->andWhere(['status' => Post::STATUS_PUBLISHED]),
+ ]);
+ }
+
+ // POST /api/users/bulk-create
+ public function actionBulkCreate(): array
+ {
+ $request = \Yii::$app->request;
+ $users = $request->post('users', []);
+ $created = [];
+ $errors = [];
+
+ $transaction = \Yii::$app->db->beginTransaction();
+
+ try {
+ foreach ($users as $index => $userData) {
+ $user = new User();
+ $user->load($userData, '');
+
+ if ($user->save()) {
+ $created[] = $user;
+ } else {
+ $errors[$index] = $user->errors;
+ }
+ }
+
+ if (!empty($errors)) {
+ $transaction->rollBack();
+ throw new \yii\web\UnprocessableEntityHttpException(json_encode($errors));
+ }
+
+ $transaction->commit();
+
+ return [
+ 'success' => true,
+ 'created' => count($created),
+ 'users' => $created,
+ ];
+ } catch (\Exception $e) {
+ $transaction->rollBack();
+ throw $e;
+ }
+ }
+
+ protected function findModel(int $id): User
+ {
+ $model = User::findOne($id);
+
+ if ($model === null) {
+ throw new \yii\web\NotFoundHttpException('User not found');
+ }
+
+ return $model;
+ }
+}
+
+// Маршруты для кастомных actions
+'rules' => [
+ [
+ 'class' => \yii\rest\UrlRule::class,
+ 'controller' => 'api/user',
+ 'extraPatterns' => [
+ 'POST {id}/follow' => 'follow',
+ 'DELETE {id}/follow' => 'unfollow',
+ 'GET {id}/posts' => 'posts',
+ 'POST bulk-create' => 'bulk-create',
+ ],
+ ],
+],
+```
+
+## Версионирование API
+
+### Структура модулей
+```php
+<?php
+// modules/api/v1/Module.php
+namespace app\modules\api\v1;
+
+class Module extends \yii\base\Module
+{
+ public $controllerNamespace = 'app\modules\api\v1\controllers';
+}
+
+// modules/api/v2/Module.php
+namespace app\modules\api\v2;
+
+class Module extends \yii\base\Module
+{
+ public $controllerNamespace = 'app\modules\api\v2\controllers';
+}
+
+// config/web.php
+return [
+ 'modules' => [
+ 'v1' => [
+ 'class' => \app\modules\api\v1\Module::class,
+ ],
+ 'v2' => [
+ 'class' => \app\modules\api\v2\Module::class,
+ ],
+ ],
+ 'components' => [
+ 'urlManager' => [
+ 'rules' => [
+ [
+ 'class' => \yii\rest\UrlRule::class,
+ 'controller' => ['v1/user' => 'v1/user'],
+ 'prefix' => 'api',
+ ],
+ [
+ 'class' => \yii\rest\UrlRule::class,
+ 'controller' => ['v2/user' => 'v2/user'],
+ 'prefix' => 'api',
+ ],
+ ],
+ ],
+ ],
+];
+```
+
+### Версионирование через заголовки
+```php
+<?php
+// components/ApiVersionNegotiator.php
+namespace app\components;
+
+use yii\base\ActionFilter;
+
+class ApiVersionNegotiator extends ActionFilter
+{
+ public string $defaultVersion = 'v1';
+ public array $supportedVersions = ['v1', 'v2'];
+
+ public function beforeAction($action): bool
+ {
+ $version = $this->detectVersion();
+
+ if (!in_array($version, $this->supportedVersions)) {
+ throw new \yii\web\BadRequestHttpException(
+ "API version '$version' is not supported"
+ );
+ }
+
+ \Yii::$app->params['apiVersion'] = $version;
+
+ return parent::beforeAction($action);
+ }
+
+ protected function detectVersion(): string
+ {
+ $request = \Yii::$app->request;
+
+ // Accept header: application/vnd.api.v2+json
+ $accept = $request->getHeaders()->get('Accept');
+ if (preg_match('/application\/vnd\.api\.(v\d+)\+json/', $accept, $matches)) {
+ return $matches[1];
+ }
+
+ // Custom header: X-API-Version: v2
+ $customHeader = $request->getHeaders()->get('X-API-Version');
+ if ($customHeader && in_array($customHeader, $this->supportedVersions)) {
+ return $customHeader;
+ }
+
+ return $this->defaultVersion;
+ }
+}
+```
+
+## Обработка ошибок
+
+### Кастомный ErrorHandler
+```php
+<?php
+// components/ApiErrorHandler.php
+namespace app\components;
+
+use yii\web\ErrorHandler;
+use yii\web\Response;
+
+class ApiErrorHandler extends ErrorHandler
+{
+ protected function renderException($exception): void
+ {
+ $response = \Yii::$app->response;
+ $response->format = Response::FORMAT_JSON;
+
+ if ($exception instanceof \yii\web\HttpException) {
+ $response->setStatusCode($exception->statusCode);
+ $response->data = [
+ 'error' => [
+ 'code' => $exception->statusCode,
+ 'message' => $exception->getMessage(),
+ 'type' => $this->getErrorType($exception->statusCode),
+ ],
+ ];
+ } elseif ($exception instanceof \yii\base\UserException) {
+ $response->setStatusCode(400);
+ $response->data = [
+ 'error' => [
+ 'code' => 400,
+ 'message' => $exception->getMessage(),
+ 'type' => 'bad_request',
+ ],
+ ];
+ } else {
+ $response->setStatusCode(500);
+ $response->data = [
+ 'error' => [
+ 'code' => 500,
+ 'message' => YII_DEBUG ? $exception->getMessage() : 'Internal server error',
+ 'type' => 'internal_error',
+ ],
+ ];
+
+ if (YII_DEBUG) {
+ $response->data['error']['trace'] = explode("\n", $exception->getTraceAsString());
+ }
+ }
+
+ $response->send();
+ }
+
+ private function getErrorType(int $code): string
+ {
+ return match ($code) {
+ 400 => 'bad_request',
+ 401 => 'unauthorized',
+ 403 => 'forbidden',
+ 404 => 'not_found',
+ 405 => 'method_not_allowed',
+ 422 => 'validation_error',
+ 429 => 'rate_limit_exceeded',
+ default => 'error',
+ };
+ }
+}
+
+// config/web.php
+return [
+ 'components' => [
+ 'errorHandler' => [
+ 'class' => \app\components\ApiErrorHandler::class,
+ ],
+ ],
+];
+```
+
+### Валидация с детальными ошибками
+```php
+<?php
+// Кастомный action для create с подробными ошибками
+class CreateAction extends \yii\rest\CreateAction
+{
+ public function run(): \yii\db\ActiveRecordInterface
+ {
+ if ($this->checkAccess) {
+ call_user_func($this->checkAccess, $this->id);
+ }
+
+ $model = new $this->modelClass([
+ 'scenario' => $this->scenario,
+ ]);
+
+ $model->load(\Yii::$app->getRequest()->getBodyParams(), '');
+
+ if (!$model->validate()) {
+ \Yii::$app->response->setStatusCode(422);
+
+ return [
+ 'error' => [
+ 'code' => 422,
+ 'message' => 'Validation failed',
+ 'type' => 'validation_error',
+ 'details' => $this->formatErrors($model->errors),
+ ],
+ ];
+ }
+
+ if (!$model->save(false)) {
+ throw new \yii\web\ServerErrorHttpException('Failed to create the object');
+ }
+
+ \Yii::$app->response->setStatusCode(201);
+
+ return $model;
+ }
+
+ private function formatErrors(array $errors): array
+ {
+ $result = [];
+
+ foreach ($errors as $field => $messages) {
+ $result[] = [
+ 'field' => $field,
+ 'messages' => $messages,
+ ];
+ }
+
+ return $result;
+ }
+}
+```
+
+## Фильтрация и поиск
+
+### DataProvider с фильтрами
+```php
+<?php
+// models/UserSearch.php
+namespace app\models;
+
+use yii\data\ActiveDataProvider;
+
+class UserSearch extends User
+{
+ public ?string $search = null;
+ public ?string $role = null;
+ public ?string $created_from = null;
+ public ?string $created_to = null;
+
+ public function rules(): array
+ {
+ return [
+ [['search', 'role', 'status'], 'safe'],
+ [['created_from', 'created_to'], 'date', 'format' => 'php:Y-m-d'],
+ ];
+ }
+
+ public function search(array $params): ActiveDataProvider
+ {
+ $query = User::find()->with(['profile']);
+
+ $dataProvider = new ActiveDataProvider([
+ 'query' => $query,
+ 'pagination' => [
+ 'pageSize' => $params['per_page'] ?? 20,
+ ],
+ 'sort' => [
+ 'defaultOrder' => ['created_at' => SORT_DESC],
+ 'attributes' => ['id', 'username', 'email', 'created_at'],
+ ],
+ ]);
+
+ $this->load($params, '');
+
+ if (!$this->validate()) {
+ return $dataProvider;
+ }
+
+ // Поиск по тексту
+ if ($this->search) {
+ $query->andWhere(['or',
+ ['ilike', 'username', $this->search],
+ ['ilike', 'email', $this->search],
+ ['ilike', 'first_name', $this->search],
+ ['ilike', 'last_name', $this->search],
+ ]);
+ }
+
+ // Фильтр по статусу
+ if ($this->status !== null) {
+ $query->andWhere(['status' => $this->status]);
+ }
+
+ // Фильтр по роли
+ if ($this->role) {
+ $query->innerJoin('auth_assignment', 'auth_assignment.user_id = users.id')
+ ->andWhere(['auth_assignment.item_name' => $this->role]);
+ }
+
+ // Фильтр по дате
+ if ($this->created_from) {
+ $query->andWhere(['>=', 'created_at', strtotime($this->created_from)]);
+ }
+ if ($this->created_to) {
+ $query->andWhere(['<=', 'created_at', strtotime($this->created_to . ' 23:59:59')]);
+ }
+
+ return $dataProvider;
+ }
+}
+
+// Использование в контроллере
+public function actionIndex(): ActiveDataProvider
+{
+ $searchModel = new UserSearch();
+ return $searchModel->search(\Yii::$app->request->queryParams);
+}
+
+// Запрос: GET /api/users?search=john&status=1&created_from=2024-01-01&per_page=10
+```
+
+## Документация API
+
+### OpenAPI/Swagger аннотации
+```php
+<?php
+/**
+ * @OA\Info(
+ * title="ERP24 API",
+ * version="1.0.0",
+ * description="REST API для ERP24"
+ * )
+ *
+ * @OA\Server(
+ * url="https://api.erp24.com/v1",
+ * description="Production"
+ * )
+ *
+ * @OA\SecurityScheme(
+ * securityScheme="bearerAuth",
+ * type="http",
+ * scheme="bearer",
+ * bearerFormat="JWT"
+ * )
+ */
+
+/**
+ * @OA\Get(
+ * path="/users",
+ * summary="Список пользователей",
+ * tags={"Users"},
+ * security={{"bearerAuth":{}}},
+ * @OA\Parameter(
+ * name="page",
+ * in="query",
+ * description="Номер страницы",
+ * @OA\Schema(type="integer", default=1)
+ * ),
+ * @OA\Parameter(
+ * name="per_page",
+ * in="query",
+ * description="Записей на странице",
+ * @OA\Schema(type="integer", default=20, maximum=100)
+ * ),
+ * @OA\Response(
+ * response=200,
+ * description="Успешный ответ",
+ * @OA\JsonContent(
+ * @OA\Property(property="data", type="array",
+ * @OA\Items(ref="#/components/schemas/User")
+ * ),
+ * @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
+ * )
+ * ),
+ * @OA\Response(response=401, description="Не авторизован")
+ * )
+ */
+public function actionIndex(): ActiveDataProvider
+{
+ // ...
+}
+
+/**
+ * @OA\Schema(
+ * schema="User",
+ * @OA\Property(property="id", type="integer", example=1),
+ * @OA\Property(property="username", type="string", example="john_doe"),
+ * @OA\Property(property="email", type="string", format="email"),
+ * @OA\Property(property="created_at", type="string", format="date-time")
+ * )
+ */
+```
+
+## CORS
+
+### Настройка CORS
+```php
+<?php
+// controllers/api/BaseController.php
+namespace app\controllers\api;
+
+use yii\rest\Controller;
+use yii\filters\Cors;
+
+class BaseController extends Controller
+{
+ public function behaviors(): array
+ {
+ $behaviors = parent::behaviors();
+
+ // CORS должен быть перед authenticator
+ $behaviors['cors'] = [
+ 'class' => Cors::class,
+ 'cors' => [
+ 'Origin' => ['https://app.example.com', 'https://admin.example.com'],
+ 'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
+ 'Access-Control-Request-Headers' => ['*'],
+ 'Access-Control-Allow-Credentials' => true,
+ 'Access-Control-Max-Age' => 86400,
+ 'Access-Control-Expose-Headers' => [
+ 'X-Pagination-Current-Page',
+ 'X-Pagination-Page-Count',
+ 'X-Pagination-Per-Page',
+ 'X-Pagination-Total-Count',
+ 'X-Rate-Limit-Limit',
+ 'X-Rate-Limit-Remaining',
+ 'X-Rate-Limit-Reset',
+ ],
+ ],
+ ];
+
+ return $behaviors;
+ }
+}
+```
+
+## Тестирование API
+
+### Codeception API тесты
+```php
+<?php
+// tests/api/UserCest.php
+class UserCest
+{
+ private string $token;
+
+ public function _before(ApiTester $I): void
+ {
+ $I->haveHttpHeader('Content-Type', 'application/json');
+ $I->haveHttpHeader('Accept', 'application/json');
+
+ // Получение токена
+ $I->sendPost('/auth/login', [
+ 'email' => 'test@example.com',
+ 'password' => 'password',
+ ]);
+ $this->token = $I->grabDataFromResponseByJsonPath('$.access_token')[0];
+ }
+
+ public function testGetUsers(ApiTester $I): void
+ {
+ $I->amBearerAuthenticated($this->token);
+ $I->sendGet('/users');
+
+ $I->seeResponseCodeIs(200);
+ $I->seeResponseIsJson();
+ $I->seeResponseContainsJson(['success' => true]);
+ $I->seeResponseMatchesJsonType([
+ 'data' => 'array',
+ 'meta' => [
+ 'totalCount' => 'integer',
+ 'pageCount' => 'integer',
+ ],
+ ]);
+ }
+
+ public function testCreateUser(ApiTester $I): void
+ {
+ $I->amBearerAuthenticated($this->token);
+ $I->sendPost('/users', [
+ 'username' => 'newuser',
+ 'email' => 'newuser@example.com',
+ 'password' => 'SecurePass123!',
+ ]);
+
+ $I->seeResponseCodeIs(201);
+ $I->seeResponseContainsJson([
+ 'username' => 'newuser',
+ 'email' => 'newuser@example.com',
+ ]);
+ }
+
+ public function testValidationError(ApiTester $I): void
+ {
+ $I->amBearerAuthenticated($this->token);
+ $I->sendPost('/users', [
+ 'username' => '', // Пустое имя
+ 'email' => 'invalid-email', // Невалидный email
+ ]);
+
+ $I->seeResponseCodeIs(422);
+ $I->seeResponseContainsJson([
+ 'error' => [
+ 'code' => 422,
+ 'type' => 'validation_error',
+ ],
+ ]);
+ }
+
+ public function testUnauthorized(ApiTester $I): void
+ {
+ $I->sendGet('/users/me');
+
+ $I->seeResponseCodeIs(401);
+ $I->seeResponseContainsJson([
+ 'error' => [
+ 'code' => 401,
+ 'type' => 'unauthorized',
+ ],
+ ]);
+ }
+}
+```
+
+## Рекомендации для Claude Code
+
+1. **ActiveController** - используйте для стандартных CRUD операций
+2. **Сериализация** - настраивайте fields() и extraFields() в моделях
+3. **Аутентификация** - используйте JWT или Bearer tokens
+4. **Версионирование** - используйте префикс /api/v1/, /api/v2/
+5. **Ошибки** - возвращайте структурированные JSON ответы
+6. **Rate Limiting** - реализуйте RateLimitInterface
+7. **CORS** - настраивайте для cross-origin запросов
+8. **Документация** - используйте OpenAPI/Swagger аннотации
--- /dev/null
+# Yii2 Widgets - Виджеты и компоненты
+
+## Базовые виджеты
+
+### GridView
+```php
+<?php
+use yii\grid\GridView;
+use yii\grid\ActionColumn;
+use yii\grid\CheckboxColumn;
+use yii\helpers\Html;
+
+echo GridView::widget([
+ 'dataProvider' => $dataProvider,
+ 'filterModel' => $searchModel,
+ 'tableOptions' => ['class' => 'table table-striped table-bordered'],
+ 'layout' => "{summary}\n{items}\n{pager}",
+ 'columns' => [
+ // Checkbox колонка
+ ['class' => CheckboxColumn::class],
+
+ // Serial column (№ п/п)
+ ['class' => \yii\grid\SerialColumn::class],
+
+ // Простые колонки
+ 'id',
+ 'username',
+ 'email:email', // Формат email
+
+ // Кастомная колонка
+ [
+ 'attribute' => 'status',
+ 'label' => 'Статус',
+ 'format' => 'html',
+ 'value' => function ($model) {
+ $statusLabels = [
+ 0 => '<span class="badge bg-secondary">Неактивен</span>',
+ 1 => '<span class="badge bg-success">Активен</span>',
+ 2 => '<span class="badge bg-danger">Заблокирован</span>',
+ ];
+ return $statusLabels[$model->status] ?? 'Неизвестно';
+ },
+ 'filter' => [
+ 0 => 'Неактивен',
+ 1 => 'Активен',
+ 2 => 'Заблокирован',
+ ],
+ ],
+
+ // Дата
+ [
+ 'attribute' => 'created_at',
+ 'format' => ['datetime', 'php:d.m.Y H:i'],
+ 'filter' => \yii\jui\DatePicker::widget([
+ 'model' => $searchModel,
+ 'attribute' => 'created_at',
+ 'dateFormat' => 'php:Y-m-d',
+ 'options' => ['class' => 'form-control'],
+ ]),
+ ],
+
+ // Связанные данные
+ [
+ 'attribute' => 'department_id',
+ 'label' => 'Отдел',
+ 'value' => 'department.name', // Связь hasOne
+ 'filter' => \yii\helpers\ArrayHelper::map(
+ \app\models\Department::find()->all(),
+ 'id',
+ 'name'
+ ),
+ ],
+
+ // Кнопки действий
+ [
+ 'class' => ActionColumn::class,
+ 'template' => '{view} {update} {delete} {activate}',
+ 'buttons' => [
+ 'activate' => function ($url, $model, $key) {
+ if ($model->status === 0) {
+ return Html::a(
+ '<i class="fas fa-check"></i>',
+ ['activate', 'id' => $model->id],
+ [
+ 'title' => 'Активировать',
+ 'data-method' => 'post',
+ 'data-confirm' => 'Активировать пользователя?',
+ ]
+ );
+ }
+ return '';
+ },
+ ],
+ 'visibleButtons' => [
+ 'delete' => function ($model) {
+ return \Yii::$app->user->can('admin');
+ },
+ ],
+ ],
+ ],
+]);
+```
+
+### ListView
+```php
+<?php
+use yii\widgets\ListView;
+
+echo ListView::widget([
+ 'dataProvider' => $dataProvider,
+ 'itemView' => '_item', // views/post/_item.php
+ 'itemOptions' => ['class' => 'post-item'],
+ 'layout' => "{summary}\n{items}\n{pager}",
+ 'emptyText' => 'Записи не найдены',
+ 'emptyTextOptions' => ['class' => 'alert alert-info'],
+ 'summary' => 'Показано {begin}-{end} из {totalCount}',
+ 'pager' => [
+ 'class' => \yii\widgets\LinkPager::class,
+ 'maxButtonCount' => 5,
+ 'firstPageLabel' => 'Первая',
+ 'lastPageLabel' => 'Последняя',
+ ],
+ 'viewParams' => [
+ 'showAuthor' => true,
+ ],
+]);
+
+// views/post/_item.php
+<?php
+/** @var app\models\Post $model */
+/** @var int $index */
+/** @var yii\widgets\ListView $widget */
+/** @var bool $showAuthor */
+?>
+<div class="post-card">
+ <h3><?= Html::encode($model->title) ?></h3>
+ <?php if ($showAuthor): ?>
+ <p class="author">Автор: <?= Html::encode($model->user->username) ?></p>
+ <?php endif; ?>
+ <p><?= Html::encode($model->excerpt) ?></p>
+ <?= Html::a('Читать далее', ['post/view', 'id' => $model->id]) ?>
+</div>
+```
+
+### DetailView
+```php
+<?php
+use yii\widgets\DetailView;
+
+echo DetailView::widget([
+ 'model' => $model,
+ 'options' => ['class' => 'table table-striped detail-view'],
+ 'attributes' => [
+ 'id',
+ 'username',
+ 'email:email',
+ 'phone:text',
+
+ // Кастомный атрибут
+ [
+ 'attribute' => 'status',
+ 'format' => 'html',
+ 'value' => function ($model) {
+ return $model->status
+ ? '<span class="badge bg-success">Активен</span>'
+ : '<span class="badge bg-danger">Неактивен</span>';
+ },
+ ],
+
+ // Без атрибута (вычисляемое значение)
+ [
+ 'label' => 'Полное имя',
+ 'value' => $model->first_name . ' ' . $model->last_name,
+ ],
+
+ // Связанные данные
+ [
+ 'attribute' => 'department_id',
+ 'value' => $model->department->name ?? 'Не указан',
+ ],
+
+ // Изображение
+ [
+ 'attribute' => 'avatar',
+ 'format' => ['image', ['width' => 100, 'height' => 100]],
+ 'value' => $model->getAvatarUrl(),
+ ],
+
+ // Дата
+ [
+ 'attribute' => 'created_at',
+ 'format' => ['datetime', 'php:d.m.Y H:i:s'],
+ ],
+
+ // Boolean
+ [
+ 'attribute' => 'is_verified',
+ 'format' => 'boolean',
+ ],
+
+ // URL
+ [
+ 'attribute' => 'website',
+ 'format' => 'url',
+ ],
+ ],
+]);
+```
+
+## Виджеты форм
+
+### ActiveForm
+```php
+<?php
+use yii\widgets\ActiveForm;
+use yii\helpers\Html;
+
+$form = ActiveForm::begin([
+ 'id' => 'user-form',
+ 'options' => ['class' => 'form-horizontal', 'enctype' => 'multipart/form-data'],
+ 'enableAjaxValidation' => true,
+ 'enableClientValidation' => true,
+ 'validateOnBlur' => true,
+ 'validateOnChange' => true,
+ 'fieldConfig' => [
+ 'template' => "{label}\n{input}\n{hint}\n{error}",
+ 'labelOptions' => ['class' => 'form-label'],
+ 'inputOptions' => ['class' => 'form-control'],
+ 'errorOptions' => ['class' => 'invalid-feedback'],
+ ],
+]);
+?>
+
+<?= $form->field($model, 'username')->textInput(['maxlength' => true]) ?>
+
+<?= $form->field($model, 'email')->input('email') ?>
+
+<?= $form->field($model, 'password')->passwordInput() ?>
+
+<?= $form->field($model, 'status')->dropDownList([
+ 0 => 'Неактивен',
+ 1 => 'Активен',
+], ['prompt' => 'Выберите статус']) ?>
+
+<?= $form->field($model, 'roles')->checkboxList([
+ 'admin' => 'Администратор',
+ 'manager' => 'Менеджер',
+ 'user' => 'Пользователь',
+]) ?>
+
+<?= $form->field($model, 'gender')->radioList([
+ 'male' => 'Мужской',
+ 'female' => 'Женский',
+]) ?>
+
+<?= $form->field($model, 'avatar')->fileInput(['accept' => 'image/*']) ?>
+
+<?= $form->field($model, 'description')->textarea(['rows' => 5]) ?>
+
+<?= $form->field($model, 'agree')->checkbox() ?>
+
+<?= $form->field($model, 'department_id')->widget(\yii\widgets\Select2::class, [
+ 'data' => \yii\helpers\ArrayHelper::map(
+ \app\models\Department::find()->all(),
+ 'id',
+ 'name'
+ ),
+ 'options' => ['placeholder' => 'Выберите отдел'],
+ 'pluginOptions' => ['allowClear' => true],
+]) ?>
+
+<div class="form-group">
+ <?= Html::submitButton('Сохранить', ['class' => 'btn btn-primary']) ?>
+ <?= Html::resetButton('Сбросить', ['class' => 'btn btn-secondary']) ?>
+</div>
+
+<?php ActiveForm::end(); ?>
+```
+
+### Pjax
+```php
+<?php
+use yii\widgets\Pjax;
+
+// Обёртка для Ajax-обновления
+Pjax::begin([
+ 'id' => 'users-pjax',
+ 'timeout' => 5000,
+ 'enablePushState' => true,
+ 'enableReplaceState' => false,
+ 'clientOptions' => [
+ 'method' => 'GET',
+ ],
+]);
+?>
+
+<?= GridView::widget([
+ 'dataProvider' => $dataProvider,
+ 'filterModel' => $searchModel,
+ // ...
+]) ?>
+
+<?php Pjax::end(); ?>
+
+<!-- JavaScript для программного обновления -->
+<script>
+// Обновить содержимое
+$.pjax.reload({container: '#users-pjax'});
+
+// С параметрами
+$.pjax.reload({
+ container: '#users-pjax',
+ url: '/users?status=1',
+ push: false
+});
+</script>
+```
+
+## Создание виджетов
+
+### Простой виджет
+```php
+<?php
+// widgets/Alert.php
+namespace app\widgets;
+
+use yii\base\Widget;
+use yii\helpers\Html;
+
+class Alert extends Widget
+{
+ public string $type = 'info'; // success, warning, danger, info
+ public string $message = '';
+ public bool $dismissible = true;
+
+ public function run(): string
+ {
+ if (empty($this->message)) {
+ return '';
+ }
+
+ $class = "alert alert-{$this->type}";
+
+ if ($this->dismissible) {
+ $class .= ' alert-dismissible fade show';
+ }
+
+ $content = Html::encode($this->message);
+
+ if ($this->dismissible) {
+ $content .= Html::button('×', [
+ 'class' => 'btn-close',
+ 'data-bs-dismiss' => 'alert',
+ ]);
+ }
+
+ return Html::tag('div', $content, [
+ 'class' => $class,
+ 'role' => 'alert',
+ ]);
+ }
+}
+
+// Использование
+echo \app\widgets\Alert::widget([
+ 'type' => 'success',
+ 'message' => 'Операция выполнена успешно!',
+]);
+```
+
+### Виджет с представлением
+```php
+<?php
+// widgets/UserCard.php
+namespace app\widgets;
+
+use yii\base\Widget;
+use app\models\User;
+
+class UserCard extends Widget
+{
+ public ?User $user = null;
+ public bool $showEmail = true;
+ public bool $showAvatar = true;
+ public string $size = 'medium'; // small, medium, large
+
+ public function init(): void
+ {
+ parent::init();
+
+ if ($this->user === null) {
+ throw new \yii\base\InvalidConfigException('User is required');
+ }
+ }
+
+ public function run(): string
+ {
+ return $this->render('user-card', [
+ 'user' => $this->user,
+ 'showEmail' => $this->showEmail,
+ 'showAvatar' => $this->showAvatar,
+ 'size' => $this->size,
+ ]);
+ }
+}
+
+// widgets/views/user-card.php
+<?php
+use yii\helpers\Html;
+
+/** @var app\models\User $user */
+/** @var bool $showEmail */
+/** @var bool $showAvatar */
+/** @var string $size */
+
+$sizeClass = match ($size) {
+ 'small' => 'user-card-sm',
+ 'large' => 'user-card-lg',
+ default => 'user-card-md',
+};
+?>
+
+<div class="user-card <?= $sizeClass ?>">
+ <?php if ($showAvatar): ?>
+ <div class="user-avatar">
+ <?= Html::img($user->getAvatarUrl(), ['alt' => $user->username]) ?>
+ </div>
+ <?php endif; ?>
+
+ <div class="user-info">
+ <h4><?= Html::encode($user->username) ?></h4>
+ <?php if ($showEmail): ?>
+ <p><?= Html::mailto($user->email) ?></p>
+ <?php endif; ?>
+ </div>
+</div>
+
+// Использование
+echo \app\widgets\UserCard::widget([
+ 'user' => $user,
+ 'showEmail' => false,
+ 'size' => 'large',
+]);
+```
+
+### Виджет с Asset Bundle
+```php
+<?php
+// widgets/assets/ChartAsset.php
+namespace app\widgets\assets;
+
+use yii\web\AssetBundle;
+
+class ChartAsset extends AssetBundle
+{
+ public $sourcePath = '@app/widgets/assets/dist';
+
+ public $css = [
+ 'css/chart.css',
+ ];
+
+ public $js = [
+ 'js/chart.min.js',
+ 'js/chart-widget.js',
+ ];
+
+ public $depends = [
+ \yii\web\JqueryAsset::class,
+ ];
+}
+
+// widgets/Chart.php
+namespace app\widgets;
+
+use yii\base\Widget;
+use yii\helpers\Json;
+use app\widgets\assets\ChartAsset;
+
+class Chart extends Widget
+{
+ public string $type = 'line'; // line, bar, pie, doughnut
+ public array $data = [];
+ public array $options = [];
+ public int $width = 400;
+ public int $height = 300;
+
+ public function run(): string
+ {
+ $this->registerAssets();
+
+ $id = $this->getId();
+ $config = Json::encode([
+ 'type' => $this->type,
+ 'data' => $this->data,
+ 'options' => $this->options,
+ ]);
+
+ $js = "new ChartWidget('#{$id}', {$config});";
+ $this->view->registerJs($js);
+
+ return "<canvas id=\"{$id}\" width=\"{$this->width}\" height=\"{$this->height}\"></canvas>";
+ }
+
+ protected function registerAssets(): void
+ {
+ ChartAsset::register($this->view);
+ }
+}
+
+// Использование
+echo \app\widgets\Chart::widget([
+ 'type' => 'bar',
+ 'data' => [
+ 'labels' => ['Январь', 'Февраль', 'Март'],
+ 'datasets' => [
+ [
+ 'label' => 'Продажи',
+ 'data' => [100, 150, 200],
+ ],
+ ],
+ ],
+ 'options' => [
+ 'responsive' => true,
+ ],
+]);
+```
+
+### Виджет с буферизацией контента
+```php
+<?php
+// widgets/Panel.php
+namespace app\widgets;
+
+use yii\base\Widget;
+use yii\helpers\Html;
+
+class Panel extends Widget
+{
+ public string $title = '';
+ public string $type = 'default'; // default, primary, success, warning, danger
+ public array $headerOptions = [];
+ public array $bodyOptions = [];
+ public array $footerOptions = [];
+ public ?string $footer = null;
+
+ public function init(): void
+ {
+ parent::init();
+ ob_start();
+ }
+
+ public function run(): string
+ {
+ $content = ob_get_clean();
+
+ $header = '';
+ if ($this->title) {
+ $headerOptions = array_merge(['class' => 'card-header'], $this->headerOptions);
+ $header = Html::tag('div', Html::encode($this->title), $headerOptions);
+ }
+
+ $bodyOptions = array_merge(['class' => 'card-body'], $this->bodyOptions);
+ $body = Html::tag('div', $content, $bodyOptions);
+
+ $footer = '';
+ if ($this->footer !== null) {
+ $footerOptions = array_merge(['class' => 'card-footer'], $this->footerOptions);
+ $footer = Html::tag('div', $this->footer, $footerOptions);
+ }
+
+ return Html::tag('div', $header . $body . $footer, [
+ 'class' => "card border-{$this->type}",
+ ]);
+ }
+}
+
+// Использование
+<?php \app\widgets\Panel::begin([
+ 'title' => 'Информация о пользователе',
+ 'type' => 'primary',
+]); ?>
+
+<p>Здесь содержимое панели</p>
+<?= DetailView::widget(['model' => $user, 'attributes' => ['username', 'email']]) ?>
+
+<?php \app\widgets\Panel::end(); ?>
+```
+
+## Стандартные виджеты Yii2
+
+### Menu
+```php
+<?php
+use yii\widgets\Menu;
+
+echo Menu::widget([
+ 'items' => [
+ ['label' => 'Главная', 'url' => ['site/index']],
+ ['label' => 'О нас', 'url' => ['site/about']],
+ [
+ 'label' => 'Продукты',
+ 'url' => ['product/index'],
+ 'items' => [
+ ['label' => 'Категория 1', 'url' => ['product/category', 'id' => 1]],
+ ['label' => 'Категория 2', 'url' => ['product/category', 'id' => 2]],
+ ],
+ ],
+ [
+ 'label' => 'Админка',
+ 'url' => ['admin/index'],
+ 'visible' => \Yii::$app->user->can('admin'),
+ ],
+ [
+ 'label' => 'Войти',
+ 'url' => ['site/login'],
+ 'visible' => \Yii::$app->user->isGuest,
+ ],
+ [
+ 'label' => 'Выйти (' . \Yii::$app->user->identity?->username . ')',
+ 'url' => ['site/logout'],
+ 'visible' => !\Yii::$app->user->isGuest,
+ 'linkOptions' => ['data-method' => 'post'],
+ ],
+ ],
+ 'options' => ['class' => 'nav nav-pills'],
+ 'itemOptions' => ['class' => 'nav-item'],
+ 'linkTemplate' => '<a class="nav-link{active}" href="{url}">{label}</a>',
+ 'submenuTemplate' => "\n<ul class=\"dropdown-menu\">\n{items}\n</ul>\n",
+ 'activateParents' => true,
+]);
+```
+
+### Breadcrumbs
+```php
+<?php
+use yii\widgets\Breadcrumbs;
+
+echo Breadcrumbs::widget([
+ 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [],
+ 'homeLink' => [
+ 'label' => '<i class="fas fa-home"></i> Главная',
+ 'url' => ['/'],
+ 'encode' => false,
+ ],
+ 'options' => ['class' => 'breadcrumb'],
+ 'itemTemplate' => "<li class=\"breadcrumb-item\">{link}</li>\n",
+ 'activeItemTemplate' => "<li class=\"breadcrumb-item active\">{link}</li>\n",
+]);
+
+// В контроллере или представлении
+$this->params['breadcrumbs'][] = ['label' => 'Пользователи', 'url' => ['index']];
+$this->params['breadcrumbs'][] = ['label' => $model->username, 'url' => ['view', 'id' => $model->id]];
+$this->params['breadcrumbs'][] = 'Редактирование';
+```
+
+### LinkPager
+```php
+<?php
+use yii\widgets\LinkPager;
+
+echo LinkPager::widget([
+ 'pagination' => $dataProvider->pagination,
+ 'options' => ['class' => 'pagination justify-content-center'],
+ 'linkContainerOptions' => ['class' => 'page-item'],
+ 'linkOptions' => ['class' => 'page-link'],
+ 'disabledListItemSubTagOptions' => ['class' => 'page-link'],
+ 'maxButtonCount' => 7,
+ 'firstPageLabel' => '«',
+ 'lastPageLabel' => '»',
+ 'prevPageLabel' => '‹',
+ 'nextPageLabel' => '›',
+ 'activePageCssClass' => 'active',
+ 'disabledPageCssClass' => 'disabled',
+]);
+```
+
+### MaskedInput
+```php
+<?php
+use yii\widgets\MaskedInput;
+
+// Телефон
+echo $form->field($model, 'phone')->widget(MaskedInput::class, [
+ 'mask' => '+7 (999) 999-99-99',
+]);
+
+// Дата
+echo $form->field($model, 'birth_date')->widget(MaskedInput::class, [
+ 'mask' => '99.99.9999',
+]);
+
+// ИНН
+echo $form->field($model, 'inn')->widget(MaskedInput::class, [
+ 'mask' => '9999999999', // 10 цифр
+]);
+
+// Паспорт
+echo $form->field($model, 'passport')->widget(MaskedInput::class, [
+ 'mask' => '99 99 999999',
+]);
+
+// Кредитная карта
+echo $form->field($model, 'card_number')->widget(MaskedInput::class, [
+ 'mask' => '9999 9999 9999 9999',
+]);
+```
+
+## Bootstrap виджеты
+
+### Modal
+```php
+<?php
+use yii\bootstrap5\Modal;
+
+Modal::begin([
+ 'id' => 'user-modal',
+ 'title' => '<h4>Добавить пользователя</h4>',
+ 'size' => Modal::SIZE_LARGE,
+ 'centerVertical' => true,
+ 'scrollable' => true,
+ 'footer' => Html::button('Закрыть', [
+ 'class' => 'btn btn-secondary',
+ 'data-bs-dismiss' => 'modal',
+ ]) . Html::button('Сохранить', [
+ 'class' => 'btn btn-primary',
+ 'id' => 'save-btn',
+ ]),
+]);
+?>
+
+<div id="modal-content">
+ <!-- Контент загружается через Ajax -->
+</div>
+
+<?php Modal::end(); ?>
+
+<!-- Кнопка открытия -->
+<?= Html::button('Добавить', [
+ 'class' => 'btn btn-success',
+ 'data-bs-toggle' => 'modal',
+ 'data-bs-target' => '#user-modal',
+]) ?>
+
+<script>
+$('#user-modal').on('show.bs.modal', function () {
+ $.get('/user/create-form', function(data) {
+ $('#modal-content').html(data);
+ });
+});
+</script>
+```
+
+### Tabs
+```php
+<?php
+use yii\bootstrap5\Tabs;
+
+echo Tabs::widget([
+ 'items' => [
+ [
+ 'label' => 'Основное',
+ 'content' => $this->render('_tab_main', ['model' => $model]),
+ 'active' => true,
+ ],
+ [
+ 'label' => 'Контакты',
+ 'content' => $this->render('_tab_contacts', ['model' => $model]),
+ ],
+ [
+ 'label' => 'Документы',
+ 'content' => $this->render('_tab_documents', ['model' => $model]),
+ 'visible' => \Yii::$app->user->can('viewDocuments'),
+ ],
+ [
+ 'label' => 'Настройки',
+ 'url' => ['user/settings', 'id' => $model->id],
+ 'linkOptions' => ['data-pjax' => 0],
+ ],
+ ],
+ 'options' => ['class' => 'nav-tabs-custom'],
+ 'itemOptions' => ['class' => 'nav-item'],
+ 'encodeLabels' => false,
+]);
+```
+
+### Accordion
+```php
+<?php
+use yii\bootstrap5\Accordion;
+
+echo Accordion::widget([
+ 'items' => [
+ [
+ 'label' => 'Раздел 1',
+ 'content' => 'Содержимое первого раздела...',
+ 'contentOptions' => ['class' => 'accordion-body'],
+ ],
+ [
+ 'label' => 'Раздел 2',
+ 'content' => $this->render('_section2'),
+ 'options' => ['class' => 'accordion-item'],
+ ],
+ [
+ 'label' => 'Раздел 3 (свёрнут)',
+ 'content' => 'Содержимое третьего раздела...',
+ 'expand' => false,
+ ],
+ ],
+ 'options' => ['class' => 'accordion', 'id' => 'faq-accordion'],
+ 'autoCloseItems' => true,
+]);
+```
+
+### Progress
+```php
+<?php
+use yii\bootstrap5\Progress;
+
+// Простой прогресс
+echo Progress::widget([
+ 'percent' => 75,
+ 'barOptions' => ['class' => 'bg-success'],
+ 'label' => '75%',
+]);
+
+// Стриповый прогресс
+echo Progress::widget([
+ 'percent' => 60,
+ 'barOptions' => ['class' => 'progress-bar-striped progress-bar-animated bg-info'],
+]);
+
+// Множественные бары
+echo Progress::widget([
+ 'bars' => [
+ ['percent' => 30, 'options' => ['class' => 'bg-success']],
+ ['percent' => 20, 'options' => ['class' => 'bg-warning']],
+ ['percent' => 10, 'options' => ['class' => 'bg-danger']],
+ ],
+]);
+```
+
+## Ajax виджеты
+
+### Динамическая загрузка виджета
+```php
+<?php
+// widgets/AjaxWidget.php
+namespace app\widgets;
+
+use yii\base\Widget;
+use yii\helpers\Json;
+use yii\helpers\Url;
+
+class AjaxWidget extends Widget
+{
+ public string $url;
+ public string $loadingText = 'Загрузка...';
+ public int $refreshInterval = 0; // 0 = без автообновления
+ public array $ajaxOptions = [];
+
+ public function run(): string
+ {
+ $id = $this->getId();
+ $url = Url::to($this->url);
+ $options = Json::encode($this->ajaxOptions);
+
+ $js = <<<JS
+(function() {
+ var container = $('#{$id}');
+
+ function loadContent() {
+ $.ajax($.extend({
+ url: '{$url}',
+ success: function(data) {
+ container.html(data);
+ },
+ error: function() {
+ container.html('<div class="alert alert-danger">Ошибка загрузки</div>');
+ }
+ }, {$options}));
+ }
+
+ loadContent();
+
+ if ({$this->refreshInterval} > 0) {
+ setInterval(loadContent, {$this->refreshInterval} * 1000);
+ }
+})();
+JS;
+
+ $this->view->registerJs($js);
+
+ return "<div id=\"{$id}\">{$this->loadingText}</div>";
+ }
+}
+
+// Использование
+echo \app\widgets\AjaxWidget::widget([
+ 'url' => ['dashboard/stats'],
+ 'refreshInterval' => 30, // Обновлять каждые 30 секунд
+]);
+```
+
+### Infinite Scroll
+```php
+<?php
+// widgets/InfiniteScroll.php
+namespace app\widgets;
+
+use yii\base\Widget;
+use yii\data\Pagination;
+use yii\helpers\Url;
+
+class InfiniteScroll extends Widget
+{
+ public Pagination $pagination;
+ public string $container = '.items';
+ public string $item = '.item';
+ public string $loadingText = 'Загрузка...';
+
+ public function run(): string
+ {
+ if (!$this->pagination->getPage() < $this->pagination->getPageCount() - 1) {
+ return '';
+ }
+
+ $nextPage = $this->pagination->getPage() + 2;
+ $nextUrl = Url::current(['page' => $nextPage]);
+
+ $js = <<<JS
+(function() {
+ var loading = false;
+ var page = {$nextPage};
+ var maxPage = {$this->pagination->getPageCount()};
+
+ $(window).scroll(function() {
+ if (loading || page > maxPage) return;
+
+ if ($(window).scrollTop() + $(window).height() > $(document).height() - 200) {
+ loading = true;
+
+ var loader = $('<div class="infinite-loader">{$this->loadingText}</div>');
+ $('{$this->container}').append(loader);
+
+ $.get('{$nextUrl}'.replace('page=' + (page), 'page=' + page), function(data) {
+ loader.remove();
+ var items = $(data).find('{$this->item}');
+ $('{$this->container}').append(items);
+ page++;
+ loading = false;
+ });
+ }
+ });
+})();
+JS;
+
+ $this->view->registerJs($js);
+
+ return '';
+ }
+}
+```
+
+## Кастомизация стандартных виджетов
+
+### Расширение GridView
+```php
+<?php
+// widgets/CustomGridView.php
+namespace app\widgets;
+
+use yii\grid\GridView;
+
+class CustomGridView extends GridView
+{
+ public $layout = "{toolbar}\n{summary}\n{items}\n{pager}";
+ public $tableOptions = ['class' => 'table table-striped table-hover'];
+ public $summaryOptions = ['class' => 'summary text-muted small'];
+ public $pager = [
+ 'class' => \yii\bootstrap5\LinkPager::class,
+ 'maxButtonCount' => 7,
+ ];
+ public array $toolbar = [];
+
+ public function run(): string
+ {
+ // Регистрация CSS
+ $this->view->registerCss(<<<CSS
+.grid-view .summary { margin-bottom: 10px; }
+.grid-view .toolbar { margin-bottom: 15px; }
+CSS
+ );
+
+ return parent::run();
+ }
+
+ public function renderSection($name): string|false
+ {
+ if ($name === '{toolbar}') {
+ return $this->renderToolbar();
+ }
+
+ return parent::renderSection($name);
+ }
+
+ protected function renderToolbar(): string
+ {
+ if (empty($this->toolbar)) {
+ return '';
+ }
+
+ $buttons = [];
+ foreach ($this->toolbar as $button) {
+ $buttons[] = $button;
+ }
+
+ return '<div class="toolbar btn-group">' . implode('', $buttons) . '</div>';
+ }
+}
+
+// Использование
+echo \app\widgets\CustomGridView::widget([
+ 'dataProvider' => $dataProvider,
+ 'filterModel' => $searchModel,
+ 'toolbar' => [
+ Html::a('<i class="fas fa-plus"></i> Добавить', ['create'], ['class' => 'btn btn-success']),
+ Html::a('<i class="fas fa-download"></i> Экспорт', ['export'], ['class' => 'btn btn-info']),
+ ],
+ 'columns' => [
+ // ...
+ ],
+]);
+```
+
+## Рекомендации для Claude Code
+
+1. **GridView** - используйте для табличного отображения данных с сортировкой и фильтрацией
+2. **ListView** - для карточного или списочного представления
+3. **DetailView** - для отображения одной записи
+4. **ActiveForm** - для всех форм с валидацией
+5. **Pjax** - для Ajax-обновления без перезагрузки страницы
+6. **AssetBundle** - регистрируйте CSS/JS через бандлы
+7. **begin()/end()** - используйте для виджетов с буферизацией контента
+8. **Кастомные виджеты** - выносите повторяющийся UI в виджеты
--- /dev/null
+# PHP 8 и Yii2 Style Guide
+
+Современный набор гайдлайнов для разработки на PHP 8 и Yii2, оптимизированный для использования с Claude Code и другими AI-ассистентами.
+
+## Структура документации
+
+### Основы PHP
+- [`01-php-basics.md`](./01-php-basics.md) - Базовые правила форматирования и синтаксиса
+- [`02-php-naming.md`](./02-php-naming.md) - Соглашения об именовании
+- [`03-php-methods.md`](./03-php-methods.md) - Методы и функции
+- [`04-php-classes.md`](./04-php-classes.md) - Классы, интерфейсы и трейты
+- [`05-php-collections.md`](./05-php-collections.md) - Работа с массивами
+- [`06-php-strings.md`](./06-php-strings.md) - Работа со строками
+- [`07-php-flow-control.md`](./07-php-flow-control.md) - Управление потоком выполнения
+- [`08-php-exceptions.md`](./08-php-exceptions.md) - Обработка исключений
+- [`09-php-closures.md`](./09-php-closures.md) - Замыкания и колбэки
+
+### Yii2-специфичные правила
+- [`10-yii2-structure.md`](./10-yii2-structure.md) - Структура Yii2 приложения
+- [`11-yii2-models.md`](./11-yii2-models.md) - Модели и ActiveRecord
+- [`12-yii2-controllers.md`](./12-yii2-controllers.md) - Контроллеры
+- [`13-yii2-views.md`](./13-yii2-views.md) - Представления и шаблоны
+- [`14-yii2-routing.md`](./14-yii2-routing.md) - Маршрутизация
+- [`15-yii2-migrations.md`](./15-yii2-migrations.md) - Миграции базы данных
+- [`16-yii2-testing.md`](./16-yii2-testing.md) - Тестирование
+- [`17-yii2-security.md`](./17-yii2-security.md) - Безопасность
+- [`18-yii2-performance.md`](./18-yii2-performance.md) - Производительность
+- [`19-yii2-api.md`](./19-yii2-api.md) - REST API
+- [`20-yii2-widgets.md`](./20-yii2-widgets.md) - Виджеты и компоненты
+
+## Цель
+
+Эти гайдлайны созданы для:
+1. Обеспечения консистентности кода в PHP 8 / Yii2 проектах
+2. Оптимизации работы с AI-ассистентами (Claude Code)
+3. Следования современным best practices (PSR-12, Yii2 Coding Standards)
+4. Упрощения code review и командной работы
+
+## Как использовать
+
+### С Claude Code
+Поместите эти файлы в корень вашего проекта в папку `.claude/` или `docs/style-guide/`:
+```bash
+project/
+├── .claude/
+│ └── style-guide/
+│ ├── README.md
+│ ├── 01-php-basics.md
+│ ├── 02-php-naming.md
+│ └── ...
+```
+
+### Приоритеты
+1. **Читаемость** превыше всего
+2. **Консистентность** внутри проекта важнее идеального следования гайду
+3. **Практичность** - если правило мешает решению задачи, его можно нарушить с обоснованием
+
+## Версии
+
+- PHP: 8.1+
+- Yii2: 2.0.45+
+- Обновлено: Декабрь 2024
+
+## Стандарты
+
+Данный гайд основан на:
+- PSR-1: Basic Coding Standard
+- PSR-4: Autoloading Standard
+- PSR-12: Extended Coding Style Guide
+- Yii2 Coding Standards
+- PHP-FIG рекомендации
+
+## Конфигурация
+
+Рекомендуется использовать PHP_CodeSniffer и PHP-CS-Fixer с конфигурацией, соответствующей PSR-12 и Yii2 стандартам.
+
+```bash
+# Установка инструментов
+composer require --dev squizlabs/php_codesniffer
+composer require --dev friendsofphp/php-cs-fixer
+
+# Проверка кода
+./vendor/bin/phpcs --standard=PSR12 src/
+./vendor/bin/php-cs-fixer fix --dry-run --diff
+```
* @property float $quantity количество
* @property integer|null $type_id id типа списания
* @property string|null $type_guid guid типа списания
+ * @property bool|int $attachment_cleared Флаг очистки вложений
+ * @property string|null $attachment_cleared_at Дата очистки вложений
* @property WriteOffsProductsErp[] $writeOffsProductsErps
*/
class WriteOffsErp extends \yii\db\ActiveRecord
return [
[['guid', 'created_admin_id', 'store_id', 'store_guid', 'number', 'date', 'write_offs_type', 'created_at'], 'required'],
[['quantity'], 'required', 'message' => 'Поле Количество обязательно для заполнения'],
- [['status', 'created_admin_id', 'updated_admin_id', 'cause_id', 'confirm_admin_id', 'cause_group_id', 'store_id', 'active', 'deleted_admin_id'], 'integer'],
- [['date', 'created_at', 'send_at', 'updated_at', 'confirm_at', 'deleted_at', 'modelsProducts'], 'safe'],
+ [['status', 'created_admin_id', 'updated_admin_id', 'cause_id', 'confirm_admin_id', 'cause_group_id', 'store_id', 'active', 'deleted_admin_id', 'attachment_cleared'], 'integer'],
+ [['date', 'created_at', 'send_at', 'updated_at', 'confirm_at', 'deleted_at', 'modelsProducts', 'attachment_cleared_at'], 'safe'],
[['based_on', 'comment', 'error_text', 'write_offs_type'], 'string'],
[['summ', 'summ_retail', 'quantity', 'type_id'], 'number'],
[['guid', 'store_guid', 'type_guid', 'number', 'number_1c', 'date', 'based_on', 'write_offs_type', 'created_at', 'updated_at', 'deleted_at', 'confirm_at', 'send_at'], 'string', 'max' => 100],
'summ_retail' => 'Summ Retail',
'type_id' => 'id типа списания',
'type_guid' => 'guid типа списания',
+ 'attachment_cleared' => 'Вложения очищены',
+ 'attachment_cleared_at' => 'Дата очистки вложений',
'send_at' => 'Дата отправки в 1с',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
* Возвращает вложения для всех документов, дата которых старше одного месяца.
* Можно передать свою граничную дату в $borderDate (формат Y-m-d или Y-m-d H:i:s).
* Структура ответа: массив элементов с ключами document_id, date, attachments.
+ * Возвращает только документы с attachment_cleared = 0 (не очищенные).
*/
public static function getAttachmentsOlderThanMonth(?string $borderDate = null): array
{
$docs = self::find()
->andWhere(['status' => self::STATUS_CREATED_1C])
+ ->andWhere(['attachment_cleared' => 0])
->andWhere(['<', 'date', $borderDate])
->all();
* Возвращает вложения для всех документов, дата которых старше одного месяца.
* Можно передать свою граничную дату в $borderDate (формат Y-m-d или Y-m-d H:i:s).
* Структура ответа: массив элементов с ключами document_id, date, attachments.
+ * Возвращает только документы с attachment_cleared = 0 (не очищенные).
*/
public static function getAttachmentsOlderThanMonthList(?string $borderDate = null): array
{
$docs = self::find()
->andWhere(['status' => self::STATUS_CREATED_1C])
+ ->andWhere(['attachment_cleared' => 0])
->andWhere(['<', 'date', $borderDate])
->all();