[{"data":1,"prerenderedAt":2184},["ShallowReactive",2],{"tag-architecture":3},[4,1144],{"id":5,"title":6,"body":7,"category":1129,"cover":1130,"date":1131,"description":1132,"draft":1133,"extension":1134,"meta":1135,"navigation":436,"path":1136,"seo":1137,"stem":1138,"tags":1139,"updated":1142,"__hash__":1143},"blog\u002Fblog\u002Fcqrs.md","CQRS на практике: разделение команд и запросов без лишней сложности",{"type":8,"value":9,"toc":1092},"minimark",[10,15,23,48,59,70,74,122,134,138,143,150,160,163,167,170,176,179,183,186,190,244,248,292,295,329,333,336,342,347,371,375,379,382,467,470,474,555,558,562,596,600,603,607,610,640,646,658,662,666,673,730,733,740,743,810,813,817,828,832,836,839,843,850,854,868,872,875,879,882,886,906,909,923,927,991,995,1058,1062,1065,1071,1085,1088],[11,12,14],"h2",{"id":13},"что-такое-cqrs","Что такое CQRS",[16,17,18,22],"p",{},[19,20,21],"strong",{},"CQRS (Command Query Responsibility Segregation)"," — архитектурный паттерн, который разделяет операции над системой на два принципиально разных типа:",[24,25,26,38],"ul",{},[27,28,29,32,33,37],"li",{},[19,30,31],{},"Command"," (команда) — действие, которое ",[34,35,36],"em",{},"изменяет"," состояние системы.",[27,39,40,43,44,47],{},[19,41,42],{},"Query"," (запрос) — операция, которая ",[34,45,46],{},"читает"," данные, не производя изменений.",[16,49,50,51,54,55,58],{},"Идея не нова: ещё в 1988 году Бертран Мейер в книге ",[34,52,53],{},"Object-Oriented Software Construction"," сформулировал принцип ",[19,56,57],{},"CQS"," (Command Query Separation) на уровне отдельных методов.\nCQRS поднимает ту же идею на уровень архитектуры: команды и запросы разделяются не просто по методам, а по классам, модулям, а иногда и по целым сервисам.",[16,60,61,62,65,66,69],{},"Термин популяризировали ",[19,63,64],{},"Грег Янг"," (Greg Young) и ",[19,67,68],{},"Уди Дахан"," (Udi Dahan) в конце 2000-х, когда стало ясно, что в сложных доменах модели чтения и записи живут по разным правилам и пытаются ужиться в одной сущности себе в убыток.",[11,71,73],{"id":72},"главный-вопрос-это-команда-или-запрос","Главный вопрос: это команда или запрос?",[75,76,77,93],"table",{},[78,79,80],"thead",{},[81,82,83,87,90],"tr",{},[84,85,86],"th",{},"Вопрос об операции",[84,88,89],{},"Тип",[84,91,92],{},"Пример",[94,95,96,110],"tbody",{},[81,97,98,102,107],{},[99,100,101],"td",{},"Меняет ли она данные в системе?",[99,103,104],{},[105,106,31],"code",{},[99,108,109],{},"Создание заказа, списание средств",[81,111,112,115,119],{},[99,113,114],{},"Только возвращает данные?",[99,116,117],{},[105,118,42],{},[99,120,121],{},"Получение списка заказов, расчёт итога корзины",[16,123,124,127,128,130,131,133],{},[19,125,126],{},"Критерий простой:"," если после операции данные в хранилище (БД, кэш, очередь) изменились — это ",[105,129,31],{},".\nЕсли данные остались прежними — ",[105,132,42],{},".",[11,135,137],{"id":136},"два-уровня-cqrs","Два уровня CQRS",[139,140,142],"h3",{"id":141},"лёгкий-cqrs-в-рамках-одного-сервиса","Лёгкий CQRS (в рамках одного сервиса)",[16,144,145,146,149],{},"Команды и запросы живут в одном приложении, работают с одной базой данных, но ",[19,147,148],{},"разделены по папкам, классам и семантике",".\nЭто самый распространённый вариант: он даёт 80 % пользы при 20 % усилий.",[151,152,158],"pre",{"className":153,"code":155,"language":156,"meta":157},[154],"language-text","Один сервис\n├── Commands → пишут в БД\n└── Queries → читают из БД\n","text","",[105,159,155],{"__ignoreMap":157},[16,161,162],{},"Именно о нём пойдёт речь ниже.",[139,164,166],{"id":165},"полный-cqrs-раздельные-модели","Полный CQRS (раздельные модели)",[16,168,169],{},"Модель записи и модель чтения физически разделены: разные базы, разные схемы, синхронизация через события.\nПрименяется в высоконагруженных системах, где нагрузка на чтение и запись отличается на порядки.",[151,171,174],{"className":172,"code":173,"language":156,"meta":157},[154],"Command Side (PostgreSQL) ──события──▶ Query Side (Elasticsearch \u002F Redis)\n",[105,175,173],{"__ignoreMap":157},[16,177,178],{},"Это мощный, но дорогой инструмент. Если у вас нет миллионов запросов в секунду, скорее всего, он избыточен.",[11,180,182],{"id":181},"примеры-команды-и-запросы","Примеры: команды и запросы",[16,184,185],{},"Чтобы не привязываться к одному домену, возьмём несколько типичных.",[139,187,189],{"id":188},"команды-изменяют-состояние","Команды (изменяют состояние)",[75,191,192,202],{},[78,193,194],{},[81,195,196,199],{},[84,197,198],{},"Команда",[84,200,201],{},"Что делает",[94,203,204,214,224,234],{},[81,205,206,211],{},[99,207,208],{},[105,209,210],{},"PlaceOrderCommand",[99,212,213],{},"Создаёт заказ и списывает товары со склада",[81,215,216,221],{},[99,217,218],{},[105,219,220],{},"CancelSubscriptionCommand",[99,222,223],{},"Меняет статус подписки на «отменена»",[81,225,226,231],{},[99,227,228],{},[105,229,230],{},"ResetUserPreferencesCommand",[99,232,233],{},"Удаляет пользовательские настройки из кэша и БД",[81,235,236,241],{},[99,237,238],{},[105,239,240],{},"PublishArticleCommand",[99,242,243],{},"Переводит статью из черновика в опубликованную",[139,245,247],{"id":246},"запросы-читают-данные","Запросы (читают данные)",[75,249,250,260],{},[78,251,252],{},[81,253,254,257],{},[84,255,256],{},"Запрос",[84,258,259],{},"Что возвращает",[94,261,262,272,282],{},[81,263,264,269],{},[99,265,266],{},[105,267,268],{},"GetOrderHistoryQuery",[99,270,271],{},"Список заказов пользователя с пагинацией",[81,273,274,279],{},[99,275,276],{},[105,277,278],{},"GetDashboardStatsQuery",[99,280,281],{},"Агрегированные метрики для дашборда",[81,283,284,289],{},[99,285,286],{},[105,287,288],{},"GetArticleByIdQuery",[99,290,291],{},"Текст и метаданные статьи",[16,293,294],{},"Обратите внимание на именование:",[24,296,297,317],{},[27,298,299,300,303,304,307,308,307,311,307,314,133],{},"Команды отражают ",[19,301,302],{},"действие",": ",[105,305,306],{},"Place",", ",[105,309,310],{},"Cancel",[105,312,313],{},"Reset",[105,315,316],{},"Publish",[27,318,319,320,303,323,307,326,133],{},"Запросы начинаются с ",[19,321,322],{},"Get",[105,324,325],{},"GetOrderHistory",[105,327,328],{},"GetDashboardStats",[11,330,332],{"id":331},"структура-кода","Структура кода",[16,334,335],{},"При лёгком CQRS логично организовать папки по типу операции и по доменной области:",[151,337,340],{"className":338,"code":339,"language":156,"meta":157},[154],"Application\u002F\n├── Order\u002F\n│ ├── Command\u002F\n│ │ └── PlaceOrderCommand.php\n│ ├── CommandHandler\u002F\n│ │ └── PlaceOrderHandler.php\n│ ├── Query\u002F\n│ │ └── GetOrderHistoryQuery.php\n│ ├── QueryHandler\u002F\n│ │ └── GetOrderHistoryHandler.php\n│ └── DTO\u002F\n│   └── OrderSummary.php\n│\n├── Analytics\u002F\n│ ├── Query\u002F\n│ │ └── GetDashboardStatsQuery.php\n│ ├── QueryHandler\u002F\n│ │ ├── GetDashboardStatsHandler.php\n│ │ └── CachedGetDashboardStatsHandler.php\n│ └── DTO\u002F\n│   └── DashboardStats.php\n",[105,341,339],{"__ignoreMap":157},[16,343,344],{},[19,345,346],{},"Соглашения:",[24,348,349,359,368],{},[27,350,351,352,355,356,133],{},"Команды лежат в ",[105,353,354],{},"Command\u002F",", их обработчики — в ",[105,357,358],{},"CommandHandler\u002F",[27,360,361,362,355,365,133],{},"Запросы лежат в ",[105,363,364],{},"Query\u002F",[105,366,367],{},"QueryHandler\u002F",[27,369,370],{},"Каждый класс — одна операция. Никаких «универсальных» обработчиков.",[11,372,374],{"id":373},"почему-это-важно","Почему это важно",[139,376,378],{"id":377},"_1-явная-семантика","1. Явная семантика",[16,380,381],{},"По имени класса мгновенно понятно, что происходит:",[151,383,387],{"className":384,"code":385,"language":386,"meta":157,"style":157},"language-php shiki shiki-themes github-light github-dark","\u002F\u002F Очевидно: создаёт заказ, меняет состояние\n$this->handler->handle(new PlaceOrderCommand($cartId, $userId));\n\n\u002F\u002F Очевидно: только читает историю\n$this->handler->handle(new GetOrderHistoryQuery($userId, $page));\n","php",[105,388,389,398,431,438,444],{"__ignoreMap":157},[390,391,394],"span",{"class":392,"line":393},"line",1,[390,395,397],{"class":396},"sJ8bj","\u002F\u002F Очевидно: создаёт заказ, меняет состояние\n",[390,399,401,405,409,413,415,419,422,425,428],{"class":392,"line":400},2,[390,402,404],{"class":403},"sj4cs","$this",[390,406,408],{"class":407},"szBVR","->",[390,410,412],{"class":411},"sVt8B","handler",[390,414,408],{"class":407},[390,416,418],{"class":417},"sScJk","handle",[390,420,421],{"class":411},"(",[390,423,424],{"class":407},"new",[390,426,427],{"class":403}," PlaceOrderCommand",[390,429,430],{"class":411},"($cartId, $userId));\n",[390,432,434],{"class":392,"line":433},3,[390,435,437],{"emptyLinePlaceholder":436},true,"\n",[390,439,441],{"class":392,"line":440},4,[390,442,443],{"class":396},"\u002F\u002F Очевидно: только читает историю\n",[390,445,447,449,451,453,455,457,459,461,464],{"class":392,"line":446},5,[390,448,404],{"class":403},[390,450,408],{"class":407},[390,452,412],{"class":411},[390,454,408],{"class":407},[390,456,418],{"class":417},[390,458,421],{"class":411},[390,460,424],{"class":407},[390,462,463],{"class":403}," GetOrderHistoryQuery",[390,465,466],{"class":411},"($userId, $page));\n",[16,468,469],{},"Читая код ревью, вы сразу видите, где система мутирует данные, а где нет.\nЭто снижает когнитивную нагрузку на порядок.",[139,471,473],{"id":472},"_2-разные-стратегии-обработки","2. Разные стратегии обработки",[75,475,476,487],{},[78,477,478],{},[81,479,480,483,485],{},[84,481,482],{},"Аспект",[84,484,31],{},[84,486,42],{},[94,488,489,500,511,522,533,544],{},[81,490,491,494,497],{},[99,492,493],{},"Транзакции БД",[99,495,496],{},"Обязательны",[99,498,499],{},"Не нужны",[81,501,502,505,508],{},[99,503,504],{},"Кэширование",[99,506,507],{},"Нельзя кэшировать",[99,509,510],{},"Можно и нужно",[81,512,513,516,519],{},[99,514,515],{},"Валидация бизнес-правил",[99,517,518],{},"Полная",[99,520,521],{},"Минимальная",[81,523,524,527,530],{},[99,525,526],{},"Масштабирование",[99,528,529],{},"Master-нода БД",[99,531,532],{},"Read-replicas",[81,534,535,538,541],{},[99,536,537],{},"Идемпотентность",[99,539,540],{},"Нужно проектировать",[99,542,543],{},"Естественна",[81,545,546,549,552],{},[99,547,548],{},"Логирование \u002F аудит",[99,550,551],{},"Критично",[99,553,554],{},"Опционально",[16,556,557],{},"Кэширование команды — ошибка, которая приводит к потере данных. Кэширование запроса — оптимизация.\nРазделение исключает путаницу.",[139,559,561],{"id":560},"_3-разные-модели-данных","3. Разные модели данных",[24,563,564,577,590,593],{},[27,565,566,569,570,307,573,576],{},[19,567,568],{},"Команды"," работают с доменными агрегатами (",[105,571,572],{},"Order",[105,574,575],{},"Subscription",") и соблюдают бизнес-инварианты.",[27,578,579,582,583,307,586,589],{},[19,580,581],{},"Запросы"," возвращают специализированные DTO (",[105,584,585],{},"OrderSummary",[105,587,588],{},"DashboardStats","), оптимизированные под конкретный экран или API-эндпоинт.",[27,591,592],{},"Не нужно тянуть весь граф объектов заказа, чтобы показать пользователю список из десяти строк.",[27,594,595],{},"Query-модель плоская и лёгкая.",[139,597,599],{"id":598},"_4-тестируемость","4. Тестируемость",[16,601,602],{},"Команды тестируются через побочный эффект: «после выполнения в БД появилась запись X».\nЗапросы тестируются через возвращаемое значение: «для входных данных Y возвращается DTO Z».\nЭто разные типы тестов, и разделение позволяет не смешивать их.",[11,604,606],{"id":605},"когда-граница-размывается","Когда граница размывается",[16,608,609],{},"Операция может и читать, и писать. Например, обработчик размещения заказа:",[611,612,613,619,625,634],"ol",{},[27,614,615,618],{},[19,616,617],{},"Читает"," состав корзины из Redis.",[27,620,621,624],{},[19,622,623],{},"Проверяет"," наличие товаров на складе.",[27,626,627,630,631,633],{},[19,628,629],{},"Создаёт"," запись ",[105,632,572],{}," в БД.",[27,635,636,639],{},[19,637,638],{},"Уменьшает"," остатки на складе.",[16,641,642,643,645],{},"Это ",[105,644,31],{},", потому что главное — изменение состояния. Чтение здесь вспомогательное.",[647,648,649],"blockquote",{},[16,650,651,654,655,657],{},[19,652,653],{},"Правило:"," определяйте тип по конечному эффекту.\nЕсли данные изменились — ",[105,656,31],{},".\nЧтение внутри команды не делает её запросом.",[11,659,661],{"id":660},"связь-с-другими-паттернами","Связь с другими паттернами",[139,663,665],{"id":664},"mediator-message-bus","Mediator \u002F Message Bus",[16,667,668,669,672],{},"Команды и запросы часто передаются через ",[19,670,671],{},"шину сообщений"," (Mediator). Вызов выглядит единообразно:",[151,674,676],{"className":384,"code":675,"language":386,"meta":157,"style":157},"$this->messageBus->dispatch(new PlaceOrderCommand(...));\n$this->messageBus->dispatch(new GetOrderHistoryQuery(...));\n",[105,677,678,706],{"__ignoreMap":157},[390,679,680,682,684,687,689,692,694,696,698,700,703],{"class":392,"line":393},[390,681,404],{"class":403},[390,683,408],{"class":407},[390,685,686],{"class":411},"messageBus",[390,688,408],{"class":407},[390,690,691],{"class":417},"dispatch",[390,693,421],{"class":411},[390,695,424],{"class":407},[390,697,427],{"class":403},[390,699,421],{"class":411},[390,701,702],{"class":407},"...",[390,704,705],{"class":411},"));\n",[390,707,708,710,712,714,716,718,720,722,724,726,728],{"class":392,"line":400},[390,709,404],{"class":403},[390,711,408],{"class":407},[390,713,686],{"class":411},[390,715,408],{"class":407},[390,717,691],{"class":417},[390,719,421],{"class":411},[390,721,424],{"class":407},[390,723,463],{"class":403},[390,725,421],{"class":411},[390,727,702],{"class":407},[390,729,705],{"class":411},[16,731,732],{},"Шина не знает, что внутри: она просто маршрутизирует объект в нужный обработчик.\nЭто убирает жёсткую связность между контроллером и бизнес-логикой.",[139,734,736,737],{"id":735},"паттерн-__invoke","Паттерн ",[105,738,739],{},"__invoke",[16,741,742],{},"Обработчики удобно делать «вызываемыми» объектами:",[151,744,746],{"className":384,"code":745,"language":386,"meta":157,"style":157},"final class PlaceOrderHandler\n{\n    public function __invoke(PlaceOrderCommand $command): OrderId\n    {\n        \u002F\u002F логика создания заказа\n    }\n}\n",[105,747,748,759,764,788,793,798,804],{"__ignoreMap":157},[390,749,750,753,756],{"class":392,"line":393},[390,751,752],{"class":407},"final",[390,754,755],{"class":407}," class",[390,757,758],{"class":417}," PlaceOrderHandler\n",[390,760,761],{"class":392,"line":400},[390,762,763],{"class":411},"{\n",[390,765,766,769,772,775,777,779,782,785],{"class":392,"line":433},[390,767,768],{"class":407},"    public",[390,770,771],{"class":407}," function",[390,773,774],{"class":403}," __invoke",[390,776,421],{"class":411},[390,778,210],{"class":403},[390,780,781],{"class":411}," $command)",[390,783,784],{"class":407},":",[390,786,787],{"class":403}," OrderId\n",[390,789,790],{"class":392,"line":440},[390,791,792],{"class":411},"    {\n",[390,794,795],{"class":392,"line":446},[390,796,797],{"class":396},"        \u002F\u002F логика создания заказа\n",[390,799,801],{"class":392,"line":800},6,[390,802,803],{"class":411},"    }\n",[390,805,807],{"class":392,"line":806},7,[390,808,809],{"class":411},"}\n",[16,811,812],{},"Это упрощает DI-контейнеру регистрацию и делает вызов лаконичным.",[139,814,816],{"id":815},"repository-и-read-модель","Repository и Read-модель",[16,818,819,820,823,824,827],{},"В лёгком CQRS команды обычно идут через ",[19,821,822],{},"Repository"," (паттерн для работы с агрегатами), а запросы — через ",[19,825,826],{},"Read-модель"," (прямые SQL-запросы, Query Builder, Elasticsearch).\nНе нужно прогонять аналитический запрос через агрегат и коллекцию объектов.",[11,829,831],{"id":830},"частые-заблуждения","Частые заблуждения",[139,833,835],{"id":834},"cqrs-требует-две-разные-базы-данных","«CQRS требует две разные базы данных»",[16,837,838],{},"Нет. Полный CQRS с раздельными хранилищами — продвинутый вариант для высоконагруженных систем.\nЛёгкий CQRS на уровне кода даёт главные преимущества (семантика, кэширование, тестируемость) без усложнения инфраструктуры.",[139,840,842],{"id":841},"каждый-select-к-бд-это-query","«Каждый SELECT к БД — это Query»",[16,844,845,846,849],{},"Нет. Если метод читает данные как часть команды (например, проверка «есть ли товар на складе?» внутри ",[105,847,848],{},"PlaceOrderHandler",") — это часть Command, а не отдельный Query.\nQuery — это самостоятельная операция чтения, инициированная извне.",[139,851,853],{"id":852},"command-не-может-возвращать-данные","«Command не может возвращать данные»",[16,855,856,857,859,860,863,864,867],{},"Может, но минимум. ",[105,858,848],{}," возвращает ",[105,861,862],{},"OrderId"," — этого достаточно для HTTP-ответа ",[105,865,866],{},"201 Created",".\nНо команда не должна возвращать большие структуры данных: для этого есть Query.",[139,869,871],{"id":870},"cqrs-это-про-микросервисы","«CQRS — это про микросервисы»",[16,873,874],{},"Нет. CQRS отлично работает в монолите.\nБолее того, в монолите он часто приносит больше пользы, потому что без него границы между чтением и записью размываются быстрее.",[139,876,878],{"id":877},"нужен-event-sourcing","«Нужен Event Sourcing»",[16,880,881],{},"CQRS и Event Sourcing часто упоминаются вместе, но это независимые паттерны.\nМожно использовать CQRS без Event Sourcing и наоборот.\nСвязка имеет смысл в сложных доменах с аудитом, но не является обязательной.",[11,883,885],{"id":884},"когда-cqrs-не-нужен","Когда CQRS не нужен",[24,887,888,894,900],{},[27,889,890,893],{},[19,891,892],{},"CRUD-приложение без бизнес-логики."," Если у вас пять таблиц и стандартный админ-панель, разделение на команды и запросы добавит boilerplate без реальной пользы.",[27,895,896,899],{},[19,897,898],{},"Прототип \u002F MVP."," На этапе проверки гипотезы скорость важнее архитектуры.",[27,901,902,905],{},[19,903,904],{},"Команда не знакома с паттерном."," CQRS, внедрённый формально, без понимания, превращается в ритуал и усложняет онбординг.",[16,907,908],{},"CQRS оправдан, когда:",[24,910,911,914,917,920],{},[27,912,913],{},"Операции чтения и записи имеют разную нагрузку.",[27,915,916],{},"Бизнес-правила при записи сложны и требуют явного моделирования.",[27,918,919],{},"Разные клиенты (веб, мобильное приложение, API) нуждаются в разных проекциях данных.",[27,921,922],{},"Команда растёт и нужна чёткая конвенция, чтобы не скатиться в «богатые сервисы».",[11,924,926],{"id":925},"антипаттерны","Антипаттерны",[75,928,929,939],{},[78,930,931],{},[81,932,933,936],{},[84,934,935],{},"Антипаттерн",[84,937,938],{},"Почему плохо",[94,940,941,959,967,975,983],{},[81,942,943,949],{},[99,944,945,948],{},[105,946,947],{},"SaveOrderCommand",", которая и создаёт, и обновляет, и удаляет",[99,950,951,952,307,954,307,957],{},"Нарушает единственную ответственность; разбейте на ",[105,953,306],{},[105,955,956],{},"Update",[105,958,310],{},[81,960,961,964],{},[99,962,963],{},"Query с побочным эффектом (логирует, шлёт уведомления)",[99,965,966],{},"Скрытая мутация; кэширование такого запроса сломает логику",[81,968,969,972],{},[99,970,971],{},"Один «God Handler» на все команды",[99,973,974],{},"Убивает всю пользу от разделения",[81,976,977,980],{},[99,978,979],{},"Возврат полного агрегата из Query",[99,981,982],{},"Тянет лишние данные, связывает read-модель с доменной",[81,984,985,988],{},[99,986,987],{},"Команда без валидации инвариантов",[99,989,990],{},"CQRS не отменяет проверку бизнес-правил",[11,992,994],{"id":993},"чек-лист-для-внедрения","Чек-лист для внедрения",[611,996,997,1012,1028,1034,1040,1046,1052],{},[27,998,999,1002,1003,307,1005,307,1008,1011],{},[19,1000,1001],{},"Определите доменные области"," (",[105,1004,572],{},[105,1006,1007],{},"Analytics",[105,1009,1010],{},"Identity","…).",[27,1013,1014,303,1017,307,1019,307,1021,307,1023,307,1025,133],{},[19,1015,1016],{},"Разнесите папки",[105,1018,354],{},[105,1020,358],{},[105,1022,364],{},[105,1024,367],{},[105,1026,1027],{},"DTO\u002F",[27,1029,1030,1033],{},[19,1031,1032],{},"Договоритесь об именовании",": глагол действия для команд, Get для запросов.",[27,1035,1036,1039],{},[19,1037,1038],{},"Настройте шину \u002F Mediator",", чтобы контроллеры не знали о конкретных обработчиках.",[27,1041,1042,1045],{},[19,1043,1044],{},"Запретите кэширование на уровне команд",", разрешите на уровне запросов.",[27,1047,1048,1051],{},[19,1049,1050],{},"Пишите тесты по типу",": для команд — проверка побочного эффекта, для запросов — проверка возвращаемых данных.",[27,1053,1054,1057],{},[19,1055,1056],{},"Проведите ревью",": убедитесь, что ни один Query не мутирует состояние.",[11,1059,1061],{"id":1060},"итоги","Итоги",[16,1063,1064],{},"CQRS — не обязательный атрибут «серьёзной архитектуры».",[16,1066,642,1067,1070],{},[19,1068,1069],{},"инструмент упорядочивания",", который:",[24,1072,1073,1076,1079,1082],{},[27,1074,1075],{},"делает намерения кода явными,",[27,1077,1078],{},"развязывает руки для кэширования и масштабирования чтения,",[27,1080,1081],{},"упрощает тестирование и код-ревью,",[27,1083,1084],{},"задаёт понятную конвенцию для растущей команды.",[16,1086,1087],{},"Начните с лёгкого CQRS: разделите папки, дайте классам говорящие имена, уберите SELECT-запросы из команд.\nЭто займёт несколько часов \\ дней, а окупаться будет годами.",[1089,1090,1091],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":157,"searchDepth":433,"depth":433,"links":1093},[1094,1095,1096,1100,1104,1105,1111,1112,1118,1125,1126,1127,1128],{"id":13,"depth":400,"text":14},{"id":72,"depth":400,"text":73},{"id":136,"depth":400,"text":137,"children":1097},[1098,1099],{"id":141,"depth":433,"text":142},{"id":165,"depth":433,"text":166},{"id":181,"depth":400,"text":182,"children":1101},[1102,1103],{"id":188,"depth":433,"text":189},{"id":246,"depth":433,"text":247},{"id":331,"depth":400,"text":332},{"id":373,"depth":400,"text":374,"children":1106},[1107,1108,1109,1110],{"id":377,"depth":433,"text":378},{"id":472,"depth":433,"text":473},{"id":560,"depth":433,"text":561},{"id":598,"depth":433,"text":599},{"id":605,"depth":400,"text":606},{"id":660,"depth":400,"text":661,"children":1113},[1114,1115,1117],{"id":664,"depth":433,"text":665},{"id":735,"depth":433,"text":1116},"Паттерн __invoke",{"id":815,"depth":433,"text":816},{"id":830,"depth":400,"text":831,"children":1119},[1120,1121,1122,1123,1124],{"id":834,"depth":433,"text":835},{"id":841,"depth":433,"text":842},{"id":852,"depth":433,"text":853},{"id":870,"depth":433,"text":871},{"id":877,"depth":433,"text":878},{"id":884,"depth":400,"text":885},{"id":925,"depth":400,"text":926},{"id":993,"depth":400,"text":994},{"id":1060,"depth":400,"text":1061},"Backend","\u002Fcovers\u002Fcqrs.webp","2026-08-07","Архитектурный паттерн, который разделяет операции над системой.",false,"md",{},"\u002Fblog\u002Fcqrs",{"title":6,"description":1132},"blog\u002Fcqrs",[1140,1141,386],"architecture","pattern",null,"bFZJtolTlzzCJQMiL6kROb2nQs36aQ-wPSTMdMbDZCo",{"id":1145,"title":1146,"body":1147,"category":1129,"cover":2175,"date":2176,"description":2177,"draft":1133,"extension":1134,"meta":2178,"navigation":436,"path":2179,"seo":2180,"stem":2181,"tags":2182,"updated":1142,"__hash__":2183},"blog\u002Fblog\u002Fclean-architecture.md","Универсальное руководство по Clean Architecture",{"type":8,"value":1148,"toc":2151},[1149,1153,1159,1165,1168,1172,1198,1200,1204,1207,1213,1215,1219,1226,1231,1273,1278,1337,1339,1343,1350,1354,1419,1423,1475,1477,1481,1488,1492,1559,1563,1606,1608,1612,1615,1619,1657,1661,1697,1699,1703,1708,1714,1718,1765,1767,1771,1775,1778,1805,1809,1812,1815,1840,1845,1968,1973,2020,2050,2054,2057,2059,2063,2067,2092,2096,2116,2118,2122,2125],[11,1150,1152],{"id":1151},"введение","Введение",[16,1154,1155,1158],{},[19,1156,1157],{},"Clean Architecture"," (Чистая Архитектура) — это архитектурный подход, предложенный Робертом Мартином (Uncle Bob), который объединяет идеи Hexagonal Architecture (Порты и Адаптеры), Onion Architecture и Screaming Architecture.",[16,1160,1161,1164],{},[19,1162,1163],{},"Главная цель:"," создание системы, которая независима от фреймворков, баз данных, UI и внешних сервисов. Бизнес-логика изолирована и является ядром системы.",[1166,1167],"hr",{},[11,1169,1171],{"id":1170},"фундаментальные-принципы","📐 Фундаментальные принципы",[611,1173,1174,1180,1186,1192],{},[27,1175,1176,1179],{},[19,1177,1178],{},"Правило зависимостей (The Dependency Rule):"," Исходный код зависимостей может указывать только внутрь (к центру). Внутренние слои ничего не знают о внешних.",[27,1181,1182,1185],{},[19,1183,1184],{},"Независимость от фреймворков:"," Фреймворки — это просто инструменты. Они не должны диктовать архитектуру.",[27,1187,1188,1191],{},[19,1189,1190],{},"Тестируемость:"," Бизнес-логика может быть протестирована в изоляции, без базы данных, UI или внешних API.",[27,1193,1194,1197],{},[19,1195,1196],{},"Независимость от UI и БД:"," Интерфейс пользователя и способ хранения данных — это внешние детали, которые можно заменить без изменения ядра.",[1166,1199],{},[11,1201,1203],{"id":1202},"слои-архитектуры-и-структура-директорий","🧅 Слои Архитектуры и Структура Директорий",[16,1205,1206],{},"Архитектура состоит из четырех основных слоев. Рассмотрим их от центра к периферии.",[151,1208,1211],{"className":1209,"code":1210,"language":156,"meta":157},[154],"src\u002F\n │\n ├── Domain\u002F                              # 🧠 ЯДРО (Чистый PHP, ЗАПРЕЩЕНЫ зависимости от фреймворков)\n │   ├── DTO\u002F                             # Доменные DTO (передача данных между слоями)\n │   │   └── UserDTO.php                  # Пример: UserDTO (readonly)\n │   │\n │   ├── Entity\u002F                          # Бизнес-сущности с поведением (Rich Domain Model)\n │   │   ├── User.php                     # Пример: User с методами changeStatus(), block()\n │   │   └── Order.php                    # Не содержат аннотаций ORM!\n │   │\n │   ├── ValueObject\u002F                     # Неизменяемые объекты-значения (Readonly-классы)\n │   │   ├── Email.php                    # Пример: Email, Money, Address\n │   │   └── OrderStatus.php              # Описывают данные, сравниваются по значению\n │   │\n │   ├── Repository\u002F                      # ТОЛЬКО интерфейсы (Contracts) для доступа к данным\n │   │   ├── UserRepositoryInterface.php  # Пример: UserRepositoryInterface\n │   │   └── OrderRepositoryInterface.php # Напр.: save(), findById(), findAll()\n │   │\n │   ├── Service\u002F                         # Интерфейсы для внешних сервисов (если нужно в Domain)\n │   │   └── EmailSenderInterface.php     # Пример: EmailSenderInterface, PaymentGatewayInterface\n │   │\n │   ├── Event\u002F                           # Доменные события (Domain Events)\n │   │   └── UserCreatedEvent.php         # Пример: UserCreatedEvent, OrderPlacedEvent\n │   │\n │   └── Exception\u002F                       # Исключения бизнес-правил\n │       └── UserAlreadyExistsException.php # Пример: OutdatedDataException, InvalidOrderException\n │\n ├── Application\u002F                         # ⚙️ СЦЕНАРИИ ИСПОЛЬЗОВАНИЯ (Оркестрация, зависит от Domain)\n │   ├── Port\u002F                            # Общие интерфейсы Application-слоя (DeeplinkGeneratorInterface)\n │   ├── Common\u002F                          # Общие утилиты (Pagination, DateTime)\n │   │\n │   ├── Voting\u002F                          # Модуль по доменной области (bounded context)\n │   │   ├── Command\u002F                     # DTO команд (readonly)\n │   │   ├── CommandHandler\u002F              # Синхронные обработчики команд (__invoke)\n │   │   ├── Message\u002F                     # Асинхронные сообщения для Messenger\n │   │   ├── MessageHandler\u002F              # Consumer'ы очередей (#[AsMessageHandler])\n │   │   └── DTO\u002F                         # Выходные DTO модуля\n │   │\n │   ├── Season\u002F                          # Аналогичная структура для других модулей\n │   │   ├── Command\u002F\n │   │   ├── CommandHandler\u002F\n │   │   ├── Query\u002F\n │   │   ├── QueryHandler\u002F\n │   │   └── DTO\u002F\n │   │\n │   └── Bot\u002F\n │       ├── Command\u002F                     # (если есть CQRS-команды)\n │       ├── CommandHandler\u002F\n │       ├── Message\u002F\n │       ├── MessageHandler\u002F\n │       └── DTO\u002F\n │\n │   # ⚠️ Не используйте папки UseCase\u002F, Service\u002F для CQRS-обработчиков.\n │   # CommandHandler\u002F + QueryHandler\u002F — стандарт проекта.\n │\n ├── Infrastructure\u002F                      # 🔌 ДЕТАЛИ РЕАЛИЗАЦИИ (Здесь живет фреймворк, БД, API)\n │   ├── Persistence\u002F                     # Работа с базой данных\n │   │   ├── Model\u002F                       # ORM-сущности (Doctrine Models, Eloquent Models)\n │   │   │   ├── UserModel.php            # Пример: UserModel (содержит #[ORM\\Entity], #[ORM\\Column])\n │   │   │   └── OrderModel.php           # Не содержат бизнес-логики!\n │   │   │\n │   │   ├── Mapper\u002F                      # Мапперы между Domain Entity и Infrastructure Model\n │   │   │   ├── UserMapper.php           # Пример: UserMapper (toDomain(), toModel())\n │   │   │   └── OrderMapper.php          # Конвертирует Domain Entity ↔ ORM Model\n │   │   │\n │   │   └── Repository\u002F                  # Реализации интерфейсов из Domain\\Repository\n │   │       ├── DoctrineUserRepository.php  # Пример: DoctrineUserRepository (реализует UserRepositoryInterface)\n │   │       └── EloquentOrderRepository.php # Использует EntityManager, QueryBuilder\n │   │\n │   ├── Integration\u002F                     # Интеграция с внешними API и сервисами\n │   │   ├── Api\u002F                         # HTTP-клиенты для внешних API\n │   │   │   ├── SapClient.php            # Пример: SapClient, StripeClient, SlackClient\n │   │   │   └── StripeClient.php         # Использует Guzzle, Symfony HttpClient\n │   │   │\n │   │   └── Gateway\u002F                     # Реализации интерфейсов из Domain\\Service\n │   │       ├── SmtpEmailSender.php      # Пример: SmtpEmailSender (реализует EmailSenderInterface)\n │   │       └── StripePaymentGateway.php # Использует внешние SDK\n │   │\n │   ├── Messaging\u002F                       # Интеграция с очередями сообщений\n │   │   ├── Kafka\u002F                       # Apache Kafka \u002F Redpanda\n │   │   │   ├── Dto\u002F                     # DTO для парсинга JSON из Kafka\n │   │   │   │   └── EmployeeBatchDto.php # Пример: EmployeeBatchDto, OrderEventDto\n │   │   │   ├── Message\u002F                 # Обёртки для сырых сообщений\n │   │   │   │   └── KafkaRawMessage.php  # Пример: KafkaRawMessage\n │   │   │   ├── Serializer\u002F              # Кастомные сериализаторы\n │   │   │   │   └── KafkaRawMessageSerializer.php # Для Symfony Messenger\n │   │   │   └── Handler\u002F                 # Обработчики сообщений из Kafka\n │   │   │       └── EmployeeMessageHandler.php  # Парсят JSON, создают команды, диспатчат в MessageBus\n │   │   │\n │   │   └── RabbitMQ\u002F                    # RabbitMQ (если используется)\n │   │       └── Handler\u002F\n │   │           └── OrderEventHandler.php\n │   │\n │   ├── AutoMapper\u002F                      # Конфигурация автоматического маппинга объектов\n │   │   └── AutoMapperConfig.php         # Пример: AutoMapperConfig (для AutoMapperBundle)\n │   │\n │   ├── Serializer\u002F                      # Настройки сериализации\u002Fдесериализации\n │   │   └── SerializerConfig.php         # Пример: SerializerConfig (для Symfony Serializer)\n │   │\n │   ├── Validation\u002F                      # Валидаторы и правила валидации\n │   │   ├── Validator.php                # Пример: Validator (обёртка над Symfony Validator)\n │   │   └── Rules\u002F                       # Кастомные правила валидации\n │   │       └── UniqueEmailRule.php\n │   │\n │   ├── DI\u002F                              # Конфигурация контейнера внедрения зависимостей\n │   │   └── services.php                 # Пример: services.php (для Symfony DI, Slim)\n │   │\n │   ├── Console\u002F                         # Системные консольные команды (зависят от фреймворка)\n │   │   └── Command\u002F                     # Кастомные команды (если нужны)\n │   │       └── SyncDataCommand.php      # Пример: SyncDataCommand, ClearCacheCommand\n │   │\n │   └── EventListener\u002F                   # Обработчики событий фреймворка\n │       └── KernelRequestListener.php    # Пример: KernelRequestListener, ExceptionListener\n │\n └── Presentation\u002F                        # 🖥️ ТОЧКИ ВХОДА (HTTP, CLI. Зависит от Application)\n     ├── Http\u002F                            # Взаимодействие по HTTP\n     │   ├── Controller\u002F                  # HTTP контроллеры (REST, GraphQL)\n     │   │   ├── UserController.php       # Пример: UserController, OrderController\n     │   │   └── OrderController.php      # Принимают запросы, вызывают CommandHandlers\n     │   │\n     │   ├── Request\u002F                     # HTTP Request DTO (если нужны)\n     │   │   └── CreateUserRequest.php    # Пример: CreateUserRequest, UpdateOrderRequest\n     │   │\n     │   ├── Response\u002F                    # HTTP Response DTO\n     │   │   └── UserResponse.php         # Пример: UserResponse, OrderResponse\n     │   │\n     │   └── Middleware\u002F                  # Middleware (авторизация, логирование, CORS)\n     │       └── AuthMiddleware.php       # Пример: AuthMiddleware, CorsMiddleware\n     │\n     └── Console\u002F                         # Пользовательские CLI-команды (бизнес-ориентированные)\n         └── Command\u002F                     # Пример: ProcessOrdersCommand, GenerateReportCommand\n             └── ProcessOrdersCommand.php # Вызывают Application CommandHandlers\n",[105,1212,1210],{"__ignoreMap":157},[1166,1214],{},[139,1216,1218],{"id":1217},"_1-domain-layer-ядро-сущности","🧠 1. Domain Layer (Ядро \u002F Сущности)",[16,1220,1221,1222,1225],{},"Самый внутренний слой. Содержит ",[19,1223,1224],{},"корпоративные бизнес-правила"," (Enterprise Business Rules). Это сердце приложения.",[16,1227,1228],{},[19,1229,1230],{},"Состав:",[24,1232,1233,1239,1245,1251,1261,1267],{},[27,1234,1235,1238],{},[105,1236,1237],{},"Entity\u002F"," — Бизнес-сущности с поведением (Rich Domain Model). Содержат состояние и методы его изменения.",[27,1240,1241,1244],{},[105,1242,1243],{},"ValueObject\u002F"," — Объекты-значения (Value Objects). Не имеют идентификатора, неизменяемы (immutable), сравниваются по значению.",[27,1246,1247,1250],{},[105,1248,1249],{},"Repository\u002F"," — ТОЛЬКО интерфейсы (контракты) для доступа к данным.",[27,1252,1253,1256,1257,1260],{},[105,1254,1255],{},"Service\u002F"," или ",[105,1258,1259],{},"Gateway\u002F"," — Интерфейсы для взаимодействия с внешними системами (если их нельзя отнести к Application).",[27,1262,1263,1266],{},[105,1264,1265],{},"Event\u002F"," — Доменные события (Domain Events).",[27,1268,1269,1272],{},[105,1270,1271],{},"Exception\u002F"," — Исключения, отражающие нарушение бизнес-правил.",[16,1274,1275],{},[19,1276,1277],{},"📋 Правила:",[75,1279,1280,1291],{},[78,1281,1282],{},[81,1283,1284,1288],{},[84,1285,1287],{"align":1286},"left","✅ РАЗРЕШЕНО",[84,1289,1290],{"align":1286},"❌ ЗАПРЕЩЕНО",[94,1292,1293,1301,1309,1321,1329],{},[81,1294,1295,1298],{},[99,1296,1297],{"align":1286},"Чистый PHP (без зависимостей от фреймворков)",[99,1299,1300],{"align":1286},"Использовать аннотации ORM (Doctrine, Eloquent)",[81,1302,1303,1306],{},[99,1304,1305],{"align":1286},"Создание интерфейсов (контрактов)",[99,1307,1308],{"align":1286},"Зависеть от HTTP, CLI, Базы Данных",[81,1310,1311,1318],{},[99,1312,1313,1314,1317],{"align":1286},"Использование независимых библиотек (напр., ",[105,1315,1316],{},"ramsey\u002Fuuid",")",[99,1319,1320],{"align":1286},"Знать о существовании фреймворка (Symfony, Laravel)",[81,1322,1323,1326],{},[99,1324,1325],{"align":1286},"Реализация бизнес-логики и инвариантов",[99,1327,1328],{"align":1286},"Выполнять SQL-запросы или HTTP-вызовы",[81,1330,1331,1334],{},[99,1332,1333],{"align":1286},"Фабричные методы для создания сущностей",[99,1335,1336],{"align":1286},"Бизнес-логика в конструкторах",[1166,1338],{},[139,1340,1342],{"id":1341},"️-2-application-layer-сценарии-использования","⚙️ 2. Application Layer (Сценарии использования)",[16,1344,1345,1346,1349],{},"Содержит ",[19,1347,1348],{},"бизнес-логику уровня приложения"," (Application Business Rules). Отвечает за как использовать Domain для выполнения конкретных сценариев (Use Cases).",[16,1351,1352],{},[19,1353,1230],{},[24,1355,1356,1372,1380,1387,1396,1409,1414],{},[27,1357,1358,1359,307,1362,307,1365,307,1368,1371],{},"Модули по доменным областям (",[105,1360,1361],{},"Voting\u002F",[105,1363,1364],{},"Season\u002F",[105,1366,1367],{},"Statistics\u002F",[105,1369,1370],{},"Bot\u002F",") — каждый содержит свои Command, Query, Handler, DTO",[27,1373,1374,1376,1377,1379],{},[105,1375,354],{}," + ",[105,1378,358],{}," — синхронные use case, изменяющие состояние",[27,1381,1382,1376,1384,1386],{},[105,1383,364],{},[105,1385,367],{}," — операции чтения",[27,1388,1389,1376,1392,1395],{},[105,1390,1391],{},"Message\u002F",[105,1393,1394],{},"MessageHandler\u002F"," — асинхронная обработка через Symfony Messenger (не путать с CQRS Handler!)",[27,1397,1398,1401,1402,1405,1406,1317],{},[105,1399,1400],{},"Port\u002F"," — общие интерфейсы Application-слоя (имена ",[105,1403,1404],{},"*Interface",", не ",[105,1407,1408],{},"*Port",[27,1410,1411,1413],{},[105,1412,1027],{}," — выходные объекты модулей",[27,1415,1416,1418],{},[105,1417,1271],{}," — исключения уровня приложения (напр., EmployeeNotFoundException)",[16,1420,1421],{},[19,1422,1277],{},[75,1424,1425,1433],{},[78,1426,1427],{},[81,1428,1429,1431],{},[84,1430,1287],{"align":1286},[84,1432,1290],{"align":1286},[94,1434,1435,1443,1451,1459,1467],{},[81,1436,1437,1440],{},[99,1438,1439],{"align":1286},"Координировать работу Domain и Infrastructure",[99,1441,1442],{"align":1286},"Прямая работа с HTTP-запросами\u002Fответами",[81,1444,1445,1448],{},[99,1446,1447],{"align":1286},"Вызывать репозитории и сервисы через интерфейсы",[99,1449,1450],{"align":1286},"Прямые запросы к БД (SQL, DQL)",[81,1452,1453,1456],{},[99,1454,1455],{"align":1286},"Обрабатывать исключения и логировать контекст",[99,1457,1458],{"align":1286},"Содержать бизнес-логику вне UseCase\u002FHandler",[81,1460,1461,1464],{},[99,1462,1463],{"align":1286},"Использовать паттерны (Saga, CQRS, Mediator)",[99,1465,1466],{"align":1286},"Знать о деталях реализации (Doctrine, Kafka)",[81,1468,1469,1472],{},[99,1470,1471],{"align":1286},"Диспатчить команды в Message Bus",[99,1473,1474],{"align":1286},"Работать напрямую с очередями\u002Fброкерами",[1166,1476],{},[139,1478,1480],{"id":1479},"_3-infrastructure-layer-детали-реализации","🔌 3. Infrastructure Layer (Детали реализации)",[16,1482,1483,1484,1487],{},"Самый внешний слой. Содержит ",[19,1485,1486],{},"всё, что касается технических деталей:"," базы данных, внешние API, очереди, фреймворки. Реализует интерфейсы, заданные в Domain и Application.",[16,1489,1490],{},[19,1491,1230],{},[24,1493,1494,1500,1506,1512,1517,1526,1535,1541,1547,1553],{},[27,1495,1496,1499],{},[105,1497,1498],{},"Persistence\u002F"," — Работа с БД.",[27,1501,1502,1505],{},[105,1503,1504],{},"Model\u002F"," — ORM-сущности (Doctrine Models, Eloquent Models).",[27,1507,1508,1511],{},[105,1509,1510],{},"Mapper\u002F"," — Мапперы между Domain Entity и ORM Model.",[27,1513,1514,1516],{},[105,1515,1249],{}," — Реализации интерфейсов из Domain\\Repository.",[27,1518,1519,1256,1522,1525],{},[105,1520,1521],{},"Integration\u002F",[105,1523,1524],{},"Api\u002F"," — HTTP-клиенты для внешних API.",[27,1527,1528,1256,1531,1534],{},[105,1529,1530],{},"Messaging\u002F",[105,1532,1533],{},"Kafka\u002F"," — Интеграция с очередями (Kafka, RabbitMQ).",[27,1536,1537,1540],{},[105,1538,1539],{},"DI\u002F"," — Конфигурация контейнера внедрения зависимостей.",[27,1542,1543,1546],{},[105,1544,1545],{},"Serializer\u002F"," — Настройки сериализации\u002Fдесериализации.",[27,1548,1549,1552],{},[105,1550,1551],{},"Validation\u002F"," — Валидаторы и правила валидации.",[27,1554,1555,1558],{},[105,1556,1557],{},"Console\u002F"," — Системные консольные команды, зависящие от фреймворка.",[16,1560,1561],{},[19,1562,1277],{},[75,1564,1565,1573],{},[78,1566,1567],{},[81,1568,1569,1571],{},[84,1570,1287],{"align":1286},[84,1572,1290],{"align":1286},[94,1574,1575,1583,1591,1599],{},[81,1576,1577,1580],{},[99,1578,1579],{"align":1286},"Использовать Doctrine, Eloquent, PDO, Redis",[99,1581,1582],{"align":1286},"Содержать бизнес-логику (только техническая)",[81,1584,1585,1588],{},[99,1586,1587],{"align":1286},"Реализовывать интерфейсы из Domain\u002FApplication",[99,1589,1590],{"align":1286},"Вызываться напрямую из Presentation слоя",[81,1592,1593,1596],{},[99,1594,1595],{"align":1286},"Работать с HTTP-клиентами, Kafka, SMTP",[99,1597,1598],{"align":1286},"Зависеть от Application или Presentation слоев",[81,1600,1601,1604],{},[99,1602,1603],{"align":1286},"Конфигурировать фреймворк и DI-контейнер",[99,1605],{"align":1286},[1166,1607],{},[139,1609,1611],{"id":1610},"️-4-presentation-layer-точки-входа","🖥️ 4. Presentation Layer (Точки входа)",[16,1613,1614],{},"Слой драйверов. Отвечает за доставку запросов от пользователя\u002Fвнешней системы в Application и возврат ответа. Должен быть максимально \"тонким\".",[16,1616,1617],{},[19,1618,1230],{},[24,1620,1621,1627,1633,1639,1645,1651],{},[27,1622,1623,1626],{},[105,1624,1625],{},"Http\u002FController\u002F"," — HTTP контроллеры (REST, GraphQL).",[27,1628,1629,1632],{},[105,1630,1631],{},"Http\u002FFactory\u002F"," — маппинг Request DTO → Command\u002FQuery (для сложных случаев).",[27,1634,1635,1638],{},[105,1636,1637],{},"Http\u002FDTO\u002FRequest\u002F"," — HTTP Request DTO с Symfony Validator constraints.",[27,1640,1641,1644],{},[105,1642,1643],{},"Console\u002FCommand\u002F"," — CLI-команды (бизнес-ориентированные).",[27,1646,1647,1650],{},[105,1648,1649],{},"Listener\u002F"," — Обработчики событий фреймворка.",[27,1652,1653,1656],{},[105,1654,1655],{},"Middleware\u002F"," — Проверки авторизации, валидация запросов.",[16,1658,1659],{},[19,1660,1277],{},[75,1662,1663,1671],{},[78,1664,1665],{},[81,1666,1667,1669],{},[84,1668,1287],{"align":1286},[84,1670,1290],{"align":1286},[94,1672,1673,1681,1689],{},[81,1674,1675,1678],{},[99,1676,1677],{"align":1286},"Парсить HTTP-запросы, валидировать входные данные",[99,1679,1680],{"align":1286},"Содержать бизнес-логику или оркестрацию",[81,1682,1683,1686],{},[99,1684,1685],{"align":1286},"Создавать DTO\u002FCommand и передавать в Application",[99,1687,1688],{"align":1286},"Напрямую обращаться к Repository или БД",[81,1690,1691,1694],{},[99,1692,1693],{"align":1286},"Форматировать HTTP-ответы (JSON, XML, HTML)",[99,1695,1696],{"align":1286},"Знать о деталях Infrastructure",[1166,1698],{},[139,1700,1702],{"id":1701},"поток-зависимостей-и-данных","🔄 Поток зависимостей и данных",[1704,1705,1707],"h4",{"id":1706},"граф-зависимостей-кто-от-кого-зависит","Граф зависимостей (Кто от кого зависит)",[151,1709,1712],{"className":1710,"code":1711,"language":156,"meta":157},[154]," ┌─────────────────────────────────────────────────┐\n │            Presentation Layer                   │\n │   (Controllers, CLI Commands, API Endpoints)    │\n └──────────────────────┬──────────────────────────┘\n                        │ вызывает\n                        ▼\n ┌─────────────────────────────────────────────────┐\n │            Application Layer                    │\n │   (UseCases, CommandHandlers, DTOs)             │\n └──────────────────────┬──────────────────────────┘\n                        │ использует интерфейсы\n                        ▼\n ┌─────────────────────────────────────────────────┐\n │              Domain Layer                       │\n │   (Entities, ValueObjects, Repository Contracts)│\n └───────────────────────▲─────────────────────────┘\n                         │ реализует\n ┌───────────────────────┴─────────────────────────┐\n │           Infrastructure Layer                  │\n │   (ORM Models, Mappers, Repo Implementations,   │\n │    HTTP Clients, Message Brokers, DI Config)    │\n └─────────────────────────────────────────────────┘\n",[105,1713,1711],{"__ignoreMap":157},[1704,1715,1717],{"id":1716},"поток-данных-на-примере-сохранения-сущности","Поток данных (На примере сохранения сущности)",[611,1719,1720,1733,1742,1748],{},[27,1721,1722,1725,1726,1729,1730,133],{},[19,1723,1724],{},"Presentation:"," Принимает JSON, валидирует, создает ",[105,1727,1728],{},"CreateUserCommand",", вызывает ",[105,1731,1732],{},"CreateUserHandler",[27,1734,1735,1738,1739,133],{},[19,1736,1737],{},"Application:"," CreateUserHandler получает команду. Вызывает ",[105,1740,1741],{},"UserRepositoryInterface->save($user)",[27,1743,1744,1747],{},[19,1745,1746],{},"Domain:"," Ничего не знает о том, как данные уйдут в БД. Просто отдает сущность.",[27,1749,1750,1753,1754,1757,1758,1761,1762,133],{},[19,1751,1752],{},"Infrastructure:"," ",[105,1755,1756],{},"UserRepository"," (реализация интерфейса) принимает Domain-сущность, через UserMapper конвертирует её в ",[105,1759,1760],{},"UserModel"," (ORM), сохраняет через ",[105,1763,1764],{},"EntityManager",[1166,1766],{},[11,1768,1770],{"id":1769},"ключевые-паттерны-и-концепции","💡 Ключевые паттерны и концепции",[139,1772,1774],{"id":1773},"_1-разделение-entity-и-model-orm","1. Разделение Entity и Model (ORM)",[16,1776,1777],{},"Это частая ошибка — использовать ORM-модели как бизнес-сущности.",[24,1779,1780,1793,1799],{},[27,1781,1782,1785,1786,1789,1790,133],{},[19,1783,1784],{},"Domain Entity:"," Богатая модель, содержит бизнес-инварианты (например, ",[105,1787,1788],{},"User->block()"," проверяет правила блокировки). Не имеет аннотаций ",[105,1791,1792],{},"#[ORM\\Column]",[27,1794,1795,1798],{},[19,1796,1797],{},"Infrastructure Model:"," \"Тупая\" модель (Anemic), нужна только для маппинга на таблицу в БД. Содержит только геттеры\u002Fсеттеры и ORM-аннотации.",[27,1800,1801,1804],{},[19,1802,1803],{},"Mapper:"," Класс, который переводит Domain Entity ↔ Infrastructure Model.",[139,1806,1808],{"id":1807},"_2-порты-и-адаптеры-в-php-ports-and-adapters","2. Порты и Адаптеры в PHP (Ports and Adapters)",[16,1810,1811],{},"Архитектурный паттерн Ports and Adapters (Hexagonal Architecture) универсален, но PHP-сообщество исторически не использует слово Port в нейминге классов и директорий. Вместо этого применяются более описательные названия, отражающие суть контракта.",[16,1813,1814],{},"В PHP:",[24,1816,1817,1831],{},[27,1818,1819,1822,1823,1826,1827,1830],{},[19,1820,1821],{},"Порт (Port)"," — это ",[19,1824,1825],{},"Интерфейс"," (обычно с суффиксом ",[105,1828,1829],{},"Interface","), описывающий контракт.",[27,1832,1833,1822,1836,1839],{},[19,1834,1835],{},"Адаптер (Adapter)",[19,1837,1838],{},"конкретный класс",", реализующий интерфейс и использующий технологии (Doctrine, Guzzle, Redis).",[16,1841,1842],{},[19,1843,1844],{},"Таблица соответствий терминов",[75,1846,1847,1860],{},[78,1848,1849],{},[81,1850,1851,1854,1857],{},[84,1852,1853],{"align":1286},"Академический термин",[84,1855,1856],{"align":1286},"🐘 Идиоматичный PHP-нейминг",[84,1858,1859],{"align":1286},"Где размещается",[94,1861,1862,1879,1899,1916,1933,1951],{},[81,1863,1864,1869,1874],{},[99,1865,1866],{"align":1286},[105,1867,1868],{},"UserRepositoryPort",[99,1870,1871],{"align":1286},[105,1872,1873],{},"UserRepositoryInterface",[99,1875,1876],{"align":1286},[105,1877,1878],{},"Domain\u002FRepository\u002F",[81,1880,1881,1886,1891],{},[99,1882,1883],{"align":1286},[105,1884,1885],{},"PaymentGatewayPort",[99,1887,1888],{"align":1286},[105,1889,1890],{},"PaymentGatewayInterface",[99,1892,1893,1256,1896],{"align":1286},[105,1894,1895],{},"Domain\u002FGateway\u002F",[105,1897,1898],{},"Application\u002FService\u002F",[81,1900,1901,1906,1911],{},[99,1902,1903],{"align":1286},[105,1904,1905],{},"EmailSenderPort",[99,1907,1908],{"align":1286},[105,1909,1910],{},"EmailSenderInterface",[99,1912,1913],{"align":1286},[105,1914,1915],{},"Domain\u002FService\u002F",[81,1917,1918,1923,1928],{},[99,1919,1920],{"align":1286},[105,1921,1922],{},"ExternalApiClientPort",[99,1924,1925],{"align":1286},[105,1926,1927],{},"SapClientInterface",[99,1929,1930],{"align":1286},[105,1931,1932],{},"Infrastructure\u002FIntegration\u002F",[81,1934,1935,1941,1946],{},[99,1936,1937,1940],{"align":1286},[105,1938,1939],{},"UserManagementPort"," (вход)",[99,1942,1943],{"align":1286},[105,1944,1945],{},"CreateUserCommandHandler",[99,1947,1948],{"align":1286},[105,1949,1950],{},"Application\u002FCommandHandler\u002F",[81,1952,1953,1958,1963],{},[99,1954,1955,1940],{"align":1286},[105,1956,1957],{},"HttpApiPort",[99,1959,1960],{"align":1286},[105,1961,1962],{},"UserController",[99,1964,1965],{"align":1286},[105,1966,1967],{},"Presentation\u002FHttp\u002F",[16,1969,1970],{},[19,1971,1972],{},"Почему в PHP не используют слово \"Port\"?",[611,1974,1975,1988,2006],{},[27,1976,1977,1980,1981,1983,1984,1987],{},[19,1978,1979],{},"Слишком абстрактно."," PHP-разработчики предпочитают, чтобы имя класса сразу отвечало на вопрос \"что это делает?\". ",[105,1982,1927],{}," говорит: \"это интерфейс для общения с SAP\". ",[105,1985,1986],{},"SapPort"," заставляет думать.",[27,1989,1990,1993,1994,307,1996,307,1999,307,2002,2005],{},[19,1991,1992],{},"Влияние фреймворков."," Symfony и Laravel популяризировали термины ",[105,1995,822],{},[105,1997,1998],{},"Gateway",[105,2000,2001],{},"Client",[105,2003,2004],{},"Handler",". Они прочно вошли в стандарт индустрии.",[27,2007,2008,2011,2012,2015,2016,2019],{},[19,2009,2010],{},"Screaming Architecture."," Архитектура должна \"кричать\" о предметной области, а не о паттернах. Директория ",[105,2013,2014],{},"Infrastructure\u002FPersistence\u002F"," кричит: \"тут работа с базой!\", а ",[105,2017,2018],{},"Infrastructure\u002FAdapter\u002F"," — \"тут какой-то абстрактный адаптер\".",[16,2021,2022,2025,2026,2028,2029,2032,2033,307,2035,307,2037,307,2040,307,2043,307,2046,307,2048,133],{},[19,2023,2024],{},"⚠️ Важно:"," Избегайте создания директорий ",[105,2027,1400],{}," и ",[105,2030,2031],{},"Adapter\u002F",". Используйте семантические имена: ",[105,2034,1249],{},[105,2036,1259],{},[105,2038,2039],{},"Client\u002F",[105,2041,2042],{},"Sender\u002F",[105,2044,2045],{},"Publisher\u002F",[105,2047,1521],{},[105,2049,1498],{},[139,2051,2053],{"id":2052},"_3-иммутабельность-и-dto","3. Иммутабельность и DTO",[16,2055,2056],{},"Все объекты, передающие данные между слоями (Commands, DTOs), должны быть неизменяемыми (readonly). Это гарантирует, что данные не будут изменены по пути от Presentation к Application, и упрощает потоковую безопасность.",[1166,2058],{},[11,2060,2062],{"id":2061},"️-плюсы-и-минусы-подхода","⚖️ Плюсы и Минусы подхода",[139,2064,2066],{"id":2065},"преимущества","✅ Преимущества",[24,2068,2069,2074,2080,2086],{},[27,2070,2071,2073],{},[19,2072,1190],{}," Ядро (Domain) покрывается юнит-тестами на 100% без моков БД и фреймворка.",[27,2075,2076,2079],{},[19,2077,2078],{},"Поддерживаемость:"," Четкое разделение ответственности. Разработчик точно знает, где писать бизнес-логику, а где код для работы с БД.",[27,2081,2082,2085],{},[19,2083,2084],{},"Гибкость:"," Легко заменить БД (MySQL → PostgreSQL), фреймворк (Symfony → Laravel) или способ доставки (REST → GraphQL), не трогая ядро.",[27,2087,2088,2091],{},[19,2089,2090],{},"Независимость команд:"," Можно параллельно разрабатывать ядро и инфраструктуру.",[139,2093,2095],{"id":2094},"недостатки-и-компромиссы","❌ Недостатки и Компромиссы",[24,2097,2098,2104,2110],{},[27,2099,2100,2103],{},[19,2101,2102],{},"Boilerplate (много шаблонного кода):"," Создание Entity, Model, Mapper, Repository Interface, Repository Implementation для одной сущности требует времени.",[27,2105,2106,2109],{},[19,2107,2108],{},"Порог входа:"," Команде нужно строго соблюдать правила. Новички часто пытаются \"срезать путь\" и нарушают границы слоев.",[27,2111,2112,2115],{},[19,2113,2114],{},"Overkill для простых проектов:"," Для простого CRUD-приложения Clean Architecture будет избыточной (лучше использовать Active Record или простую Service-ориентированную архитектуру).",[1166,2117],{},[11,2119,2121],{"id":2120},"️-чек-лист-для-code-review","🛡️ Чек-лист для Code Review",[16,2123,2124],{},"При ревью кода в проекте с Clean Architecture задавайте следующие вопросы:",[611,2126,2127,2130,2133,2136,2139,2142,2145,2148],{},[27,2128,2129],{},"Импортирует ли Domain слой что-либо из Symfony\u002FLaravel\u002FDoctrine? (Если да — ошибка)",[27,2131,2132],{},"Есть ли бизнес-логика (условия if\u002Felse, расчеты) в Infrastructure или Presentation слоях? (Если да — перенести в Domain\u002FApplication)",[27,2134,2135],{},"Обращается ли Presentation слой напрямую к Repository или БД? (Если да — перенести в Application UseCase)",[27,2137,2138],{},"Содержит ли Domain Entity аннотации ORM или зависимости от внешних сервисов? (Если да — вынести в Infrastructure)",[27,2140,2141],{},"Являются ли DTO и Command классы readonly?",[27,2143,2144],{},"Используются ли семантические имена для интерфейсов (Repository, Client, Sender), а не абстрактные Port\u002FAdapter?",[27,2146,2147],{},"Реализует ли Infrastructure слой интерфейсы из Domain\u002FApplication через DI-контейнер?",[27,2149,2150],{},"Находится ли бизнес-логика внутри Entity\u002FValueObject\u002FCommandHandler, а не в конструкторах и контроллерах?",{"title":157,"searchDepth":433,"depth":433,"links":2152},[2153,2154,2155,2165,2170,2174],{"id":1151,"depth":400,"text":1152},{"id":1170,"depth":400,"text":1171},{"id":1202,"depth":400,"text":1203,"children":2156},[2157,2158,2159,2160,2161],{"id":1217,"depth":433,"text":1218},{"id":1341,"depth":433,"text":1342},{"id":1479,"depth":433,"text":1480},{"id":1610,"depth":433,"text":1611},{"id":1701,"depth":433,"text":1702,"children":2162},[2163,2164],{"id":1706,"depth":440,"text":1707},{"id":1716,"depth":440,"text":1717},{"id":1769,"depth":400,"text":1770,"children":2166},[2167,2168,2169],{"id":1773,"depth":433,"text":1774},{"id":1807,"depth":433,"text":1808},{"id":2052,"depth":433,"text":2053},{"id":2061,"depth":400,"text":2062,"children":2171},[2172,2173],{"id":2065,"depth":433,"text":2066},{"id":2094,"depth":433,"text":2095},{"id":2120,"depth":400,"text":2121},"\u002Fcovers\u002Fclean-architecture.webp","2026-07-22","Главная цель Чистой архитектуры: создание системы, которая независима от фреймворков, баз данных, UI и внешних сервисов. Бизнес-логика изолирована и является ядром системы.",{},"\u002Fblog\u002Fclean-architecture",{"title":1146,"description":2177},"blog\u002Fclean-architecture",[1140,1141,386],"3Tl5PCu_W6pu1XBdh2ZiQ86-MKSh9bX59oKlvLOttCU",1792405819914]