{"id":490867,"date":"2026-08-13T15:52:10","date_gmt":"2026-08-13T15:52:10","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=490867"},"modified":"-0001-11-30T00:00:00","modified_gmt":"2026-08-13T15:52:10","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=490867","title":{"rendered":"SOLID \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u043c\u0438\u0440\u0435: LSP \u0431\u0435\u0437 \u043a\u0432\u0430\u0434\u0440\u0430\u0442\u043e\u0432 \u0438 \u043f\u0440\u044f\u043c\u043e\u0443\u0433\u043e\u043b\u044c\u043d\u0438\u043a\u043e\u0432"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0414\u0438\u0441\u043a\u043b\u0435\u0439\u043c\u0435\u0440: \u0441\u0442\u0430\u0442\u044c\u044f \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u0432 \u0443\u0440\u043e\u0432\u043d\u044f junior \u0438 middle, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0437\u043d\u0430\u043a\u043e\u043c\u044f\u0442\u0441\u044f \u0441 \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0430\u043c\u0438 \u0447\u0438\u0441\u0442\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u0438 SOLID. \u041f\u0440\u0438\u043c\u0435\u0440\u044b \u043a\u043e\u0434\u0430 \u2014 PHP 8.1+.<\/p>\n<p>\u0412\u0441\u0435\u043c \u0434\u043e\u0431\u0440\u043e\u0433\u043e \u0434\u043d\u044f! \u041d\u0430 \u0441\u0432\u044f\u0437\u0438 \u0412\u0430\u043b\u0435\u0432\u0438\u0447 \u0410\u0440\u0442\u0435\u043c \u2014 \u0442\u0438\u043c\u043b\u0438\u0434 \u0432 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 <a href=\"https:\/\/www.agima.ru\/?utm_source=habr&amp;utm_medium=article&amp;utm_campaign=solid_lsp_article\">AGIMA<\/a>.<\/p>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/f9\/6b\/5a\/f96b5a9d13b0a943084a49eb8c71a552.png\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/webt\/f9\/6b\/5a\/f96b5a9d13b0a943084a49eb8c71a552.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/webt\/f9\/6b\/5a\/f96b5a9d13b0a943084a49eb8c71a552.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<p>\u0412 <a href=\"https:\/\/habr.com\/ru\/companies\/agima\/articles\/1015408\/\">\u043f\u0435\u0440\u0432\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 \u0441\u0435\u0440\u0438\u0438<\/a> \u043c\u044b \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u043b\u0438 <strong>Single Responsibility Principle<\/strong> \u043d\u0430 \u043a\u0435\u0439\u0441\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u0437\u044b\u0432\u043e\u0432. \u0412\u043e <a href=\"https:\/\/habr.com\/ru\/companies\/agima\/articles\/1065040\/\">\u0432\u0442\u043e\u0440\u043e\u0439<\/a> \u2014 <strong>Open-Closed Principle<\/strong>: \u043f\u043e\u044f\u0432\u0438\u043b\u0438\u0441\u044c FeedbackReportFormatter, ReportSender, \u0434\u0432\u0435 \u043e\u0441\u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f \u2014 \u0444\u043e\u0440\u043c\u0430\u0442\u044b \u043e\u0442\u0447\u0451\u0442\u043e\u0432 \u0438 \u043a\u0430\u043d\u0430\u043b\u044b \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438.<\/p>\n<p>\u041a\u043e\u0434 \u0441\u0442\u0430\u043b \u0440\u0430\u0441\u0448\u0438\u0440\u044f\u0435\u043c\u044b\u043c. \u0418\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044b \u043d\u0430 \u043c\u0435\u0441\u0442\u0435. DI \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438. \u041d\u0430 code review \u0432\u0441\u0451 \u043a\u0440\u0430\u0441\u0438\u0432\u043e.<\/p>\n<p>\u0410 \u0432 \u043f\u044f\u0442\u043d\u0438\u0446\u0443 \u0432\u0435\u0447\u0435\u0440\u043e\u043c cron \u0448\u043b\u0451\u0442 \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u044b\u0439 CSV \u0432 Slack \u2014 \u0438 \u0445\u0432\u043e\u0441\u0442 \u043e\u0442\u0447\u0451\u0442\u0430 \u043e\u0431\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0431\u0435\u0437 \u0435\u0434\u0438\u043d\u043e\u0433\u043e \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u044f. \u041f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u00ab\u043f\u043e\u0434\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u0439\u00bb sender \u043e\u043a\u0430\u0437\u0430\u043b\u0441\u044f \u043d\u0435 \u0441\u043e\u0432\u0441\u0435\u043c \u043f\u043e\u0434\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u043c.<\/p>\n<p>\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0440\u0430\u0437\u0431\u0438\u0440\u0435\u043c \u2014 <strong>Liskov Substitution Principle<\/strong> (LSP, \u043f\u0440\u0438\u043d\u0446\u0438\u043f \u043f\u043e\u0434\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u041b\u0438\u0441\u043a\u043e\u0432). \u0412 \u0443\u0447\u0435\u0431\u043d\u0438\u043a\u0430\u0445 \u0435\u0433\u043e \u043e\u0431\u044a\u044f\u0441\u043d\u044f\u044e\u0442 \u0447\u0435\u0440\u0435\u0437 \u043a\u0432\u0430\u0434\u0440\u0430\u0442 \u0438 \u043f\u0440\u044f\u043c\u043e\u0443\u0433\u043e\u043b\u044c\u043d\u0438\u043a: \u0443 \u043f\u0440\u044f\u043c\u043e\u0443\u0433\u043e\u043b\u044c\u043d\u0438\u043a\u0430 \u043c\u043e\u0436\u043d\u043e \u043c\u0435\u043d\u044f\u0442\u044c \u0448\u0438\u0440\u0438\u043d\u0443 \u0438 \u0432\u044b\u0441\u043e\u0442\u0443 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e, \u0443 \u043a\u0432\u0430\u0434\u0440\u0430\u0442\u0430 \u2014 \u043d\u0435\u0442, \u0437\u043d\u0430\u0447\u0438\u0442 \u043a\u0432\u0430\u0434\u0440\u0430\u0442 \u00ab\u043b\u043e\u043c\u0430\u0435\u0442\u00bb \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043f\u0440\u044f\u043c\u043e\u0443\u0433\u043e\u043b\u044c\u043d\u0438\u043a\u0430.<\/p>\n<p>\u041f\u0440\u0438\u043c\u0435\u0440 \u043a\u043b\u0430\u0441\u0441\u0438\u0447\u0435\u0441\u043a\u0438\u0439. \u0418 \u0431\u0435\u0441\u043f\u043e\u043b\u0435\u0437\u043d\u044b\u0439, \u0435\u0441\u043b\u0438 \u0432\u044b \u043a\u0430\u0436\u0434\u044b\u0439 \u0434\u0435\u043d\u044c \u043d\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u0443\u0435\u0442\u0435 \u0433\u0435\u043e\u043c\u0435\u0442\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u0444\u0438\u0433\u0443\u0440\u044b.<\/p>\n<p>\u041d\u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0435 LSP \u043b\u043e\u043c\u0430\u0435\u0442\u0441\u044f \u0442\u0430\u043c, \u0433\u0434\u0435 OCP \u0443\u0436\u0435 \u0432\u044b\u0438\u0433\u0440\u0430\u043b: \u0432 \u043f\u043e\u043b\u0438\u043c\u043e\u0440\u0444\u043d\u044b\u0445 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430\u0445, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0432\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u043e \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u043b\u0438.<\/p>\n<p><strong>\u0413\u043b\u0430\u0432\u043d\u044b\u0439 \u0432\u043e\u043f\u0440\u043e\u0441:<\/strong> \u043a\u0430\u043a \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0442\u044c LSP, \u043a\u043e\u0433\u0434\u0430 OCP \u0443\u0436\u0435 \u0434\u0430\u043b \u043f\u043e\u043b\u0438\u043c\u043e\u0440\u0444\u0438\u0437\u043c \u2014 \u0438 \u043d\u0435 \u0441\u043b\u043e\u043c\u0430\u0442\u044c \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u00ab\u043f\u043e\u0434\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u043c\u0438\u00bb \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f\u043c\u0438?<\/p>\n<h3>\u0427\u0442\u043e \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0434\u0435\u043b\u0435 \u043e\u0437\u043d\u0430\u0447\u0430\u0435\u0442 LSP<\/h3>\n<p>\u041f\u0440\u0438\u043d\u0446\u0438\u043f \u0441\u0444\u043e\u0440\u043c\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043b\u0430 <a href=\"https:\/\/en.wikipedia.org\/wiki\/Liskov_substitution_principle\">\u0411\u0430\u0440\u0431\u0430\u0440\u0430 \u041b\u0438\u0441\u043a\u043e\u0432<\/a> \u0432 \u0434\u043e\u043a\u043b\u0430\u0434\u0435 <em>Data Abstraction and Hierarchy<\/em> (OOPSLA, 1987). \u041f\u043e\u0437\u0436\u0435, \u0432\u043c\u0435\u0441\u0442\u0435 \u0441 Jeannette Wing, \u0435\u0433\u043e \u0444\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043b\u0438 \u0432 \u0442\u0435\u0440\u043c\u0438\u043d\u0430\u0445 <strong>\u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0447\u0435\u0441\u043a\u0438\u0445 \u043f\u043e\u0434\u0442\u0438\u043f\u043e\u0432<\/strong> \u2014 <em>A Behavioral Notion of Subtyping<\/em> (1994): \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043f\u043e\u0434\u0442\u0438\u043f\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0437\u0430\u043c\u0435\u043d\u044f\u0435\u043c\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c\u0438 \u0431\u0430\u0437\u043e\u0432\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u0431\u0435\u0437 \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0441\u0442\u0438 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b.<\/p>\n<p><a href=\"https:\/\/blog.cleancoder.com\/uncle-bob\/2020\/10\/18\/Solid-Relevance.html\">\u0420\u043e\u0431\u0435\u0440\u0442 \u041c\u0430\u0440\u0442\u0438\u043d<\/a> \u0432\u043a\u043b\u044e\u0447\u0438\u043b LSP \u0432 SOLID \u0438 \u0441\u0444\u043e\u0440\u043c\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043b \u043f\u0440\u043e\u0449\u0435:<\/p>\n<blockquote>\n<p>\u041f\u043e\u0434\u0442\u0438\u043f\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0437\u0430\u043c\u0435\u043d\u044f\u0435\u043c\u044b \u0434\u043b\u044f \u0441\u0432\u043e\u0438\u0445 \u0431\u0430\u0437\u043e\u0432\u044b\u0445 \u0442\u0438\u043f\u043e\u0432.<\/p>\n<\/blockquote>\n<p>\u0417\u0432\u0443\u0447\u0438\u0442 \u043a\u0430\u043a \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 implements. \u041d\u0430 \u0434\u0435\u043b\u0435 \u2014 \u043d\u0435\u0442.<\/p>\n<p><strong>LSP \u2014 \u043f\u0440\u043e \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0438 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u0432\u044b\u0437\u044b\u0432\u0430\u044e\u0449\u0435\u0433\u043e \u043a\u043e\u0434\u0430<\/strong>, \u043d\u0435 \u043f\u0440\u043e \u043a\u043b\u044e\u0447\u0435\u0432\u043e\u0435 \u0441\u043b\u043e\u0432\u043e extends \u0432 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0438 \u043a\u043b\u0430\u0441\u0441\u0430. \u0415\u0441\u043b\u0438 \u043a\u043b\u0438\u0435\u043d\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441 ReportSender, \u043e\u043d \u0432\u043f\u0440\u0430\u0432\u0435 \u043e\u0436\u0438\u0434\u0430\u0442\u044c: \u043b\u044e\u0431\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442 \u043e\u0442\u0447\u0451\u0442 \u043f\u043e \u043e\u0434\u043d\u0438\u043c \u0438 \u0442\u0435\u043c \u0436\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430\u043c. \u041d\u0435 \u00ab\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442, \u0435\u0441\u043b\u0438 \u043f\u043e\u0432\u0435\u0437\u0451\u0442 \u0441 \u0444\u043e\u0440\u043c\u0430\u0442\u043e\u043c\u00bb.<\/p>\n<p>\u0421\u0432\u044f\u0437\u043a\u0430 \u0441 OCP \u043f\u0440\u044f\u043c\u0430\u044f. Open-Closed \u043e\u0431\u0435\u0449\u0430\u0435\u0442: \u043d\u043e\u0432\u043e\u0435 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0440\u044f\u0434\u043e\u043c, \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0439 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u043d\u0435 \u0442\u0440\u043e\u0433\u0430\u0435\u043c. \u041d\u043e \u0435\u0441\u043b\u0438 \u043d\u043e\u0432\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u0432\u0435\u0434\u0451\u0442 \u0441\u0435\u0431\u044f \u0438\u043d\u0430\u0447\u0435, \u0447\u0435\u043c \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u2014 OCP \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u0432 <strong>\u00ab\u043e\u0442\u043a\u0440\u044b\u0442\u043e \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f, \u0437\u0430\u043a\u0440\u044b\u0442\u043e \u0434\u043b\u044f \u0441\u044e\u0440\u043f\u0440\u0438\u0437\u043e\u0432 \u0432 \u043f\u0440\u043e\u0434\u0435\u00bb<\/strong>.<\/p>\n<h3>\u041a\u0435\u0439\u0441: \u043e\u0442\u0437\u044b\u0432\u044b \u043f\u043e\u0441\u043b\u0435 OCP<\/h3>\n<p>\u041d\u0430\u043f\u043e\u043c\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442. \u041f\u0440\u043e\u0434\u0443\u043a\u0442 \u0441 \u0442\u0440\u0435\u043c\u044f \u043a\u0430\u043d\u0430\u043b\u0430\u043c\u0438 \u2014 \u0441\u0430\u0439\u0442, \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, PWA. \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0442\u0437\u044b\u0432 \u0441 \u043e\u0446\u0435\u043d\u043a\u043e\u0439 1\u20135, \u0442\u0435\u043a\u0441\u0442\u043e\u043c \u0438 \u043c\u0435\u0442\u043a\u043e\u0439 \u043a\u0430\u043d\u0430\u043b\u0430. \u041e\u0442\u0437\u044b\u0432\u044b \u043b\u0435\u0436\u0430\u0442 \u0432 PostgreSQL. \u0420\u0430\u0437 \u0432 \u043d\u0435\u0434\u0435\u043b\u044e \u2014 XLSX \u043d\u0430 email, \u043f\u043b\u044e\u0441 \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u044b\u0439 CSV \u0434\u043b\u044f \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u043e\u0432. \u0427\u0430\u0441\u0442\u044c \u043e\u0442\u0447\u0451\u0442\u043e\u0432 \u0443\u0445\u043e\u0434\u0438\u0442 \u0432 Slack.<\/p>\n<p>\u0412 <a href=\"https:\/\/habr.com\/ru\/companies\/agima\/articles\/1065040\/\">OCP-\u0447\u0430\u0441\u0442\u0438<\/a> \u043c\u044b \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u043b\u0438 <strong>\u0442\u043e\u0447\u043a\u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f<\/strong> \u2014 \u0433\u0434\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u044b \u0438 \u043a\u0430\u043d\u0430\u043b\u044b. \u0427\u0435\u0441\u0442\u043d\u043e\u0441\u0442\u044c \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0445 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0439 \u0442\u0430\u043c \u043d\u0435 \u0431\u044b\u043b\u0430 \u0442\u0435\u043c\u043e\u0439: SlackReportSender \u043d\u0430 \u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0435 \u2014 \u043f\u0440\u043e\u0441\u0442\u043e \u0435\u0449\u0451 \u043e\u0434\u043d\u0430 \u0441\u0442\u0440\u0435\u043b\u043a\u0430 \u043a \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443. \u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0441\u043c\u043e\u0442\u0440\u0438\u043c, \u0447\u0442\u043e \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442, \u043a\u043e\u0433\u0434\u0430 \u0441\u0442\u0440\u0435\u043b\u043a\u0430 \u0432\u0440\u0451\u0442.<\/p>\n<p>\u041a \u0444\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0439 \u0442\u043e\u0447\u043a\u0435 OCP \u043c\u044b \u043f\u0440\u0438\u0448\u043b\u0438 \u043a \u0442\u0430\u043a\u043e\u043c\u0443 \u043a\u043e\u0434\u0443:<\/p>\n<pre><code class=\"php\">interface FeedbackReportFormatter{    \/** @param Feedback[] $feedback *\/    public function format(array $feedback): Report;}interface ReportSender{    \/**     * \u0414\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u043d\u044b\u0439 \u043e\u0442\u0447\u0451\u0442. \u041e\u0448\u0438\u0431\u043a\u0438 \u0432\u043d\u0435\u0448\u043d\u0435\u0439 \u0441\u0440\u0435\u0434\u044b \u2014 \u0447\u0435\u0440\u0435\u0437     * \u0441\u043e\u0433\u043b\u0430\u0441\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0442\u0438\u043f, \u0430 \u043d\u0435 \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u044b\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438.     *     * @throws ReportDeliveryException     *\/    public function send(Report $report): void;}class FeedbackReportService{    public function __construct(        private FeedbackRepository $repository    ) {}    public function send(        DateRange $period,        FeedbackReportFormatter $formatter,        ReportSender $sender    ): void {        $feedback = $this-&gt;repository-&gt;getForPeriod($period);        $report = $formatter-&gt;format($feedback);        $sender-&gt;send($report);    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u0435\u0440\u0438\u043e\u0434 \u0436\u0438\u0432\u0451\u0442 \u0441\u043d\u0430\u0440\u0443\u0436\u0438 \u2014 cron \u0437\u043d\u0430\u0435\u0442 \u0440\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435. \u041a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0441\u043e\u0431\u0438\u0440\u0430\u044e\u0442\u0441\u044f \u0432 job\u2019\u0430\u0445:<\/p>\n<pre><code class=\"php\">\/\/ cron: \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u044b\u0439 CSV \u2192 email$reportService-&gt;send(    DateRange::lastDay(),    new CsvFeedbackReportFormatter(),    new EmailReportSender());\/\/ cron: \u0435\u0436\u0435\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u044b\u0439 XLSX \u2192 Slack$reportService-&gt;send(    DateRange::lastWeek(),    new XlsxFeedbackReportFormatter(),    new SlackReportSender());<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>FeedbackReportService \u043d\u0435 \u0437\u043d\u0430\u0435\u0442 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0445 \u043a\u043b\u0430\u0441\u0441\u043e\u0432. \u041e\u043d \u0434\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0443: <strong>\u043b\u044e\u0431\u043e\u0439<\/strong> \u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440 \u0438 <strong>\u043b\u044e\u0431\u043e\u0439<\/strong> sender. \u0418\u043c\u0435\u043d\u043d\u043e \u0437\u0434\u0435\u0441\u044c LSP \u043b\u0438\u0431\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u043b\u0438\u0431\u043e \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043f\u044f\u0442\u043d\u0438\u0447\u043d\u044b\u0439 \u0434\u0435\u043f\u043b\u043e\u0439 \u0432 \u043a\u0432\u0435\u0441\u0442.<\/p>\n<p>\u041e\u0431\u0449\u0438\u0435 \u0442\u0438\u043f\u044b, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044f \u043a\u0435\u0439\u0441, \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u0438\u043b\u0438\u0441\u044c \u0441\u043e \u0432\u0440\u0435\u043c\u0451\u043d OCP- \u0438 SRP-\u0441\u0442\u0430\u0442\u0435\u0439:<\/p>\n<pre><code class=\"php\">final class DateRange{    public function __construct(        public readonly \\DateTimeImmutable $from,        public readonly \\DateTimeImmutable $to,    ) {}    public static function lastDay(): self    {        $to = new \\DateTimeImmutable('today');        return new self($to-&gt;modify('-1 day'), $to);    }    public static function lastWeek(): self    {        $to = new \\DateTimeImmutable('today');        return new self($to-&gt;modify('-7 days'), $to);    }}final class Attachment{    public function __construct(        public readonly string $filename,        public readonly string $content,        public readonly string $mimeType,    ) {}}final class Feedback{    public function __construct(        public readonly int $id,        public readonly int $rating,        public readonly string $text,        public readonly \\DateTimeImmutable $createdAt,        public readonly string $channel, \/\/ site | app | pwa    ) {}}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0410 \u0432\u043e\u0442 Report \u0432 \u044d\u0442\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 \u043f\u043e\u043c\u0435\u043d\u044f\u043b\u0441\u044f: \u0440\u044f\u0434\u043e\u043c \u0441 format() \u043f\u043e\u044f\u0432\u0438\u043b\u0438\u0441\u044c isTextual() \u0438 body() \u2014 \u043e\u043d\u0438 \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u044f\u0442\u0441\u044f \u0434\u0430\u043b\u044c\u0448\u0435, \u0447\u0442\u043e\u0431\u044b sender \u043c\u043e\u0433 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0442\u044c \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043f\u043e \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0443 \u043e\u0442\u0447\u0451\u0442\u0430, \u0430 \u043d\u0435 \u043f\u043e \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044e \u0444\u043e\u0440\u043c\u0430\u0442\u043e\u0432 \u0432 \u043a\u043e\u0434\u0435:<\/p>\n<pre><code class=\"php\">final class Report{    private const TEXTUAL_FORMATS = ['csv'];    public function __construct(        private string $format, \/\/ csv | xlsx | pdf        private string $body,        private string $filename,    ) {}    public function format(): string    {        return $this-&gt;format;    }    \/** csv \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0442\u0435\u043a\u0441\u0442; xlsx\/pdf \u2014 \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u0435 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u044b *\/    public function isTextual(): bool    {        return in_array($this-&gt;format, self::TEXTUAL_FORMATS, true);    }    public function body(): string    {        return $this-&gt;body;    }    public function mimeType(): string    {        return match ($this-&gt;format) {            'csv' =&gt; 'text\/csv',            'xlsx' =&gt; 'application\/vnd.openxmlformats-officedocument.spreadsheetml.sheet',            'pdf' =&gt; 'application\/pdf',            default =&gt; 'application\/octet-stream',        };    }    public function asAttachment(): Attachment    {        return new Attachment($this-&gt;filename, $this-&gt;body, $this-&gt;mimeType());    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h3>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 1: \u00ab\u043f\u043e\u0434\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u0439\u00bb sender \u0441 \u0443\u0436\u0435\u0441\u0442\u043e\u0447\u0451\u043d\u043d\u044b\u043c\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430\u043c\u0438<\/h3>\n<p>\u0420\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043f\u0438\u0448\u0435\u0442 SlackReportSender. \u0423 Slack \u0435\u0441\u0442\u044c \u043f\u0440\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f \u043d\u0430 \u0440\u0430\u0437\u043c\u0435\u0440 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f (\u0443\u0442\u043e\u0447\u043d\u044f\u0439\u0442\u0435 \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u044b \u2014 \u043e\u043d\u043e \u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f). \u0420\u0430\u0437\u0443\u043c\u043d\u0430\u044f \u043c\u044b\u0441\u043b\u044c. \u041d\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0441\u044f \u0442\u0430\u043a\u043e\u0439:<\/p>\n<pre><code class=\"php\">class SlackReportSender implements ReportSender{    private const MAX_MESSAGE_LENGTH = 4000;    public function __construct(        private SlackClient $slack,        private string $channelId,    ) {}    public function send(Report $report): void    {        if (!$report-&gt;isTextual()) {            throw new \\InvalidArgumentException('Slack sender \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u044b\u0435 \u043e\u0442\u0447\u0451\u0442\u044b');        }        $fullText = $report-&gt;body();        $text = mb_strlen($fullText) &gt; self::MAX_MESSAGE_LENGTH            ? mb_substr($fullText, 0, self::MAX_MESSAGE_LENGTH) \/\/ \u043c\u043e\u043b\u0447\u0430 \u0440\u0435\u0436\u0435\u043c \u0445\u0432\u043e\u0441\u0442            : $fullText;        $this-&gt;slack-&gt;postMessage($this-&gt;channelId, $text);    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0414\u043b\u044f \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u044f \u2014 EmailReportSender, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c cron \u0443\u0436\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0431\u0435\u0437 \u0441\u044e\u0440\u043f\u0440\u0438\u0437\u043e\u0432:<\/p>\n<pre><code class=\"php\">class EmailReportSender implements ReportSender{    public function __construct(        private Mailer $mailer,        private string $recipient,    ) {}    public function send(Report $report): void    {        $this-&gt;mailer-&gt;send(            to: $this-&gt;recipient,            subject: '\u041e\u0442\u0447\u0451\u0442 \u043f\u043e \u043e\u0442\u0437\u044b\u0432\u0430\u043c',            attachment: $report-&gt;asAttachment(),        );    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0424\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e SlackReportSender \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u0442 ReportSender. \u041d\u0430 \u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0435 \u2014 \u0435\u0449\u0451 \u043e\u0434\u043d\u0430 \u0441\u0442\u0440\u0435\u043b\u043e\u0447\u043a\u0430 \u043a \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443.<\/p>\n<p>\u041f\u0440\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u2014 \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0435 LSP, \u043f\u0440\u0438\u0447\u0451\u043c \u0441\u0440\u0430\u0437\u0443 \u0434\u0432\u043e\u0439\u043d\u043e\u0435. \u0412\u0430\u0436\u043d\u043e: \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0435 \u2014 \u043e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u043e <strong>\u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430<\/strong> \u0438 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0430 ReportSender (\u00ab\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043b\u044e\u0431\u043e\u0439 Report \u0446\u0435\u043b\u0438\u043a\u043e\u043c\u00bb), \u0430 \u043d\u0435 \u043e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u00ab\u0441\u0440\u0435\u0434\u043d\u0435\u0433\u043e \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u044f\u00bb \u0441\u043e\u0441\u0435\u0434\u043d\u0438\u0445 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0439. \u0421\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u0435 \u0441 EmailReportSender \u043d\u0438\u0436\u0435 \u2014 \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u043a\u043b\u0438\u0435\u043d\u0442 \u0443\u0436\u0435 \u043f\u0440\u0438\u0432\u044b\u043a \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c; \u0441\u0430\u043c \u043f\u043e \u0441\u0435\u0431\u0435 \u0444\u0430\u043a\u0442 \u00abEmail \u0432\u0435\u0434\u0451\u0442 \u0441\u0435\u0431\u044f \u0438\u043d\u0430\u0447\u0435\u00bb \u043d\u0435 \u0437\u0430\u0434\u0430\u0451\u0442 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u0434\u043b\u044f Slack.<\/p>\n<p><strong>\u0443\u0436\u0435\u0441\u0442\u043e\u0447\u0430\u0435\u0442 \u043f\u0440\u0435\u0434\u0443\u0441\u043b\u043e\u0432\u0438\u0435<\/strong> \u043d\u0430 \u0432\u0445\u043e\u0434: \u043e\u0442\u043a\u043b\u043e\u043d\u044f\u0435\u0442 XLSX \u2014 \u0445\u043e\u0442\u044f \u043a\u043b\u0438\u0435\u043d\u0442 ReportSender \u0432\u043f\u0440\u0430\u0432\u0435 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u043b\u044e\u0431\u043e\u0439 Report (\u0438 EmailReportSender \u044d\u0442\u043e \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442);<\/p>\n<p><strong>\u043e\u0441\u043b\u0430\u0431\u043b\u044f\u0435\u0442 \u043f\u043e\u0441\u0442\u0443\u0441\u043b\u043e\u0432\u0438\u0435<\/strong> \u043d\u0430 \u0442\u043e\u043c, \u0447\u0442\u043e \u043f\u0440\u0438\u043d\u044f\u043b: \u043c\u043e\u043b\u0447\u0430 \u043e\u0431\u0440\u0435\u0437\u0430\u0435\u0442 \u0434\u043b\u0438\u043d\u043d\u044b\u0439 CSV \u2014 \u0445\u043e\u0442\u044f \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u043e\u0431\u0435\u0449\u0430\u0435\u0442 \u043f\u043e\u043b\u043d\u0443\u044e \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0443 (\u0438 email \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u0431\u044b \u043e\u0442\u0447\u0451\u0442 \u0446\u0435\u043b\u0438\u043a\u043e\u043c).<\/p>\n<p>\u042d\u0442\u043e \u0438 \u0435\u0441\u0442\u044c \u043f\u044f\u0442\u043d\u0438\u0447\u043d\u044b\u0439 \u0438\u043d\u0446\u0438\u0434\u0435\u043d\u0442 \u0438\u0437 \u0438\u043d\u0442\u0440\u043e: \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u044b\u0439 CSV \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044e \u0438 \u0442\u0438\u0445\u043e \u0442\u0435\u0440\u044f\u0435\u0442 \u0445\u0432\u043e\u0441\u0442, \u0435\u0441\u043b\u0438 \u043e\u043d \u0434\u043b\u0438\u043d\u043d\u0435\u0435 \u043b\u0438\u043c\u0438\u0442\u0430 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u2014 \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0438\u043c\u0435\u043d\u043d\u043e \u043f\u043e\u0442\u0435\u0440\u044f\u0435\u0442\u0441\u044f, \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u043e\u0431\u044a\u0451\u043c\u0430 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u043e\u0442\u0447\u0451\u0442\u0430, \u043d\u0435 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u00ab\u043f\u043e\u043b\u043e\u0432\u0438\u043d\u0430\u00bb. \u0410 \u0435\u0436\u0435\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u044b\u0439 XLSX \u0432 Slack \u0432 \u044d\u0442\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043e\u0434\u0430 \u043f\u0430\u0434\u0430\u0435\u0442 \u0441 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u043c \u0441\u0440\u0430\u0437\u0443 \u2014 \u0442\u043e \u0435\u0441\u0442\u044c \u043e\u0442\u0447\u0451\u0442 \u0442\u0435\u0440\u044f\u0435\u0442\u0441\u044f \u0446\u0435\u043b\u0438\u043a\u043e\u043c, \u0430 \u043d\u0435 \u0447\u0430\u0441\u0442\u0438\u0447\u043d\u043e, \u0447\u0442\u043e \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u044f \u043d\u0438\u0447\u0435\u043c \u043d\u0435 \u043b\u0443\u0447\u0448\u0435.<\/p>\n<p>\u0427\u0435\u0440\u0435\u0437 \u043d\u0435\u0434\u0435\u043b\u044e \u0432 \u0432\u044b\u0437\u044b\u0432\u0430\u044e\u0449\u0435\u043c \u043a\u043e\u0434\u0435 \u043f\u043e\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f:<\/p>\n<pre><code class=\"php\">$formatter = new XlsxFeedbackReportFormatter();$sender = $channel === 'slack'    ? new SlackReportSender($slack, $channelId)    : new EmailReportSender($mailer, $recipient);if ($sender instanceof SlackReportSender &amp;&amp; $formatter instanceof XlsxFeedbackReportFormatter) {    $sender = new EmailReportSender($mailer, $recipient); \/\/ \u043e\u0431\u0445\u043e\u0434\u043d\u043e\u0439 \u043c\u0430\u043d\u0451\u0432\u0440}$reportService-&gt;send(DateRange::lastWeek(), $formatter, $sender);<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>instanceof \u0432 \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0435 \u2014 \u043a\u043b\u0430\u0441\u0441\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0437\u0430\u043f\u0430\u0445: <strong>\u043f\u043e\u0434\u0442\u0438\u043f \u043d\u0435\u043b\u044c\u0437\u044f \u0447\u0435\u0441\u0442\u043d\u043e \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0432\u043c\u0435\u0441\u0442\u043e \u0431\u0430\u0437\u043e\u0432\u043e\u0433\u043e \u0442\u0438\u043f\u0430<\/strong>. \u0418\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043e\u0431\u0435\u0449\u0430\u0435\u0442 \u043e\u0434\u043d\u043e, \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u2014 \u0434\u0440\u0443\u0433\u043e\u0435.<\/p>\n<h3>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 2: \u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u0435 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f \u00ab\u0434\u043b\u044f \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430\u00bb<\/h3>\n<p>\u0412\u0435\u0440\u043d\u0451\u043c\u0441\u044f \u043a \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044e. \u0412 <a href=\"https:\/\/habr.com\/ru\/companies\/agima\/articles\/1015408\/\">SRP-\u0447\u0430\u0441\u0442\u0438<\/a> FeedbackRepository \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0438 \u0447\u0442\u0435\u043d\u0438\u0435:<\/p>\n<pre><code class=\"php\">class FeedbackRepository{    public function __construct(private \\PDO $pdo) {}    public function save(Feedback $feedback): void    {        $stmt = $this-&gt;pdo-&gt;prepare(            'INSERT INTO feedback (rating, text, channel, created_at)             VALUES (:rating, :text, :channel, :created_at)'        );        $stmt-&gt;execute([            'rating' =&gt; $feedback-&gt;rating,            'text' =&gt; $feedback-&gt;text,            'channel' =&gt; $feedback-&gt;channel,            'created_at' =&gt; $feedback-&gt;createdAt-&gt;format('Y-m-d H:i:s'),        ]);    }    public function getForPeriod(DateRange $period): array    {        $stmt = $this-&gt;pdo-&gt;prepare(            'SELECT id, rating, text, channel, created_at             FROM feedback             WHERE created_at BETWEEN :from AND :to             ORDER BY created_at'        );        $stmt-&gt;execute([            'from' =&gt; $period-&gt;from-&gt;format('Y-m-d H:i:s'),            'to' =&gt; $period-&gt;to-&gt;format('Y-m-d H:i:s'),        ]);        return array_map(            fn(array $row) =&gt; new Feedback(                id: (int) $row['id'],                rating: (int) $row['rating'],                text: $row['text'],                createdAt: new \\DateTimeImmutable($row['created_at']),                channel: $row['channel'],            ),            $stmt-&gt;fetchAll(\\PDO::FETCH_ASSOC)        );    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c LSP \u0438 <strong>Interface Segregation<\/strong> \u0441\u043c\u044b\u043a\u0430\u044e\u0442\u0441\u044f \u0437\u0430\u0440\u0430\u043d\u0435\u0435: \u0442\u043e, \u0447\u0442\u043e \u043f\u0440\u043e\u0438\u0437\u043e\u0439\u0434\u0451\u0442 \u0434\u0430\u043b\u044c\u0448\u0435 \u2014 \u0441\u0438\u043c\u043f\u0442\u043e\u043c \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u044f \u043f\u043e\u0434\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438, \u043d\u043e \u0435\u0433\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0430 \u2014 \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0448\u0438\u0440\u043e\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441. \u041f\u043e\u043b\u043d\u044b\u0439 \u0440\u0430\u0437\u0431\u043e\u0440 \u0441\u0435\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438 \u0431\u0443\u0434\u0435\u0442 \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435, \u043f\u043e\u0441\u0432\u044f\u0449\u0451\u043d\u043d\u043e\u0439 ISP, \u0437\u0434\u0435\u0441\u044c \u0444\u0438\u043a\u0441\u0438\u0440\u0443\u0435\u043c \u0442\u043e\u043b\u044c\u043a\u043e LSP-\u0447\u0430\u0441\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b.<\/p>\n<p>\u041f\u043e\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0437\u0430\u0434\u0430\u0447\u0430: \u043d\u0443\u0436\u0435\u043d \u0432\u043e\u0440\u043a\u0435\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u0437\u044b\u0432\u044b \u0432\u043e \u0432\u043d\u0435\u0448\u043d\u044e\u044e \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0443\u044e \u0441\u0438\u0441\u0442\u0435\u043c\u0443, \u2014 \u0435\u043c\u0443 \u043d\u0443\u0436\u043d\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u0435\u043d\u0438\u0435. \u041a\u0442\u043e-\u0442\u043e \u0440\u0435\u0448\u0430\u0435\u0442 \u00ab\u0443\u043d\u0430\u0441\u043b\u0435\u0434\u0443\u0435\u043c\u0441\u044f \u2014 \u043c\u0435\u043d\u044c\u0448\u0435 \u043a\u043e\u0434\u0430\u00bb:<\/p>\n<pre><code class=\"php\">class ReadOnlyFeedbackRepository extends FeedbackRepository{    public function save(Feedback $feedback): void    {        throw new \\LogicException('Read-only repository');    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041a\u043b\u0430\u0441\u0441\u0438\u043a\u0430 \u0443\u0447\u0435\u0431\u043d\u0438\u043a\u043e\u0432. \u0418 \u043a\u043b\u0430\u0441\u0441\u0438\u043a\u0430 \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u043d-\u0431\u0430\u0433\u043e\u0432.<\/p>\n<p>FeedbackReportService \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 FeedbackRepository \u2014 \u0445\u043e\u0442\u044f \u0434\u043b\u044f \u043e\u0442\u0447\u0451\u0442\u043d\u043e\u0433\u043e \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u044f write \u043d\u0435 \u043d\u0443\u0436\u0435\u043d. \u0417\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044c \u043e\u0442 \u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f \u0441\u0430\u043c\u0430 \u043f\u043e \u0441\u0435\u0431\u0435 \u0440\u0430\u0441\u0448\u0438\u0440\u044f\u0435\u0442 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442: \u0441\u0435\u0440\u0432\u0438\u0441\u0443 \u0442\u0435\u043e\u0440\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d save(), \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043e\u043d \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u0432\u044b\u0437\u043e\u0432\u0435\u0442. \u0422\u0435\u0441\u0442\u044b \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0442 \u043c\u043e\u043a. \u0414\u043b\u044f \u044d\u043a\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0433\u043e \u0432\u043e\u0440\u043a\u0435\u0440\u0430 \u2014 ReadOnlyFeedbackRepository. \u0422\u0438\u043f\u044b \u0441\u0445\u043e\u0434\u044f\u0442\u0441\u044f.<\/p>\n<p>\u041f\u043e\u043a\u0430 \u043f\u043e\u043b\u0433\u043e\u0434\u0430 \u0441\u043f\u0443\u0441\u0442\u044f \u043f\u0440\u0430\u0432\u0438\u043b\u043e autowiring \u0432 DI-\u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0435 \u043d\u0435 \u043f\u0435\u0440\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0442\u043e\u0442 \u0436\u0435 \u0431\u0438\u043d\u0434\u0438\u043d\u0433 \u0442\u0430\u043c, \u0433\u0434\u0435 \u0435\u0433\u043e \u043d\u0435 \u0436\u0434\u0430\u043b\u0438: \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0432 \u043a\u043e\u043d\u0441\u043e\u043b\u044c\u043d\u043e\u0439 \u043a\u043e\u043c\u0430\u043d\u0434\u0435 \u0431\u044d\u043a\u0444\u0438\u043b\u043b\u0430, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0434\u043e\u043b\u0436\u043d\u0430 \u0434\u043e\u0437\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u043f\u0440\u043e\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0435 \u043e\u0442\u0437\u044b\u0432\u044b \u0438\u0437 \u043e\u0447\u0435\u0440\u0435\u0434\u0438 \u043f\u043e\u0441\u043b\u0435 \u0438\u043d\u0446\u0438\u0434\u0435\u043d\u0442\u0430. \u041a\u043e\u043c\u0430\u043d\u0434\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 ReadOnlyFeedbackRepository \u0432\u043c\u0435\u0441\u0442\u043e \u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f \u2014 \u0438 \u043f\u0430\u0434\u0430\u0435\u0442 \u0432 \u0440\u0430\u043d\u0442\u0430\u0439\u043c\u0435 \u043d\u0430 save(). \u041f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e <strong>\u043f\u043e\u0434\u0442\u0438\u043f \u043d\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442 \u043f\u043e\u0441\u0442\u0443\u0441\u043b\u043e\u0432\u0438\u0435<\/strong>** ****save()**: \u0431\u0430\u0437\u043e\u0432\u044b\u0439 \u0442\u0438\u043f \u043e\u0431\u0435\u0449\u0430\u0435\u0442 \u0443\u0441\u043f\u0435\u0448\u043d\u043e\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435, \u043f\u043e\u0434\u0442\u0438\u043f \u2014 \u043d\u0435\u0442.<\/p>\n<p>\u0414\u0440\u0443\u0433\u043e\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u2014 CachingFeedbackRepository <strong>\u0431\u0435\u0437 \u0438\u043d\u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438<\/strong>:<\/p>\n<pre><code class=\"php\">class CachingFeedbackRepository extends FeedbackRepository{    \/** @var array&lt;string, Feedback[]&gt; *\/    private array $cache = [];    public function getForPeriod(DateRange $period): array    {        $key = $period-&gt;from-&gt;format(DATE_ATOM) . ':' . $period-&gt;to-&gt;format(DATE_ATOM);        return $this-&gt;cache[$key] ??= parent::getForPeriod($period);    }    public function save(Feedback $feedback): void    {        parent::save($feedback);        \/\/ \u043a\u0435\u0448 \u043d\u0435 \u0441\u0431\u0440\u043e\u0448\u0435\u043d \u2014 getForPeriod() \u0432\u0435\u0440\u043d\u0451\u0442 \u0441\u043d\u0438\u043c\u043e\u043a \u0434\u043e save()    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>save() \u043f\u0438\u0448\u0435\u0442 \u0432 \u0411\u0414, \u043d\u043e \u043a\u0435\u0448 \u043f\u043e \u043a\u043b\u044e\u0447\u0443 \u043f\u0435\u0440\u0438\u043e\u0434\u0430 \u043d\u0435 \u0441\u0431\u0440\u0430\u0441\u044b\u0432\u0430\u0435\u0442\u0441\u044f. \u0415\u0436\u0435\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u043e\u0442\u0447\u0451\u0442, \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0432 \u0442\u043e\u043c \u0436\u0435 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0435, \u0443\u0445\u043e\u0434\u0438\u0442 \u0441 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438. \u041f\u043e\u0434\u0442\u0438\u043f <strong>\u043e\u0441\u043b\u0430\u0431\u0438\u043b \u043f\u043e\u0441\u0442\u0443\u0441\u043b\u043e\u0432\u0438\u0435<\/strong>** ****getForPeriod()**: \u00ab\u0432\u0435\u0440\u043d\u0451\u0442 \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0435 \u043e\u0442\u0437\u044b\u0432\u044b \u0437\u0430 \u043f\u0435\u0440\u0438\u043e\u0434\u00bb.<\/p>\n<p>\u041a\u0432\u0430\u0434\u0440\u0430\u0442 \u0438 \u043f\u0440\u044f\u043c\u043e\u0443\u0433\u043e\u043b\u044c\u043d\u0438\u043a \u2014 \u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0446\u0438\u044f. \u041e\u0442\u0447\u0451\u0442 \u0441 \u043d\u0435\u043f\u043e\u043b\u043d\u044b\u043c\u0438 \u043e\u0442\u0437\u044b\u0432\u0430\u043c\u0438 \u2014 \u0443\u0436\u0435 \u0440\u0430\u0437\u0433\u043e\u0432\u043e\u0440 \u0441 \u043f\u0440\u043e\u0434\u0430\u043a\u0442\u043e\u043c \u0432 \u043f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a \u0443\u0442\u0440\u043e\u043c.<\/p>\n<h3>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 3: \u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440 \u0441 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u043a\u043e\u0439<\/h3>\n<p>\u0422\u0440\u0435\u0442\u044c\u0435 \u043c\u0435\u0441\u0442\u043e, \u0433\u0434\u0435 LSP \u043b\u043e\u043c\u0430\u0435\u0442\u0441\u044f \u043d\u0435\u0437\u0430\u043c\u0435\u0442\u043d\u043e \u2014 FeedbackReportFormatter. \u041f\u0435\u0440\u0438\u043e\u0434 \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043d\u0430\u0440\u0443\u0436\u0438, \u0438 \u0441\u0435\u0440\u0432\u0438\u0441 \u0447\u0435\u0441\u0442\u043d\u043e \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u0442 \u0443 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f \u0432\u0441\u0435 \u043e\u0442\u0437\u044b\u0432\u044b \u0437\u0430 DateRange. \u041d\u043e \u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440 \u043c\u043e\u0436\u0435\u0442 \u0442\u0438\u0445\u043e \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0441\u043c\u044b\u0441\u043b \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<p><strong>\u0422\u0438\u0445\u0430\u044f \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u043d\u0435\u0442 \u0432 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0435:<\/strong><\/p>\n<pre><code class=\"php\">class CsvFeedbackReportFormatter implements FeedbackReportFormatter{    public function format(array $feedback): Report    {        \/\/ \u0431\u0438\u0437\u043d\u0435\u0441 \u043d\u0435 \u043f\u0440\u043e\u0441\u0438\u043b, \u043d\u043e \u00ab\u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0430\u043c \u043d\u0443\u0436\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0445\u043e\u0440\u043e\u0448\u0438\u0435\u00bb        $filtered = array_filter($feedback, fn(Feedback $f) =&gt; $f-&gt;rating &gt;= 4);        $rows = array_map(            fn(Feedback $f) =&gt; [$f-&gt;id, $f-&gt;rating, $f-&gt;text, $f-&gt;channel],            $filtered        );        $body = (new CsvWriter())-&gt;write(['id', 'rating', 'text', 'channel'], $rows);        return new Report('csv', $body, 'feedback-daily.csv');    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>FeedbackReportService \u043f\u0435\u0440\u0435\u0434\u0430\u043b \u0432 format() \u0432\u0441\u0435 \u043e\u0442\u0437\u044b\u0432\u044b \u0437\u0430 \u0441\u0443\u0442\u043a\u0438 \u2014 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439 \u043e\u0442\u0440\u0430\u0431\u043e\u0442\u0430\u043b \u043f\u043e $period. \u0410 CsvFeedbackReportFormatter \u0442\u0438\u0445\u043e \u043e\u0442\u0431\u0440\u0430\u0441\u044b\u0432\u0430\u0435\u0442 \u0447\u0430\u0441\u0442\u044c. XlsxFeedbackReportFormatter \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441\u043e \u0432\u0441\u0435\u043c \u043c\u0430\u0441\u0441\u0438\u0432\u043e\u043c. \u041f\u043e\u0434\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0444\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430, \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u043a\u0430 \u2014 \u0440\u0430\u0437\u043d\u0430\u044f.<\/p>\n<p>\u0412\u0430\u0436\u043d\u043e: \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043d\u0435 \u0432 \u0441\u0430\u043c\u043e\u043c \u0444\u0430\u043a\u0442\u0435 \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438, \u0430 \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e \u043e\u043d\u0430 \u0441\u043f\u0440\u044f\u0442\u0430\u043d\u0430 \u0432\u043d\u0443\u0442\u0440\u0438 \u043a\u043b\u0430\u0441\u0441\u0430, \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u044e\u0449\u0435\u0433\u043e \u043e\u0431\u0449\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441. \u041f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435 \u043a\u043b\u0430\u0441\u0441\u0430 \u0432 HighRatedCsvFeedbackReportFormatter \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u043d\u0435 \u0440\u0435\u0448\u0430\u0435\u0442 \u2014 FeedbackReportService \u043f\u043e-\u043f\u0440\u0435\u0436\u043d\u0435\u043c\u0443 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 <strong>\u043b\u044e\u0431\u043e\u0439<\/strong> FeedbackReportFormatter, \u0438 \u043f\u043e\u0434\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u043e\u0434\u043d\u043e\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u043c\u0435\u0441\u0442\u043e \u0434\u0440\u0443\u0433\u043e\u0439 \u043f\u043e-\u043f\u0440\u0435\u0436\u043d\u0435\u043c\u0443 \u043c\u0435\u043d\u044f\u0435\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 \u043e\u0442\u0447\u0451\u0442\u0430. \u041a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 FeedbackReportFormatter \u0434\u043e\u043b\u0436\u0435\u043d \u0437\u0432\u0443\u0447\u0430\u0442\u044c \u043e\u0434\u043d\u043e\u0437\u043d\u0430\u0447\u043d\u043e: <strong>\u043d\u0430 \u0432\u044b\u0445\u043e\u0434\u0435 \u2014 \u043e\u0442\u0447\u0451\u0442 \u0438\u0437 \u0432\u0441\u0435\u0445 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0445 \u0437\u0430\u043f\u0438\u0441\u0435\u0439, \u0431\u0435\u0437 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439<\/strong>. \u0415\u0441\u043b\u0438 \u0431\u0438\u0437\u043d\u0435\u0441\u0443 \u043d\u0443\u0436\u043d\u0430 \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044f \u2014 \u044d\u0442\u043e \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0430\u044f \u043e\u0441\u044c \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439, \u0438 \u043c\u0435\u0441\u0442\u043e \u0434\u043b\u044f \u043d\u0435\u0451 \u043d\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440, \u0430 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0434\u0430\u043d\u043d\u044b\u0445. \u0420\u0430\u0437\u0431\u0435\u0440\u0451\u043c \u044d\u0442\u043e \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u0441 \u0440\u0435\u0448\u0435\u043d\u0438\u0435\u043c.<\/p>\n<p><strong>\u041c\u0443\u0442\u0430\u0446\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0432\u043d\u0443\u0442\u0440\u0438 \u0432\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043c\u0430\u0441\u0441\u0438\u0432\u0430:<\/strong><\/p>\n<p>\u0412 PHP \u043c\u0430\u0441\u0441\u0438\u0432 \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442\u0441\u044f \u043f\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044e, \u043d\u043e <strong>\u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0432\u043d\u0443\u0442\u0440\u0438 \u2014 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435<\/strong>. \u0412 \u043d\u0430\u0448\u0435\u043c \u043a\u0435\u0439\u0441\u0435 Feedback \u2014 readonly: \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c $feedback-&gt;text \u043d\u0435\u043b\u044c\u0437\u044f, \u0438 \u0446\u0435\u043b\u044b\u0439 \u043a\u043b\u0430\u0441\u0441 \u0442\u0430\u043a\u0438\u0445 \u0431\u0430\u0433\u043e\u0432 \u0441\u043d\u0438\u043c\u0430\u0435\u0442\u0441\u044f \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 \u0442\u0438\u043f\u0430. \u041d\u043e \u0432 \u043b\u0435\u0433\u0430\u0441\u0438-\u0441\u043b\u043e\u0435 \u043e\u0442\u0437\u044b\u0432\u044b \u0447\u0430\u0441\u0442\u043e \u043b\u0435\u0436\u0430\u0442 \u0432 mutable-DTO \u2014 \u0438 \u0442\u0430\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440 \u043c\u043e\u0436\u0435\u0442 \u0442\u0438\u0445\u043e \u043f\u0435\u0440\u0435\u043f\u0438\u0441\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435:<\/p>\n<pre><code class=\"php\">class FeedbackRecord \/\/ \u043b\u0435\u0433\u0430\u0441\u0438-\u043c\u043e\u0434\u0435\u043b\u044c, \u043d\u0435 readonly{    public function __construct(        public int $id,        public int $rating,        public string $text,        public \\DateTimeImmutable $createdAt,        public string $channel,    ) {}}class LegacyXlsxFeedbackReportFormatter implements FeedbackReportFormatter{    \/** @param FeedbackRecord[] $feedback *\/    public function format(array $feedback): Report    {        foreach ($feedback as $record) {            \/\/ \u00ab\u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0443\u0435\u043c\u00bb \u0442\u0435\u043a\u0441\u0442 \u043f\u0440\u044f\u043c\u043e \u0432 \u043e\u0431\u044a\u0435\u043a\u0442\u0435            $record-&gt;text = trim(strip_tags($record-&gt;text));        }        $body = (new XlsxWriter())-&gt;fromRecords($feedback);        return new Report('xlsx', $body, 'feedback-weekly.xlsx');    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0434\u0435\u043b\u0435 <strong>\u0434\u0432\u0430<\/strong> \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u044f LSP, \u0438 \u043e\u0431\u0430 \u0441\u0442\u043e\u0438\u0442 \u0443\u043c\u0435\u0442\u044c \u0440\u0430\u0437\u043b\u0438\u0447\u0430\u0442\u044c.<\/p>\n<p>\u041f\u0435\u0440\u0432\u043e\u0435 \u2014 \u043c\u0443\u0442\u0430\u0446\u0438\u044f: \u0441\u0435\u0440\u0432\u0438\u0441 \u043f\u0435\u0440\u0435\u0434\u0430\u043b \u043c\u0430\u0441\u0441\u0438\u0432 \u0432 format() \u0438 \u043c\u043e\u0436\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0442\u043e\u0442 \u0436\u0435 \u043c\u0430\u0441\u0441\u0438\u0432 \u0434\u0430\u043b\u044c\u0448\u0435 \u2014 \u0432 \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435, \u043c\u0435\u0442\u0440\u0438\u043a\u0438, \u0434\u0440\u0443\u0433\u043e\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a. \u0422\u0435\u043a\u0441\u0442\u044b \u0443\u0436\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u044b. \u0414\u0440\u0443\u0433\u0438\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043d\u0435 \u0442\u0440\u043e\u0433\u0430\u044e\u0442. \u041a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u00ab\u043d\u0430 \u0432\u0445\u043e\u0434\u0435 \u2014 \u043e\u0442\u0437\u044b\u0432\u044b \u0432 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u043c \u0432\u0438\u0434\u0435\u00bb \u043d\u0430\u0440\u0443\u0448\u0435\u043d \u043d\u0435\u044f\u0432\u043d\u043e.<\/p>\n<p>\u0412\u0442\u043e\u0440\u043e\u0435 \u2014 \u043c\u0435\u043d\u0435\u0435 \u043e\u0447\u0435\u0432\u0438\u0434\u043d\u043e\u0435: LegacyXlsxFeedbackReportFormatter \u043e\u0431\u044a\u044f\u0432\u043b\u044f\u0435\u0442 <a class=\"mention\" href=\"\/users\/param\">@param<\/a> FeedbackRecord[], \u0445\u043e\u0442\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 <a class=\"mention\" href=\"\/users\/param\">@param<\/a> Feedback[]. \u042d\u0442\u043e <strong>\u0441\u0443\u0436\u0435\u043d\u0438\u0435 \u0442\u0438\u043f\u0430 \u0432\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430<\/strong> \u2014 \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0435 LSP \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 \u0441\u0438\u0433\u043d\u0430\u0442\u0443\u0440\u044b (\u043a\u043e\u043d\u0442\u0440\u0430\u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043d\u043e\u0441\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0430 \u0432 \u043e\u0431\u0440\u0430\u0442\u043d\u0443\u044e \u0441\u0442\u043e\u0440\u043e\u043d\u0443). \u0421\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440 (PHPStan, Psalm) \u043d\u0430 \u043f\u0440\u0438\u043b\u0438\u0447\u043d\u043e\u043c \u0443\u0440\u043e\u0432\u043d\u0435 \u0441\u0442\u0440\u043e\u0433\u043e\u0441\u0442\u0438 \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u0438\u0442 \u044d\u0442\u043e \u0440\u0430\u043d\u044c\u0448\u0435, \u0447\u0435\u043c \u0437\u0430\u043c\u0435\u0442\u0438\u0442 \u0447\u0435\u043b\u043e\u0432\u0435\u043a \u043d\u0430 \u043a\u043e\u0434-\u0440\u0435\u0432\u044c\u044e. \u0427\u0435\u0441\u0442\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u2014 \u043d\u0435 \u043f\u043e\u0434\u043c\u0435\u0448\u0438\u0432\u0430\u0442\u044c \u043b\u0435\u0433\u0430\u0441\u0438-\u0442\u0438\u043f \u0432 \u043e\u0431\u0449\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u0430 \u0440\u0430\u0437\u0432\u0435\u0441\u0442\u0438 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u044b: \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0434\u043b\u044f \u043b\u0435\u0433\u0430\u0441\u0438-\u043a\u043e\u043d\u0432\u0435\u0439\u0435\u0440\u0430, \u043b\u0438\u0431\u043e \u044f\u0432\u043d\u043e\u0435 \u043f\u0440\u0438\u0432\u0435\u0434\u0435\u043d\u0438\u0435 FeedbackRecord \u043a Feedback \u043d\u0430 \u0433\u0440\u0430\u043d\u0438\u0446\u0435 \u043f\u0435\u0440\u0435\u0434 \u0432\u044b\u0437\u043e\u0432\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440\u0430:<\/p>\n<pre><code class=\"php\">interface LegacyFeedbackReportFormatter{    \/** @param FeedbackRecord[] $feedback *\/    public function format(array $feedback): Report;}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0422\u0430\u043a \u043b\u0435\u0433\u0430\u0441\u0438-\u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u0440\u0438\u0442\u0432\u043e\u0440\u044f\u0435\u0442\u0441\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439 FeedbackReportFormatter \u0441 \u0441\u0443\u0436\u0435\u043d\u043d\u044b\u043c <a class=\"mention\" href=\"\/users\/param\">@param<\/a> \u2014 \u0443 \u043d\u0435\u0433\u043e \u0441\u0432\u043e\u0439 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u0438 \u0441\u0432\u043e\u0439 \u043a\u043e\u043d\u0432\u0435\u0439\u0435\u0440.<\/p>\n<p>\u0427\u0435\u0441\u0442\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u043c\u0443\u0442\u0430\u0446\u0438\u0435\u0439 \u2014 \u043d\u0435 \u043c\u0443\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0445\u043e\u0434, \u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0441 \u043a\u043e\u043f\u0438\u0435\u0439:<\/p>\n<pre><code class=\"php\">$normalized = array_map(    fn(FeedbackRecord $r) =&gt; new FeedbackRecord(        id: $r-&gt;id,        rating: $r-&gt;rating,        text: trim(strip_tags($r-&gt;text)),        createdAt: $r-&gt;createdAt,        channel: $r-&gt;channel,    ),    $feedback);$body = (new XlsxWriter())-&gt;fromRecords($normalized);return new Report('xlsx', $body, 'feedback-weekly.xlsx');<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><strong>\u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0430 \u00ab\u043d\u0430 \u0431\u0443\u0434\u0443\u0449\u0435\u0435\u00bb:<\/strong><\/p>\n<p>\u0412 <a href=\"https:\/\/habr.com\/ru\/companies\/agima\/articles\/1065040\/\">OCP-\u0447\u0430\u0441\u0442\u0438<\/a> PdfFeedbackReportFormatter \u043f\u043e\u043a\u0430\u0437\u0430\u043d \u043a\u0430\u043a <strong>\u0433\u0438\u043f\u043e\u0442\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435<\/strong> \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u2014 \u00ab\u0435\u0441\u043b\u0438 \u0431\u0438\u0437\u043d\u0435\u0441 \u043f\u043e\u043f\u0440\u043e\u0441\u0438\u0442\u00bb. \u041d\u0430 \u0431\u0443\u043c\u0430\u0433\u0435 \u044d\u0442\u043e \u0437\u0434\u043e\u0440\u043e\u0432\u044b\u0439 OCP. LSP \u043b\u043e\u043c\u0430\u0435\u0442\u0441\u044f, \u043a\u043e\u0433\u0434\u0430 \u0442\u0430\u043a\u0443\u044e \u0437\u0430\u0433\u043b\u0443\u0448\u043a\u0443 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u044e\u0442 \u0432 DI \u0438 cron <strong>\u0434\u043e \u0433\u043e\u0442\u043e\u0432\u043d\u043e\u0441\u0442\u0438<\/strong>:<\/p>\n<pre><code class=\"php\">class PdfFeedbackReportFormatter implements FeedbackReportFormatter{    public function format(array $feedback): Report    {        return new Report('pdf', '', 'feedback-monthly.pdf'); \/\/ PDF \u0441\u0434\u0435\u043b\u0430\u0435\u043c \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u0441\u043f\u0440\u0438\u043d\u0442\u0435    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041a\u043b\u0430\u0441\u0441 \u0443\u0436\u0435 \u0432 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0435. Unit-\u0442\u0435\u0441\u0442\u044b \u0441 \u043c\u043e\u043a\u043e\u043c \u2014 \u0437\u0435\u043b\u0451\u043d\u044b\u0435. Cron \u0432 \u043f\u0440\u043e\u0434\u0435 \u2014 \u043f\u0443\u0441\u0442\u044b\u0435 \u043e\u0442\u0447\u0451\u0442\u044b. \u041f\u043e\u0434\u0442\u0438\u043f \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u0442\u0438\u043f\u043e\u0432\u043e\u0439 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u0444\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e \u043d\u0435 \u043d\u0430\u0440\u0443\u0448\u0435\u043d, \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0447\u0435\u0441\u043a\u0438\u0439 \u2014 \u0441\u043b\u043e\u043c\u0430\u043d.<\/p>\n<h3>\u0413\u0434\u0435 \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0433\u0440\u0430\u043d\u0438\u0446\u0430: \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u043f\u043e\u0434\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438<\/h3>\n<p>LSP \u0432 \u0430\u043a\u0430\u0434\u0435\u043c\u0438\u0447\u0435\u0441\u043a\u043e\u043c \u0432\u0438\u0434\u0435 \u0433\u043e\u0432\u043e\u0440\u0438\u0442 \u043f\u0440\u043e \u043f\u0440\u0435\u0434\u0443\u0441\u043b\u043e\u0432\u0438\u044f, \u043f\u043e\u0441\u0442\u0443\u0441\u043b\u043e\u0432\u0438\u044f \u0438 \u0438\u043d\u0432\u0430\u0440\u0438\u0430\u043d\u0442\u044b. \u0412 production \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0442\u0440\u0451\u0445 \u043f\u0440\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0432\u043e\u043f\u0440\u043e\u0441\u043e\u0432:<\/p>\n<p>\u041c\u043e\u0436\u043d\u043e \u043b\u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0432 FeedbackReportService <strong>\u0431\u0435\u0437 \u0432\u0435\u0442\u0432\u043b\u0435\u043d\u0438\u044f \u043f\u043e \u0442\u0438\u043f\u0443<\/strong>?<\/p>\n<p>\u0412\u0435\u0434\u0443\u0442 \u043b\u0438 \u0432\u0441\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u0435\u0431\u044f <strong>\u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u043e \u0441 \u0442\u043e\u0447\u043a\u0438 \u0437\u0440\u0435\u043d\u0438\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430<\/strong> \u2014 \u0447\u0442\u043e \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u044e\u0442, \u0447\u0442\u043e \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0440\u0443\u044e\u0442, \u043a\u043e\u0433\u0434\u0430 \u0431\u0440\u043e\u0441\u0430\u044e\u0442 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435?<\/p>\n<p>\u041c\u043e\u0436\u043d\u043e \u043b\u0438 <strong>\u043f\u043e\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0434\u0440\u0443\u0433\u0443\u044e \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0442\u043e\u0433\u043e \u0436\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430<\/strong> \u2014 \u0434\u0440\u0443\u0433\u043e\u0439 ReportSender \u0432\u043c\u0435\u0441\u0442\u043e email, \u0434\u0440\u0443\u0433\u043e\u0439 \u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440 \u0432\u043c\u0435\u0441\u0442\u043e XLSX \u2014 \u0431\u0435\u0437 \u043f\u0440\u0430\u0432\u043e\u043a FeedbackReportService?<\/p>\n<p>\u0422\u0440\u0435\u0442\u0438\u0439 \u0432\u043e\u043f\u0440\u043e\u0441 \u2014 \u043d\u0435 \u043f\u0440\u043e \u00ab\u043b\u044e\u0431\u0430\u044f \u043f\u0430\u0440\u0430 formatter \u00d7 sender \u0432 cron\u00bb. \u041a\u043e\u043c\u043f\u043e\u0437\u0438\u0446\u0438\u044f \u043d\u0430 \u0433\u0440\u0430\u043d\u0438\u0446\u0435 (cron, \u043a\u043e\u043d\u0444\u0438\u0433) \u043c\u043e\u0436\u0435\u0442 \u0438 \u0434\u043e\u043b\u0436\u043d\u0430 \u043e\u0442\u0441\u0435\u0438\u0432\u0430\u0442\u044c \u0431\u0438\u0437\u043d\u0435\u0441-\u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u044b\u0435 \u043f\u0430\u0440\u044b. LSP \u043f\u0440\u043e <strong>\u043f\u043e\u0434\u0442\u0438\u043f\u044b \u0432\u043d\u0443\u0442\u0440\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430<\/strong>, \u0430 \u043d\u0435 \u043f\u0440\u043e \u0442\u043e, \u0447\u0442\u043e daily CSV \u043e\u0431\u044f\u0437\u0430\u043d \u0443\u043b\u0435\u0442\u0430\u0442\u044c \u0432 Slack.<\/p>\n<p>\u0415\u0441\u043b\u0438 \u043d\u0430 \u0432\u043e\u043f\u0440\u043e\u0441\u044b 1\u20133 \u043e\u0442\u0432\u0435\u0442 \u00ab\u043d\u0435\u0442\u00bb \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043d\u0435 \u0432 \u00ab\u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u043c SOLID\u00bb, \u0430 \u0432 \u0434\u044b\u0440\u0435 \u0432 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0435.<\/p>\n<h4>\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u043e \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0443 \u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u043e \u0441\u0440\u0435\u0434\u0435 \u2014 \u043d\u0435 \u043e\u0434\u043d\u043e \u0438 \u0442\u043e \u0436\u0435<\/h4>\n<p>\u0420\u0430\u0437\u0443\u043c\u043d\u044b\u0439 \u0432\u043e\u043f\u0440\u043e\u0441: SlackReportSender \u0431\u0440\u043e\u0441\u0430\u0435\u0442 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043d\u0430 \u043d\u0435\u0442\u0435\u043a\u0441\u0442\u043e\u0432\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 \u2014 \u043f\u043e\u0447\u0435\u043c\u0443 \u044d\u0442\u043e \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0435 LSP, \u0430 EmailReportSender, \u043f\u0430\u0434\u0430\u044e\u0449\u0438\u0439 \u043f\u0440\u0438 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u043c SMTP, \u2014 \u043d\u0435\u0442? \u0412 \u043e\u0431\u043e\u0438\u0445 \u0441\u043b\u0443\u0447\u0430\u044f\u0445 throw.<\/p>\n<p>\u0420\u0430\u0437\u043d\u0438\u0446\u0430 \u2014 \u0432 \u0442\u043e\u043c, <strong>\u043e\u0442 \u0447\u0435\u0433\u043e \u0437\u0430\u0432\u0438\u0441\u0438\u0442<\/strong> \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435:<\/p>\n<p><strong>\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435, \u0437\u0430\u0432\u0438\u0441\u044f\u0449\u0435\u0435 \u043e\u0442 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0430<\/strong> (\u00ab\u044d\u0442\u043e\u0442 \u0444\u043e\u0440\u043c\u0430\u0442 \u044f \u043d\u0435 \u043f\u0440\u0438\u043c\u0443\u00bb) \u2014 \u0443\u0436\u0435\u0441\u0442\u043e\u0447\u0451\u043d\u043d\u043e\u0435 \u043f\u0440\u0435\u0434\u0443\u0441\u043b\u043e\u0432\u0438\u0435. \u041e\u0434\u0438\u043d \u0438 \u0442\u043e\u0442 \u0436\u0435 \u0432\u044b\u0437\u043e\u0432 \u0441 \u043e\u0434\u043d\u0438\u043c \u0438 \u0442\u0435\u043c \u0436\u0435 \u043e\u0442\u0447\u0451\u0442\u043e\u043c \u0432\u0435\u0434\u0451\u0442 \u0441\u0435\u0431\u044f \u043f\u043e-\u0440\u0430\u0437\u043d\u043e\u043c\u0443 \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0442\u043e\u0433\u043e, \u043a\u0430\u043a\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0430. \u042d\u0442\u043e \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0435.<\/p>\n<p><strong>\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435, \u0437\u0430\u0432\u0438\u0441\u044f\u0449\u0435\u0435 \u043e\u0442 \u0432\u043d\u0435\u0448\u043d\u0435\u0439 \u0441\u0440\u0435\u0434\u044b<\/strong> (\u00abSMTP \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d\u00bb, \u00abSlack API \u0432\u0435\u0440\u043d\u0443\u043b 500\u00bb) \u2014 \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0430, \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u0430\u044f \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0439: \u043b\u044e\u0431\u043e\u0439 sender \u043c\u043e\u0436\u0435\u0442 \u043d\u0435 \u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043e\u0442\u0447\u0451\u0442 \u043f\u043e \u0432\u0438\u043d\u0435 \u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b. \u042d\u0442\u043e \u043d\u043e\u0440\u043c\u0430, \u0438 \u043a\u043b\u0438\u0435\u043d\u0442 \u043e\u0431\u044f\u0437\u0430\u043d \u0431\u044b\u0442\u044c \u0433\u043e\u0442\u043e\u0432 \u043a \u0442\u0430\u043a\u043e\u043c\u0443 \u0438\u0441\u0445\u043e\u0434\u0443 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u043e\u0442 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438. \u041d\u043e \u00ab\u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0430\u00bb \u043e\u0437\u043d\u0430\u0447\u0430\u0435\u0442 \u0438 \u0441\u043e\u0433\u043b\u0430\u0441\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <strong>\u0442\u0438\u043f<\/strong> \u043e\u0448\u0438\u0431\u043a\u0438: \u0435\u0441\u043b\u0438 \u043e\u0434\u0438\u043d sender \u0431\u0440\u043e\u0441\u0430\u0435\u0442 TransportException, \u0434\u0440\u0443\u0433\u043e\u0439 \u2014 InvalidArgumentException, \u0430 \u043a\u043b\u0438\u0435\u043d\u0442 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u0442 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0435\u0434\u0438\u043d\u044b\u0439 \u0442\u0438\u043f \u2014 \u043f\u043e\u0434\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0443\u0436\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043d\u0430. \u041e\u0448\u0438\u0431\u043a\u0438 \u0441\u0440\u0435\u0434\u044b \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b, \u0435\u0441\u043b\u0438 \u0438\u0445 \u0442\u0438\u043f, \u043c\u043e\u043c\u0435\u043d\u0442 \u0432\u043e\u0437\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u044f \u0438 \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u043a\u0430 \u0441\u043e\u0433\u043b\u0430\u0441\u043e\u0432\u0430\u043d\u044b \u043e\u0431\u0449\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u043e\u043c \u2014 \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, <a class=\"mention\" href=\"\/users\/throws\">@throws<\/a> ReportDeliveryException \u043d\u0430 ReportSender::send().<\/p>\n<p>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043f\u0440\u043e\u0441\u0442\u0430\u044f: \u0435\u0441\u043b\u0438 \u0434\u0432\u0430 \u0432\u044b\u0437\u043e\u0432\u0430 \u0441 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u044b\u043c Report, \u043d\u043e \u0440\u0430\u0437\u043d\u044b\u043c\u0438 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430\u043c\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u0434\u0430\u044e\u0442 \u0440\u0430\u0437\u043d\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 <strong>\u0438\u0437-\u0437\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e<\/strong>** **<strong>Report<\/strong> \u2014 \u044d\u0442\u043e LSP. \u0415\u0441\u043b\u0438 \u0440\u0430\u0437\u043d\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u2014 \u044d\u0442\u043e \u044d\u043a\u0441\u043f\u043b\u0443\u0430\u0442\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0440\u0438\u0441\u043a, \u043d\u0435 \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0430 (\u043f\u0440\u0438 \u0443\u0441\u043b\u043e\u0432\u0438\u0438, \u0447\u0442\u043e \u0442\u0438\u043f \u043e\u0448\u0438\u0431\u043a\u0438 \u0442\u043e\u0436\u0435 \u0441\u043e\u0433\u043b\u0430\u0441\u043e\u0432\u0430\u043d).<\/p>\n<h4>\u0427\u0435\u043a\u043b\u0438\u0441\u0442 \u043f\u0435\u0440\u0435\u0434 merge<\/h4>\n<p>\u041f\u0435\u0440\u0435\u0434 \u0442\u0435\u043c \u043a\u0430\u043a \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430, \u0437\u0430\u0434\u0430\u0439\u0442\u0435 \u043f\u044f\u0442\u044c \u0432\u043e\u043f\u0440\u043e\u0441\u043e\u0432:<\/p>\n<p><strong>\u0412\u044b\u0437\u044b\u0432\u0430\u044e\u0449\u0438\u0439 \u043a\u043e\u0434 \u0437\u043d\u0430\u0435\u0442 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043f\u043e\u0434\u0442\u0438\u043f?<\/strong> \u0415\u0441\u043b\u0438 \u0434\u0430 \u2014 LSP \u0443\u0436\u0435 \u043f\u043e\u0434 \u0432\u043e\u043f\u0440\u043e\u0441\u043e\u043c. \u0418\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u0438\u0432\u043d\u044b\u0439.<\/p>\n<p><strong>\u041f\u043e\u0434\u0442\u0438\u043f \u0443\u0436\u0435\u0441\u0442\u043e\u0447\u0430\u0435\u0442 \u0432\u0445\u043e\u0434?<\/strong> \u00ab\u041f\u0440\u0438\u043d\u0438\u043c\u0430\u044e \u0442\u043e\u043b\u044c\u043a\u043e \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u044b\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u044b\u00bb, \u00ab\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f admin\u00bb \u2014 \u044d\u0442\u043e \u0434\u0440\u0443\u0433\u043e\u0439 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442. \u041d\u0443\u0436\u0435\u043d \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0438\u043b\u0438 \u044f\u0432\u043d\u044b\u0439 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440, \u043d\u0435 \u0442\u0438\u0445\u0438\u0439 throw \u0432\u043d\u0443\u0442\u0440\u0438.<\/p>\n<p><strong>\u041f\u043e\u0434\u0442\u0438\u043f \u043e\u0441\u043b\u0430\u0431\u043b\u044f\u0435\u0442 \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0438?<\/strong> \u041d\u0435\u043f\u043e\u043b\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435, \u043c\u043e\u043b\u0447\u0430\u043b\u0438\u0432\u0430\u044f \u043e\u0431\u0440\u0435\u0437\u043a\u0430, no-op \u0432\u043c\u0435\u0441\u0442\u043e \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u2014 composition \u0438\u043b\u0438 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0442\u0438\u043f, \u043d\u0435 extends.<\/p>\n<p><strong>\u0422\u0435\u0441\u0442\u044b \u0441 \u043c\u043e\u043a\u043e\u043c \u0437\u0435\u043b\u0451\u043d\u044b\u0435, \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441 \u0434\u0440\u0443\u0433\u043e\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u2014 \u043d\u0435\u0442?<\/strong> \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u043d\u0430 \u0432\u0441\u0435\u0445 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f\u0445, \u043d\u0435 \u043d\u0430 \u043e\u0434\u043d\u043e\u0439.<\/p>\n<p><strong>\u0412 \u0431\u0438\u0437\u043d\u0435\u0441-\u043a\u043e\u0434\u0435 \u043f\u043e\u044f\u0432\u0438\u043b\u0441\u044f<\/strong>** <strong>instanceof<\/strong>?** \u041f\u0435\u0440\u0435\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0438\u0435\u0440\u0430\u0440\u0445\u0438\u044e. \u0421\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e, \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0448\u0438\u0440\u043e\u043a\u0438\u0439 \u0438\u043b\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u0435\u0447\u0435\u0441\u0442\u043d\u0430\u044f.<\/p>\n<p>\u042d\u0442\u043e \u043d\u0435 \u043c\u0430\u0442\u0435\u043c\u0430\u0442\u0438\u043a\u0430. \u042d\u0442\u043e \u0444\u0438\u043b\u044c\u0442\u0440 \u043c\u0435\u0436\u0434\u0443 \u00ab\u0443 \u043d\u0430\u0441 \u043f\u043e\u043b\u0438\u043c\u043e\u0440\u0444\u0438\u0437\u043c\u00bb \u0438 \u00ab\u0443 \u043d\u0430\u0441 \u043f\u043e\u043b\u0438\u043c\u043e\u0440\u0444\u0438\u0437\u043c, \u043d\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u0437\u043d\u0430\u0442\u044c, \u043a\u0430\u043a\u043e\u0439 \u043a\u043b\u0430\u0441\u0441 \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044c\u00bb.<\/p>\n<h3>\u0420\u0430\u0437\u0443\u043c\u043d\u043e\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u0435: \u0447\u0435\u0441\u0442\u043d\u0430\u044f \u043f\u043e\u0434\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430<\/h3>\n<h4>Sender: \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043f\u043e \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0443 \u043e\u0442\u0447\u0451\u0442\u0430, \u043d\u0435 \u043f\u043e \u0441\u043f\u0438\u0441\u043a\u0443 \u0444\u043e\u0440\u043c\u0430\u0442\u043e\u0432<\/h4>\n<p>SlackReportSender \u0434\u043e\u043b\u0436\u0435\u043d \u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c <strong>\u0432\u0435\u0441\u044c<\/strong> \u043e\u0442\u0447\u0451\u0442 \u2014 \u043a\u0430\u043a EmailReportSender. \u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u044b \u0440\u0435\u0448\u0430\u044e\u0442\u0441\u044f \u0432\u043d\u0443\u0442\u0440\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438, \u0430 \u043d\u0435 \u0437\u0430 \u0441\u0447\u0451\u0442 \u043c\u043e\u043b\u0447\u0430\u043b\u0438\u0432\u043e\u0439 \u043f\u043e\u0442\u0435\u0440\u0438 \u0434\u0430\u043d\u043d\u044b\u0445:<\/p>\n<pre><code class=\"php\">class SlackReportSender implements ReportSender{    private const MAX_MESSAGE_LENGTH = 4000;    public function __construct(        private SlackClient $slack,        private string $channelId,    ) {}    public function send(Report $report): void    {        if (!$report-&gt;isTextual() || mb_strlen($report-&gt;body()) &gt; self::MAX_MESSAGE_LENGTH) {            $attachment = $report-&gt;asAttachment();            $this-&gt;slack-&gt;uploadFile(                channel: $this-&gt;channelId,                filename: $attachment-&gt;filename,                content: $attachment-&gt;content,                mimeType: $attachment-&gt;mimeType,                comment: '\u041e\u0442\u0447\u0451\u0442 \u043f\u043e \u043e\u0442\u0437\u044b\u0432\u0430\u043c',            );            return;        }        $this-&gt;slack-&gt;postMessage($this-&gt;channelId, $report-&gt;body());    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041a\u043b\u0438\u0435\u043d\u0442 \u043d\u0435 \u0437\u043d\u0430\u0435\u0442 \u043f\u0440\u043e \u043b\u0438\u043c\u0438\u0442\u044b Slack \u0438 \u043f\u0440\u043e \u0442\u043e, \u043a\u0430\u043a\u0438\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u0435. \u041a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 ReportSender \u0441\u043e\u0445\u0440\u0430\u043d\u0451\u043d: send() \u043b\u0438\u0431\u043e \u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 <strong>\u043f\u043e\u043b\u043d\u044b\u0439<\/strong> \u043e\u0442\u0447\u0451\u0442 \u2014 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435\u043c \u0438\u043b\u0438 \u0444\u0430\u0439\u043b\u043e\u043c, \u2014 \u043b\u0438\u0431\u043e \u0431\u0440\u043e\u0441\u0430\u0435\u0442 ReportDeliveryException \u043f\u043e \u0432\u0438\u043d\u0435 \u0441\u0440\u0435\u0434\u044b, \u043a\u0430\u043a email \u043f\u0440\u0438 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u043c SMTP. \u00ab\u041f\u043e\u043b\u043d\u044b\u0439\u00bb \u0437\u0434\u0435\u0441\u044c \u043e\u0437\u043d\u0430\u0447\u0430\u0435\u0442 \u043d\u0430\u0431\u043b\u044e\u0434\u0430\u0435\u043c\u0443\u044e \u0434\u043b\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0446\u0435\u043b\u043e\u0441\u0442\u043d\u043e\u0441\u0442\u044c: \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435, \u0438\u043c\u044f \u0444\u0430\u0439\u043b\u0430 \u0438 MIME type \u2014 \u0430 \u043d\u0435 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u044b\u0439 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438. \u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0432\u043c\u0435\u0441\u0442\u043e \u0432\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e, \u0435\u0441\u043b\u0438 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 \u043d\u0435 \u043f\u043e\u0442\u0435\u0440\u044f\u043d\u043e; \u043a\u0430\u043d\u0430\u043b, \u0447\u0430\u0441\u0442\u0438\u0447\u043d\u044b\u0439 \u0443\u0441\u043f\u0435\u0445 \u0438 \u0440\u0435\u0442\u0440\u0430\u0438 \u2014 \u0443\u0436\u0435 \u044d\u043a\u0441\u043f\u043b\u0443\u0430\u0442\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0435 \u0434\u0435\u0442\u0430\u043b\u0438 \u0437\u0430 \u0441\u043a\u043e\u0443\u043f\u043e\u043c \u044d\u0442\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0430. \u041d\u0438\u043a\u0430\u043a\u043e\u0439 \u0432\u0435\u0442\u043a\u0438 \u00ab\u0440\u0435\u0436\u0435\u043c \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u0447\u0430\u0441\u0442\u044c\u00bb. \u0420\u0435\u0448\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442\u0441\u044f \u043f\u043e Report::isTextual() \u0438 \u0434\u043b\u0438\u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u2014 \u0430 \u043d\u0435 \u043f\u043e \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0445 \u0444\u043e\u0440\u043c\u0430\u0442\u043e\u0432, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u043e\u0432\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 (PDF, \u0435\u0441\u043b\u0438 \u0431\u0438\u0437\u043d\u0435\u0441 \u0435\u0433\u043e \u043f\u043e\u043f\u0440\u043e\u0441\u0438\u0442) \u043d\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043f\u0440\u0430\u0432\u043e\u043a sender\u2019\u0430: \u044d\u0442\u043e \u0442\u043e\u0442 \u0436\u0435 OCP, \u0437\u0430 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u044b \u0443\u0436\u0435 \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 \u0432 \u043f\u0440\u043e\u0448\u043b\u043e\u0439 \u0447\u0430\u0441\u0442\u0438.<\/p>\n<h4>Repository: composition \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u044f<\/h4>\n<p>Read-only \u0434\u043e\u0441\u0442\u0443\u043f \u2014 \u043d\u0435 \u043f\u043e\u0434\u0442\u0438\u043f writable-\u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f. \u041e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442:<\/p>\n<pre><code class=\"php\">interface FeedbackReader{    \/**     * @return Feedback[] \u041e\u0442\u0437\u044b\u0432\u044b \u0437\u0430 \u043f\u0435\u0440\u0438\u043e\u0434.     *                     \u041a\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 \u0438\u043d\u044b\u0435 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u044b \u043f\u043e\u0432\u0435\u0440\u0445 reader'\u0430     *                     \u043d\u0435 \u043c\u0435\u043d\u044f\u044e\u0442 \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u043a\u0443 \u0432\u044b\u0431\u043e\u0440\u043a\u0438 \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u0441\u043f\u043e\u0441\u043e\u0431 \u0435\u0451     *                     \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c. \u0424\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044f \u2014 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439     *                     \u0442\u0438\u043f (\u0441\u043c. HighRatedFeedbackReader \u043d\u0438\u0436\u0435), \u0430 \u043d\u0435     *                     \u00ab\u043b\u044e\u0431\u043e\u0439 \u0441\u043e\u0441\u0442\u0430\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0440\u0435\u0448\u0438\u0442 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f\u00bb.     *\/    public function getForPeriod(DateRange $period): array;}interface FeedbackWriter{    public function save(Feedback $feedback): void;}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>FeedbackRepository \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u0442 \u043e\u0431\u0430 \u2014 \u0434\u043b\u044f \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0435\u0432 \u0437\u0430\u043f\u0438\u0441\u0438. \u042d\u043a\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u0432\u043e\u0440\u043a\u0435\u0440 \u0438 \u043a\u043e\u043d\u0441\u043e\u043b\u044c\u043d\u0430\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u0431\u044d\u043a\u0444\u0438\u043b\u043b\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u044e\u0442 \u0438\u043c\u0435\u043d\u043d\u043e \u0442\u043e\u0442 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0438\u043c \u043d\u0443\u0436\u0435\u043d: \u0432\u043e\u0440\u043a\u0435\u0440 \u2014 FeedbackReader, \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u0431\u044d\u043a\u0444\u0438\u043b\u043b\u0430 \u2014 \u043e\u0431\u0430. \u041f\u0435\u0440\u0435\u043f\u0443\u0442\u0430\u0442\u044c \u0438\u0445 \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 DI-\u0442\u0438\u043f\u043e\u0432 \u0442\u0435\u043f\u0435\u0440\u044c \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0444\u0438\u0437\u0438\u0447\u0435\u0441\u043a\u0438, \u0430 \u043d\u0435 \u043f\u043e \u0441\u043e\u0433\u043b\u0430\u0448\u0435\u043d\u0438\u044e. \u041e\u0442\u0447\u0451\u0442\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0438\u0441 \u0441\u0443\u0436\u0430\u0435\u043c:<\/p>\n<pre><code class=\"php\">class FeedbackReportService{    public function __construct(        private FeedbackReader $repository    ) {}    public function send(        DateRange $period,        FeedbackReportFormatter $formatter,        ReportSender $sender    ): void {        $feedback = $this-&gt;repository-&gt;getForPeriod($period);        $report = $formatter-&gt;format($feedback);        $sender-&gt;send($report);    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041d\u0438\u043a\u0430\u043a\u043e\u0433\u043e throw \u0432 \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u043e\u043c save(). \u041d\u0435\u0447\u0435\u0441\u0442\u043d\u043e\u0433\u043e \u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u044f \u0440\u0430\u0434\u0438 \u044d\u043a\u043e\u043d\u043e\u043c\u0438\u0438 \u0441\u0442\u0440\u043e\u043a\u0438 implements. \u041b\u0438\u0448\u043d\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0438 wiring \u0432 DI \u2014 \u0446\u0435\u043d\u0430 \u0437\u0430 \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u044e \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 \u0442\u0438\u043f\u043e\u0432 \u0432\u043c\u0435\u0441\u0442\u043e runtime LogicException. \u0417\u0434\u0435\u0441\u044c \u2014 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u043b\u0438\u0442 \u0440\u0430\u0434\u0438 \u0447\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u043e\u0434\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438; \u043f\u043e\u043b\u043d\u043e\u0446\u0435\u043d\u043d\u044b\u0439 \u0440\u0430\u0437\u0431\u043e\u0440 \u0441\u0435\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438 \u2014 \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 (\u0441\u043c. trade-off \u043d\u0438\u0436\u0435).<\/p>\n<p>\u041a\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0442\u0435\u043f\u0435\u0440\u044c \u0442\u043e\u0436\u0435 \u0440\u0435\u0448\u0430\u0435\u0442\u0441\u044f composition, \u0430 \u043d\u0435 \u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u043a\u043b\u0430\u0441\u0441\u0430:<\/p>\n<pre><code class=\"php\">final class CachingFeedbackReader implements FeedbackReader{    \/** @var array&lt;string, Feedback[]&gt; *\/    private array $cache = [];    public function __construct(private FeedbackReader $inner) {}    public function getForPeriod(DateRange $period): array    {        $key = $period-&gt;from-&gt;format(DATE_ATOM) . ':' . $period-&gt;to-&gt;format(DATE_ATOM);        return $this-&gt;cache[$key] ??= $this-&gt;inner-&gt;getForPeriod($period);    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0420\u0430\u0437 \u0437\u0430\u043f\u0438\u0441\u044c \u0442\u0435\u043f\u0435\u0440\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 (FeedbackWriter), \u0432 \u043e\u0442\u0447\u0451\u0442\u043d\u043e\u043c \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0438 \u043a\u0435\u0448\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u0435\u043d\u0438\u0435 \u2014 \u043f\u0443\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u0438 \u0447\u0435\u0440\u0435\u0437 \u044d\u0442\u043e\u0442 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 \u0432\u043e\u043e\u0431\u0449\u0435 \u043d\u0435 \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442, \u0438 \u043a\u043b\u0430\u0441\u0441\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0431\u0430\u0433 \u00absave() \u043d\u0435 \u0441\u0431\u0440\u043e\u0441\u0438\u043b \u043a\u0435\u0448\u00bb \u0437\u0434\u0435\u0441\u044c \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u043d\u043e \u043d\u0435 \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u0435\u0442.<\/p>\n<p>\u041e\u0433\u043e\u0432\u043e\u0440\u043a\u0430 \u0447\u0435\u0441\u0442\u043d\u043e\u0441\u0442\u0438 \u0437\u0434\u0435\u0441\u044c \u0436\u0451\u0441\u0442\u0447\u0435, \u0447\u0435\u043c \u00ab\u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u0440\u0430\u043c\u043a\u0430\u0445 \u043e\u0434\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430\u00bb. \u0414\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 \u0437\u0430\u0449\u0438\u0449\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0442 staleness, \u0432\u044b\u0437\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u044c\u044e <strong>\u0447\u0435\u0440\u0435\u0437 \u0441\u0435\u0431\u044f \u0436\u0435<\/strong>. \u0414\u0430\u0436\u0435 \u0432\u043d\u0443\u0442\u0440\u0438 \u043e\u0434\u043d\u043e\u0433\u043e long-running \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 (\u0432\u043e\u0440\u043a\u0435\u0440, \u0434\u0435\u043c\u043e\u043d) \u043a\u0435\u0448 \u0443\u0441\u0442\u0430\u0440\u0435\u0435\u0442, \u0435\u0441\u043b\u0438 \u0437\u0430\u043f\u0438\u0441\u044c \u043f\u0440\u043e\u0439\u0434\u0451\u0442 \u043c\u0438\u043c\u043e \u044d\u0442\u043e\u0433\u043e \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 \u2014 \u0447\u0435\u0440\u0435\u0437 \u0434\u0440\u0443\u0433\u043e\u0439 FeedbackWriter, \u0441\u043e\u0441\u0435\u0434\u043d\u0438\u0439 \u0441\u0435\u0440\u0432\u0438\u0441 \u0438\u043b\u0438 \u043f\u0440\u044f\u043c\u043e\u0439 SQL-\u0437\u0430\u043f\u0440\u043e\u0441, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0435 \u0432 \u043a\u0443\u0440\u0441\u0435, \u0447\u0442\u043e \u0440\u044f\u0434\u043e\u043c \u043a\u0442\u043e-\u0442\u043e \u043a\u0435\u0448\u0438\u0440\u0443\u0435\u0442. \u0415\u0441\u043b\u0438 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0436\u0438\u0432\u0451\u0442 \u0434\u043e\u043b\u044c\u0448\u0435 \u043e\u0434\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0438 \u0434\u0435\u0440\u0436\u0438\u0442 \u0442\u0430\u043a\u043e\u0439 \u043a\u0435\u0448, TTL \u0438\u043b\u0438 \u044f\u0432\u043d\u0430\u044f \u0438\u043d\u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f \u043f\u043e \u0441\u043e\u0431\u044b\u0442\u0438\u044e save() \u043d\u0443\u0436\u043d\u044b \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u043e\u0442 \u0442\u043e\u0433\u043e, \u043e\u0434\u0438\u043d \u0432\u043e\u0440\u043a\u0435\u0440 \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e. \u041f\u0440\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430\u0445 \u044d\u0442\u043e \u0432\u0435\u0440\u043d\u043e \u0435\u0449\u0451 \u0441\u0438\u043b\u044c\u043d\u0435\u0435: \u043a\u0435\u0448 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0436\u0438\u0432\u0451\u0442 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e, \u0438 \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u0430\u044f \u0438\u043d\u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f \u2014 \u0443\u0436\u0435 \u0432\u043e\u043f\u0440\u043e\u0441 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u044b \u043a\u0435\u0448\u0430, \u043d\u0435 LSP.<\/p>\n<h4>Formatter: \u043d\u0435\u0438\u0437\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u0439 \u0432\u0445\u043e\u0434, \u0432\u0441\u044f \u0432\u044b\u0431\u043e\u0440\u043a\u0430, \u0447\u0435\u0441\u0442\u043d\u0430\u044f \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u043a\u0430<\/h4>\n<p>\u041a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 FeedbackReportFormatter \u0444\u0438\u043a\u0441\u0438\u0440\u0443\u0435\u043c \u044f\u0432\u043d\u043e \u2014 \u0432 PHPDoc \u0438 \u0432 \u0442\u0435\u0441\u0442\u0430\u0445:<\/p>\n<p>format() <strong>\u043d\u0435 \u043c\u0443\u0442\u0438\u0440\u0443\u0435\u0442<\/strong> \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0432\u043e \u0432\u0445\u043e\u0434\u043d\u043e\u043c \u043c\u0430\u0441\u0441\u0438\u0432\u0435 (\u0434\u043b\u044f readonly Feedback \u044d\u0442\u043e \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u044f \u0442\u0438\u043f\u0430; \u0434\u043b\u044f \u043b\u0435\u0433\u0430\u0441\u0438 \u2014 \u044f\u0432\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442);<\/p>\n<p>format() \u0441\u0442\u0440\u043e\u0438\u0442 \u043e\u0442\u0447\u0451\u0442 \u0438\u0437 <strong>\u0432\u0441\u0435\u0445<\/strong> \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0445 \u043e\u0442\u0437\u044b\u0432\u043e\u0432 \u2014 \u0431\u0435\u0437 \u0442\u0438\u0445\u043e\u0439 \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438, \u0431\u0435\u0437 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439;<\/p>\n<p>\u0441\u0438\u0433\u043d\u0430\u0442\u0443\u0440\u0430 format(array $feedback) \u043d\u0435 \u0441\u0443\u0436\u0430\u0435\u0442\u0441\u044f \u0432 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u2014 \u043b\u0435\u0433\u0430\u0441\u0438-\u043a\u043e\u043d\u0432\u0435\u0439\u0435\u0440 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u0430 \u043d\u0435 \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u0439 \u0442\u0438\u043f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430.<\/p>\n<p>\u0424\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044f \u043f\u043e \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0443 \u2014 \u043d\u0435 \u0437\u0430\u0434\u0430\u0447\u0430 \u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440\u0430. \u041e\u043d\u0430 \u043b\u0438\u0431\u043e \u0432 \u0441\u0430\u043c\u043e\u043c \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043a \u0411\u0414, \u043b\u0438\u0431\u043e \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u043c \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u0435 \u043d\u0430\u0434 FeedbackReader, \u0442\u0435\u043c \u0436\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c, \u043a\u0430\u043a\u0438\u043c \u043c\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u043e \u0437\u0430\u043a\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043b\u0438 \u0447\u0442\u0435\u043d\u0438\u0435:<\/p>\n<pre><code class=\"php\">final class HighRatedFeedbackReader implements FeedbackReader{    public function __construct(private FeedbackReader $inner) {}    public function getForPeriod(DateRange $period): array    {        return array_values(array_filter(            $this-&gt;inner-&gt;getForPeriod($period),            fn(Feedback $f) =&gt; $f-&gt;rating &gt;= 4        ));    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u0441\u0442\u043e\u0438\u0442 \u0431\u044b\u0442\u044c \u0447\u0435\u0441\u0442\u043d\u044b\u043c: \u043f\u0435\u0440\u0435\u043d\u043e\u0441 \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438 \u0441 \u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440\u0430 \u043d\u0430 reader \u043d\u0435 \u043e\u0442\u043c\u0435\u043d\u044f\u0435\u0442 \u0432\u043e\u043f\u0440\u043e\u0441 \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u043a\u0438, \u0430 \u043b\u0438\u0448\u044c \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0438\u0442 \u0435\u0433\u043e \u0432 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u043c\u0435\u0441\u0442\u043e. \u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 FeedbackReader::getForPeriod() \u043e\u0431\u0435\u0449\u0430\u0435\u0442 <strong>\u043e\u0442\u0437\u044b\u0432\u044b \u0437\u0430 \u043f\u0435\u0440\u0438\u043e\u0434<\/strong> \u2014 \u043d\u0435 \u00ab\u043a\u0430\u043a\u043e\u0439-\u0442\u043e \u043d\u0430\u0431\u043e\u0440 \u043d\u0430 \u0443\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u0438\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438\u00bb. HighRatedFeedbackReader \u0444\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u0442 \u0442\u043e\u0442 \u0436\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u043d\u043e \u044d\u0442\u043e <strong>\u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0442\u0438\u043f-\u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440<\/strong>: \u043e\u043d \u0441\u0443\u0436\u0430\u0435\u0442 \u0432\u044b\u0431\u043e\u0440\u043a\u0443 \u043f\u043e \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0443 \u0438 \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0447\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u043e\u0434\u043c\u0435\u043d\u043e\u0439 \u00ab\u043f\u043e\u043b\u043d\u043e\u0433\u043e\u00bb reader\u2019\u0430 \u0432\u043d\u0443\u0442\u0440\u0438 \u043a\u043e\u0434\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0436\u0434\u0451\u0442 \u0432\u0441\u044e \u0432\u044b\u0431\u043e\u0440\u043a\u0443. LSP \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u043d\u0430\u0440\u0443\u0448\u0435\u043d \u0438\u043c\u0435\u043d\u043d\u043e \u043f\u043e\u0442\u043e\u043c\u0443, \u0447\u0442\u043e \u043f\u043e\u0434\u043c\u0435\u043d\u0430 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 <strong>\u043d\u0435 \u0442\u0430\u043c<\/strong>, \u0433\u0434\u0435 \u043a\u043b\u0438\u0435\u043d\u0442 \u0443\u0436\u0435 \u043d\u0430\u043f\u0438\u0441\u0430\u043d \u043f\u043e\u0434 \u043f\u043e\u043b\u043d\u0443\u044e \u0432\u044b\u0431\u043e\u0440\u043a\u0443, \u2014 \u0430 \u0432 composition root, \u0433\u0434\u0435 cron \u044f\u0432\u043d\u043e \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442 <strong>\u0434\u0440\u0443\u0433\u0443\u044e<\/strong> \u0441\u0431\u043e\u0440\u043a\u0443 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439 \u043f\u043e\u0434 \u0437\u0430\u0434\u0430\u0447\u0443 \u00ab\u0442\u043e\u043b\u044c\u043a\u043e \u0432\u044b\u0441\u043e\u043a\u0438\u0435 \u043e\u0446\u0435\u043d\u043a\u0438\u00bb. \u0415\u0441\u043b\u0438 \u0431\u044b FeedbackReportService \u0441\u0430\u043c \u0440\u0435\u0448\u0430\u043b, \u043a\u0430\u043a\u0438\u043c reader\u2019\u043e\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0443\u0441\u043b\u043e\u0432\u0438\u0439 \u2014 \u044d\u0442\u043e \u0443\u0436\u0435 \u0431\u044b\u043b \u0431\u044b instanceof-\u0437\u0430\u043f\u0430\u0445 \u0438\u0437 \u0447\u0435\u043a\u043b\u0438\u0441\u0442\u0430 \u0432\u044b\u0448\u0435, \u0438 \u043f\u043e\u0434\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 HighRatedFeedbackReader \u0432\u043c\u0435\u0441\u0442\u043e \u043f\u043e\u043b\u043d\u043e\u0433\u043e reader\u2019\u0430 \u0432\u043d\u0443\u0442\u0440\u0438 \u0443\u0436\u0435 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u043b\u043e\u043c\u0430\u043b\u0430 \u0431\u044b \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u0435 \u00ab\u0432\u0441\u0435 \u043e\u0442\u0437\u044b\u0432\u044b \u0437\u0430 \u043f\u0435\u0440\u0438\u043e\u0434\u00bb. \u0410 \u043f\u043e\u043a\u0430 \u0432\u044b\u0431\u043e\u0440 reader\u2019\u0430 \u2014 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f job\u2019\u0430, \u0430 \u043d\u0435 runtime-\u0432\u0435\u0442\u0432\u043b\u0435\u043d\u0438\u0435, \u0440\u0430\u0437\u043d\u044b\u0435 job\u2019\u044b \u043f\u043e\u043b\u0443\u0447\u0430\u044e\u0442 \u0440\u0430\u0437\u043d\u044b\u0435 \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u0441\u0431\u043e\u0440\u043a\u0438 \u043e\u0441\u043e\u0437\u043d\u0430\u043d\u043d\u043e: \u043f\u043e\u043b\u043d\u044b\u0439 reader \u2014 \u0434\u043b\u044f \u043f\u043e\u043b\u043d\u043e\u0439 \u0432\u044b\u0431\u043e\u0440\u043a\u0438, HighRatedFeedbackReader \u2014 \u0434\u043b\u044f \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0439. \u041e\u0441\u043b\u0430\u0431\u043b\u044f\u0442\u044c \u0431\u0430\u0437\u043e\u0432\u044b\u0439 PHPDoc \u0434\u043e \u00ab\u0441\u043e\u0441\u0442\u0430\u0432 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442\u0441\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439\u00bb \u0440\u0430\u0434\u0438 \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u0444\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e \u00ab\u043d\u0435 \u043d\u0430\u0440\u0443\u0448\u0438\u0442\u044c\u00bb LSP, \u043d\u0435 \u0441\u0442\u043e\u0438\u0442: \u0442\u0430\u043a\u043e\u0439 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u043f\u043e\u0447\u0442\u0438 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0440\u0443\u0448\u0438\u0442\u044c, \u043d\u043e \u043e\u043d \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0441\u043b\u0430\u0431 \u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441-\u0441\u0435\u0440\u0432\u0438\u0441\u0430.<\/p>\n<p>Cron, \u043a\u043e\u0442\u043e\u0440\u043e\u043c\u0443 \u043d\u0443\u0436\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u044b\u0441\u043e\u043a\u0438\u0435 \u043e\u0446\u0435\u043d\u043a\u0438, \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442 \u0441\u0435\u0440\u0432\u0438\u0441 \u0441 \u043d\u0443\u0436\u043d\u044b\u043c reader\u2019\u043e\u043c \u2014 \u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440 \u043e\u0431 \u044d\u0442\u043e\u043c \u043d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u0437\u043d\u0430\u0435\u0442 \u0438 \u043d\u0435 \u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f:<\/p>\n<pre><code class=\"php\">$reportService = new FeedbackReportService(    new HighRatedFeedbackReader($repository));$reportService-&gt;send(    DateRange::lastDay(),    new CsvFeedbackReportFormatter(),    new SlackReportSender($slack, $channelId));<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0422\u0430\u043a \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044f \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u043e\u0441\u044c\u044e \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u2014 \u043f\u0440\u0435\u0434\u0441\u043a\u0430\u0437\u0443\u0435\u043c\u043e \u0434\u043b\u044f OCP, \u0438 \u043d\u0435 \u043b\u043e\u043c\u0430\u0435\u0442 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 FeedbackReportFormatter \u0434\u043b\u044f LSP. CsvFeedbackReportFormatter \u0438\u0437 \u0430\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d\u0430 \u0432\u044b\u0448\u0435 \u043f\u0440\u043e\u0441\u0442\u043e \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u0451\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c \u0432 \u0442\u0430\u043a\u043e\u0439 \u0444\u043e\u0440\u043c\u0435: \u043b\u044e\u0431\u043e\u0439 \u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440 \u0447\u0435\u0441\u0442\u043d\u043e \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0432\u0441\u0451, \u0447\u0442\u043e \u0435\u043c\u0443 \u043f\u0435\u0440\u0435\u0434\u0430\u043b\u0438.<\/p>\n<h4>\u041a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u043d\u044b\u0435 \u0442\u0435\u0441\u0442\u044b \u2014 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0446\u0435\u043d\u0430 OCP<\/h4>\n<p>\u0414\u043b\u044f ReportSender \u0434\u0430\u0442\u0430-\u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u043f\u043b\u043e\u0445\u043e \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442: \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u0447\u0442\u043e sender <strong>\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u043b<\/strong> \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435, \u0442\u0435\u0441\u0442\u0443 \u043d\u0443\u0436\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044e fake-\u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u0430 \u043f\u043e\u0441\u043b\u0435 send() \u2014 \u0430 PHPUnit 10+ \u0442\u0440\u0435\u0431\u0443\u0435\u0442, \u0447\u0442\u043e\u0431\u044b \u0434\u0430\u0442\u0430-\u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u044b \u0431\u044b\u043b\u0438 static \u0438 \u043d\u0435 \u0438\u043c\u0435\u043b\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a $this. \u041f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442 \u0437\u0434\u0435\u0441\u044c \u2014 \u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0442\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u043d\u044b\u0439 \u0442\u0435\u0441\u0442-\u043a\u0435\u0439\u0441, \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u044e\u0449\u0438\u0439 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0438 \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0439. \u0412\u0430\u0436\u043d\u043e, \u0447\u0442\u043e\u0431\u044b \u0432 \u043d\u0430\u0431\u043e\u0440\u0435 \u0431\u044b\u043b \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u0438\u043d\u043d\u044b\u0439 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u044b\u0439 \u043e\u0442\u0447\u0451\u0442, \u043d\u043e \u0438 <strong>\u0431\u0438\u043d\u0430\u0440\u043d\u044b\u0439<\/strong> \u2014 \u0438\u043c\u0435\u043d\u043d\u043e \u043e\u043d \u043b\u043e\u0432\u0438\u0442 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u0438\u0437 \u0438\u043d\u0442\u0440\u043e (XLSX, \u043e\u0442\u043a\u043b\u043e\u043d\u0451\u043d\u043d\u044b\u0439 \u0438\u043b\u0438 \u043e\u0431\u0440\u0435\u0437\u0430\u043d\u043d\u044b\u0439 Slack-\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439):<\/p>\n<pre><code class=\"php\">abstract class ReportSenderContractTest extends TestCase{    abstract protected function createSender(): ReportSender;    abstract protected function assertDeliveredIntact(Report $report): void;    public function testDeliversShortTextualReport(): void    {        $report = new Report('csv', \"id,rating\\n1,5\\n\", 'daily.csv');        $this-&gt;createSender()-&gt;send($report);        $this-&gt;assertDeliveredIntact($report);    }    public function testDeliversLongTextualReport(): void    {        $report = new Report('csv', str_repeat('A', 5000), 'weekly.csv');        $this-&gt;createSender()-&gt;send($report);        $this-&gt;assertDeliveredIntact($report);    }    public function testDeliversBinaryReport(): void    {        $report = new Report('xlsx', \"PK\\x03\\x04binary-content\", 'weekly.xlsx');        $this-&gt;createSender()-&gt;send($report);        $this-&gt;assertDeliveredIntact($report);    }}final class SlackReportSenderTest extends ReportSenderContractTest{    private FakeSlackClient $slack;    protected function createSender(): ReportSender    {        $this-&gt;slack = new FakeSlackClient();        return new SlackReportSender($this-&gt;slack, 'C123');    }    protected function assertDeliveredIntact(Report $report): void    {        \/\/ FakeSlackClient \u0437\u0430\u043f\u043e\u043c\u0438\u043d\u0430\u0435\u0442 \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0443 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u043e\u0442 \u0442\u043e\u0433\u043e,        \/\/ \u0443\u0448\u043b\u0430 \u043e\u043d\u0430 \u0447\u0435\u0440\u0435\u0437 postMessage() \u0438\u043b\u0438 uploadFile()        self::assertSame($report-&gt;body(), $this-&gt;slack-&gt;deliveredContent());        \/\/ \u0418\u043c\u044f \u0444\u0430\u0439\u043b\u0430 \u0438 MIME \u2014 \u0447\u0430\u0441\u0442\u044c \u043f\u043e\u043b\u043d\u043e\u0439 \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438 \u0434\u043b\u044f \u0432\u043b\u043e\u0436\u0435\u043d\u0438\u044f;        \/\/ \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0435 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0438\u0445 \u043d\u0435 \u043d\u0435\u0441\u0451\u0442, \u0438 \u044d\u0442\u043e \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e        \/\/ \u043f\u043e \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0443 (\u0441\u043c. \u0432\u044b\u0448\u0435: \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0432\u043c\u0435\u0441\u0442\u043e \u0444\u0430\u0439\u043b\u0430 \u043e\u043a, \u0435\u0441\u043b\u0438        \/\/ \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 \u0446\u0435\u043b\u043e\u0435)        if ($this-&gt;slack-&gt;deliveredAsFile()) {            self::assertSame($report-&gt;asAttachment()-&gt;filename, $this-&gt;slack-&gt;deliveredFilename());            self::assertSame($report-&gt;mimeType(), $this-&gt;slack-&gt;deliveredMimeType());        }    }}final class EmailReportSenderTest extends ReportSenderContractTest{    private FakeMailer $mailer;    protected function createSender(): ReportSender    {        $this-&gt;mailer = new FakeMailer();        return new EmailReportSender($this-&gt;mailer, 'team@example.com');    }    protected function assertDeliveredIntact(Report $report): void    {        $attachment = $this-&gt;mailer-&gt;lastAttachment();        self::assertSame($report-&gt;body(), $attachment-&gt;content);        self::assertSame($report-&gt;asAttachment()-&gt;filename, $attachment-&gt;filename);        self::assertSame($report-&gt;mimeType(), $attachment-&gt;mimeType);    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0418\u043c\u0435\u043d\u043d\u043e testDeliversBinaryReport \u0432 \u0441\u0442\u0430\u0440\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 SlackReportSender \u0438\u0437 \u0430\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d\u0430 1 \u0443\u043f\u0430\u043b \u0431\u044b \u0441 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u043c, \u0430 \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 \u0434\u043e \u043d\u0435\u0451 \u2014 \u0442\u0438\u0445\u043e \u0438\u0441\u043f\u043e\u0440\u0442\u0438\u043b \u0431\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u0439 XLSX \u043e\u0431\u0440\u0435\u0437\u043a\u043e\u0439 \u043f\u043e mb_substr. \u041e\u0431\u0430 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430 \u043d\u0435 \u043f\u0440\u043e\u0445\u043e\u0434\u044f\u0442 \u043e\u0434\u0438\u043d \u0438 \u0442\u043e\u0442 \u0436\u0435 \u0442\u0435\u0441\u0442 \u043d\u0430 \u0432\u0441\u0435\u0445 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u2014 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u043b\u043e\u0432\u0438\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438, \u0430 \u043d\u0435 \u043a\u043e\u0433\u0434\u0430 \u043a\u0442\u043e-\u0442\u043e \u0437\u0430\u043c\u0435\u0442\u0438\u0442 \u043f\u0443\u0441\u0442\u043e\u0439 \u043e\u0442\u0447\u0451\u0442 \u0432 \u043f\u0440\u043e\u0434\u0435.<\/p>\n<p>\u041d\u043e\u0432\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f ReportSender \u0431\u0435\u0437 \u0441\u0432\u043e\u0435\u0433\u043e \u043d\u0430\u0441\u043b\u0435\u0434\u043d\u0438\u043a\u0430 ReportSenderContractTest \u2014 \u043a\u0440\u0430\u0441\u043d\u044b\u0439 \u0444\u043b\u0430\u0433 \u043d\u0430 code review: \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u043d\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d.<\/p>\n<p>\u0414\u043b\u044f \u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440\u043e\u0432 \u0434\u0430\u0442\u0430-\u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e \u2014 \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u043d\u0443\u0436\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044e \u043f\u043e\u0441\u043b\u0435 \u0432\u044b\u0437\u043e\u0432\u0430, \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430:<\/p>\n<pre><code class=\"php\">\/** @dataProvider formatterProvider *\/public function testFormatterIncludesAllFeedback(FeedbackReportFormatter $formatter): void{    $feedback = [        new Feedback(1, 1, '\u043f\u043b\u043e\u0445\u043e', new \\DateTimeImmutable(), 'site'),        new Feedback(2, 5, '\u043e\u0442\u043b\u0438\u0447\u043d\u043e', new \\DateTimeImmutable(), 'app'),    ];    $report = $formatter-&gt;format($feedback);    self::assertStringContainsString('\u043f\u043b\u043e\u0445\u043e', $report-&gt;body());    self::assertStringContainsString('\u043e\u0442\u043b\u0438\u0447\u043d\u043e', $report-&gt;body());}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0418\u043c\u0435\u043d\u043d\u043e \u044d\u0442\u043e\u0442 \u0442\u0435\u0441\u0442 \u043b\u043e\u0432\u0438\u0442 \u0442\u0438\u0445\u0443\u044e \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044e \u0438\u0437 \u0430\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d\u0430 3 \u2014 \u0438 \u043d\u0435 \u043f\u0440\u043e\u043f\u0443\u0441\u0442\u0438\u0442 \u0432 formatterProvider \u043d\u0438\u0447\u0435\u0433\u043e, \u0447\u0442\u043e \u0444\u0438\u043b\u044c\u0442\u0440\u0443\u0435\u0442 \u00ab\u0434\u043b\u044f \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u043e\u0432\u00bb.<\/p>\n<p>\u0414\u043b\u044f \u043b\u0435\u0433\u0430\u0441\u0438-\u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440\u043e\u0432 \u043d\u0430 mutable-DTO \u2014 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440, \u043d\u0435 \u0441\u043c\u0435\u0448\u0438\u0432\u0430\u0435\u043c \u0441 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u043c:<\/p>\n<pre><code class=\"php\">\/** @dataProvider legacyFormatterProvider *\/public function testLegacyFormatterDoesNotMutateInputObjects(FeedbackReportFormatter $formatter): void{    $feedback = [        new FeedbackRecord(1, 3, '  ok  ', new \\DateTimeImmutable(), 'app'),        new FeedbackRecord(2, 5, 'great', new \\DateTimeImmutable('-1 hour'), 'site'),    ];    $textBefore = array_map(fn(FeedbackRecord $r) =&gt; $r-&gt;text, $feedback);    $formatter-&gt;format($feedback);    self::assertSame($textBefore, array_map(fn(FeedbackRecord $r) =&gt; $r-&gt;text, $feedback));}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>PdfFeedbackReportFormatter-\u0437\u0430\u0433\u043b\u0443\u0448\u043a\u0430 \u0432 \u0442\u043e\u043c \u0436\u0435 formatterProvider \u0443\u043f\u0430\u0434\u0451\u0442 \u043d\u0430 testFormatterIncludesAllFeedback \u2014 \u0438 \u043d\u0435 \u043f\u043e\u043f\u0430\u0434\u0451\u0442 \u0432 cron, \u043f\u043e\u043a\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u0435 \u0433\u043e\u0442\u043e\u0432\u0430.<\/p>\n<p>\u0422\u0435\u0441\u0442\u044b \u0433\u043e\u043d\u044f\u0435\u043c \u0447\u0435\u0440\u0435\u0437 <strong>fake transport<\/strong> (in-memory mail, fake Slack API), \u043d\u0435 \u0447\u0435\u0440\u0435\u0437 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0435 \u043a\u0430\u043d\u0430\u043b\u044b. \u0422\u043e\u0442 \u0436\u0435 \u043f\u043e\u0434\u0445\u043e\u0434 \u2014 \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430. \u0414\u0435\u0448\u0435\u0432\u043b\u0435, \u0447\u0435\u043c \u0447\u0438\u043d\u0438\u0442\u044c \u043f\u0440\u043e\u0434 \u0432 \u043f\u044f\u0442\u043d\u0438\u0446\u0443.<\/p>\n<h3>\u0427\u0442\u043e \u0441\u043e\u0437\u043d\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0435 \u0434\u0435\u043b\u0430\u0435\u043c<\/h3>\n<p>\u0414\u0430\u0436\u0435 \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u0432\u0448\u0438\u0441\u044c \u0441 LSP, \u043c\u044b <strong>\u043d\u0435<\/strong> \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c:<\/p>\n<p>ContractValidatorInterface \u0438 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0434\u0442\u0438\u043f\u043e\u0432<\/p>\n<p>AbstractReportSender \u0441 \u0434\u0435\u0444\u043e\u043b\u0442\u043d\u044b\u043c\u0438 throw new UnsupportedOperationException<\/p>\n<p>LspComplianceChecker \u0432 CI<\/p>\n<p>\u0438\u0435\u0440\u0430\u0440\u0445\u0438\u044e \u0438\u0437 \u043f\u044f\u0442\u0438 \u0443\u0440\u043e\u0432\u043d\u0435\u0439 \u00ab\u043d\u0430 \u0441\u043b\u0443\u0447\u0430\u0439, \u0435\u0441\u043b\u0438 \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u043a\u0430\u043d\u0430\u043b\u00bb<\/p>\n<p>\u0417\u0430\u0434\u0430\u0447\u0430 \u2014 <strong>\u0447\u0435\u0441\u0442\u043d\u044b\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0445 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u043e\u0432<\/strong>, \u0430 \u043d\u0435 \u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0432\u043e\u043a\u0440\u0443\u0433 \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0430. YAGNI \u043d\u0438\u043a\u0443\u0434\u0430 \u043d\u0435 \u0434\u0435\u043b\u0441\u044f.<\/p>\n<h3>Trade-off: \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u043a\u0430, ISP \u0438 \u0442\u0435\u0441\u0442\u044b<\/h3>\n<p><strong>\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c.<\/strong> LSP \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442, \u0447\u0442\u043e\u0431\u044b \u0432\u0441\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u043e \u0431\u044b\u0441\u0442\u0440\u043e. \u0422\u0440\u0435\u0431\u0443\u0435\u0442 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u043e\u0439 <strong>\u043d\u0430\u0431\u043b\u044e\u0434\u0430\u0435\u043c\u043e\u0439 \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u043a\u0438<\/strong>: \u043a\u043b\u0438\u0435\u043d\u0442 \u043d\u0435 \u0434\u043e\u043b\u0436\u0435\u043d \u043c\u0435\u043d\u044f\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443 \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u043f\u043e\u0434\u0442\u0438\u043f\u0430.<\/p>\n<p><strong>ISP \u2194 LSP.<\/strong> \u0415\u0441\u043b\u0438 \u0447\u0435\u0441\u0442\u043d\u043e \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043f\u043e\u0434\u0442\u0438\u043f \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u2014 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u0441\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e, \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0448\u0438\u0440\u043e\u043a\u0438\u0439. ReadOnlyFeedbackRepository extends FeedbackRepository \u2014 \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0435 LSP \u0438 \u043f\u0440\u0435\u0434\u0432\u043a\u0443\u0441 <strong>Interface Segregation<\/strong> (\u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u0441\u0442\u0430\u0442\u044c\u044f \u0441\u0435\u0440\u0438\u0438): \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435 read \u0438 write, \u043f\u043e\u043a\u0430 \u043a\u0442\u043e-\u0442\u043e \u043d\u0435 \u043d\u0430\u043f\u0438\u0441\u0430\u043b \u0442\u0440\u0435\u0442\u0438\u0439 instanceof.<\/p>\n<p><strong>\u041a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u043d\u044b\u0435 \u0442\u0435\u0441\u0442\u044b vs \u0434\u0443\u0431\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435.<\/strong> \u0410\u0431\u0441\u0442\u0440\u0430\u043a\u0442\u043d\u044b\u0439 \u0442\u0435\u0441\u0442-\u043a\u0435\u0439\u0441 \u043d\u0430 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u2014 \u043d\u0435 DRY \u0440\u0430\u0434\u0438 DRY, \u0430 \u0441\u0442\u0440\u0430\u0445\u043e\u0432\u043a\u0430 \u043f\u043e\u043b\u0438\u043c\u043e\u0440\u0444\u0438\u0437\u043c\u0430, \u0437\u0430 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u044b \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 \u0432 OCP-\u0447\u0430\u0441\u0442\u0438.<\/p>\n<p><strong>\u0410\u0434\u0430\u043f\u0442\u0430\u0446\u0438\u044f \u0432\u043d\u0443\u0442\u0440\u0438 sender.<\/strong> Email \u00ab\u043f\u0440\u043e\u0441\u0442\u043e \u0448\u043b\u0451\u0442 \u0432\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u00bb. Slack \u2014 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0438\u043b\u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0444\u0430\u0439\u043b\u0430 \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0442\u0438\u043f\u0430 \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430: \u0440\u0430\u0437\u043d\u0430\u044f \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c \u0438 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0435 \u0440\u0438\u0441\u043a\u0438. LSP \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u043e\u0439 <strong>\u043d\u0430\u0431\u043b\u044e\u0434\u0430\u0435\u043c\u043e\u0439<\/strong> \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u043a\u0438 \u0434\u043b\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430, \u043d\u0435 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u043e\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u043d\u0443\u0442\u0440\u0438.<\/p>\n<p><strong>\u0412\u0430\u0440\u0438\u0430\u043d\u0442\u043d\u043e\u0441\u0442\u044c \u0442\u0438\u043f\u043e\u0432 \u0432 PHP.<\/strong> \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u044b\u0439 \u0442\u0438\u043f \u043c\u0435\u0442\u043e\u0434\u0430 \u0432 \u043d\u0430\u0441\u043b\u0435\u0434\u043d\u0438\u043a\u0435 \u043c\u043e\u0436\u043d\u043e \u0441\u0443\u0436\u0430\u0442\u044c (\u043a\u043e\u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043d\u043e\u0441\u0442\u044c, PHP 7.4+) \u2014 \u044d\u0442\u043e \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0438 LSP \u043d\u0435 \u043d\u0430\u0440\u0443\u0448\u0430\u0435\u0442. \u0410 \u0432\u043e\u0442 \u0442\u0438\u043f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 \u0441\u0443\u0436\u0430\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f \u2014 \u044d\u0442\u043e \u0443\u0436\u0435\u0441\u0442\u043e\u0447\u0430\u0435\u0442 \u043f\u0440\u0435\u0434\u0443\u0441\u043b\u043e\u0432\u0438\u0435. PHP \u0447\u0430\u0441\u0442\u0438\u0447\u043d\u043e \u0437\u0430\u0449\u0438\u0449\u0430\u0435\u0442 \u043e\u0442 \u044d\u0442\u043e\u0433\u043e \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 \u0441\u0438\u0433\u043d\u0430\u0442\u0443\u0440 \u043a\u043b\u0430\u0441\u0441\u043e\u0432, \u043d\u043e \u043d\u0435 \u0437\u0430\u0449\u0438\u0449\u0430\u0435\u0442 PHPDoc-\u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u0438 \u0432\u0440\u043e\u0434\u0435 <a class=\"mention\" href=\"\/users\/param\">@param<\/a> FeedbackRecord[] \u0438\u0437 \u0430\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d\u0430 3 \u2014 \u0438\u0445 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440, \u0430 \u043d\u0435 \u0434\u0432\u0438\u0436\u043e\u043a.<\/p>\n<p><strong>final<\/strong>** **<strong>\u043a\u0430\u043a \u043f\u0440\u043e\u0444\u0438\u043b\u0430\u043a\u0442\u0438\u043a\u0430.<\/strong> \u041a\u043b\u0430\u0441\u0441, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0435 \u043f\u043b\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043b\u0441\u044f \u043a\u0430\u043a \u0431\u0430\u0437\u0430 \u0434\u043b\u044f \u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u044f, \u0441\u0442\u043e\u0438\u0442 \u043f\u043e\u043c\u0435\u0447\u0430\u0442\u044c final. \u041f\u043e\u043b\u043e\u0432\u0438\u043d\u0430 \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0439 LSP \u0432 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 (\u00ab\u0443\u043d\u0430\u0441\u043b\u0435\u0434\u0443\u0435\u043c\u0441\u044f \u0434\u043b\u044f \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430\u00bb) \u0444\u0438\u0437\u0438\u0447\u0435\u0441\u043a\u0438 \u043d\u0435 \u0441\u043b\u0443\u0447\u0438\u0442\u0441\u044f, \u0435\u0441\u043b\u0438 \u0443\u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043f\u0440\u043e\u0441\u0442\u043e \u043d\u0435\u043b\u044c\u0437\u044f.<\/p>\n<h3>\u0412\u044b\u0432\u043e\u0434<\/h3>\n<p>Liskov Substitution Principle \u2014 \u043d\u0435 \u043f\u0440\u043e \u043a\u0432\u0430\u0434\u0440\u0430\u0442\u044b \u043d\u0430 \u0434\u043e\u0441\u043a\u0435 \u0438 \u043d\u0435 \u043f\u0440\u043e \u0437\u0430\u043f\u0440\u0435\u0442 \u043d\u0430 extends.<\/p>\n<p>\u041f\u0440\u0430\u043a\u0442\u0438\u0447\u043d\u044b\u0439 \u043f\u043e\u0434\u0445\u043e\u0434:<\/p>\n<p><strong>\u041f\u043e\u043b\u0438\u043c\u043e\u0440\u0444\u0438\u0437\u043c \u0438\u0437 OCP \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u0447\u0435\u0441\u0442\u043d\u044b\u043c\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f\u043c\u0438.<\/strong> \u0418\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0431\u0435\u0437 LSP \u2014 \u0434\u0435\u043a\u043e\u0440\u0430\u0446\u0438\u044f.<\/p>\n<p><strong>instanceof<\/strong>** **<strong>\u0432 \u0431\u0438\u0437\u043d\u0435\u0441-\u043a\u043e\u0434\u0435 \u2014 \u0441\u0438\u043c\u043f\u0442\u043e\u043c.<\/strong> \u041b\u0435\u0447\u0438\u0442\u0441\u044f \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u043e\u043c, \u043d\u0435 \u0435\u0449\u0451 \u043e\u0434\u043d\u043e\u0439 \u0432\u0435\u0442\u043a\u043e\u0439.<\/p>\n<p><strong>\u041d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u0435 \u0440\u0430\u0434\u0438 \u043f\u0435\u0440\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0438\u043c\u0435\u043d\u0438<\/strong> \u2014 \u0447\u0430\u0441\u0442\u044b\u0439 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0439. Composition \u0438 \u0443\u0437\u043a\u0438\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044b \u043e\u0431\u044b\u0447\u043d\u043e \u0434\u0435\u0448\u0435\u0432\u043b\u0435.<\/p>\n<p><strong>\u041a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u0444\u0438\u043a\u0441\u0438\u0440\u0443\u0439\u0442\u0435 \u0442\u0435\u0441\u0442\u0430\u043c\u0438<\/strong>, \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e PHPDoc. \u041e\u0441\u043e\u0431\u0435\u043d\u043d\u043e \u0435\u0441\u043b\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0439 \u0431\u043e\u043b\u044c\u0448\u0435 \u043e\u0434\u043d\u043e\u0439 \u2014 \u0438 \u043e\u0441\u043e\u0431\u0435\u043d\u043d\u043e \u0435\u0441\u043b\u0438 \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043d\u0443\u0436\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044e fake-\u043e\u0431\u044a\u0435\u043a\u0442\u0430: \u0442\u043e\u0433\u0434\u0430 \u0434\u0430\u0442\u0430-\u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u043d\u0435 \u043f\u043e\u0434\u043e\u0439\u0434\u0451\u0442, \u043d\u0443\u0436\u0435\u043d \u0430\u0431\u0441\u0442\u0440\u0430\u043a\u0442\u043d\u044b\u0439 \u0442\u0435\u0441\u0442-\u043a\u0435\u0439\u0441.<\/p>\n<p><strong>\u0423\u0436\u0435\u0441\u0442\u043e\u0447\u0430\u0442\u044c \u0432\u0445\u043e\u0434 \u0438\u043b\u0438 \u043e\u0441\u043b\u0430\u0431\u043b\u044f\u0442\u044c \u0432\u044b\u0445\u043e\u0434<\/strong> \u0432 \u043f\u043e\u0434\u0442\u0438\u043f\u0435 \u2014 \u043f\u043e\u0432\u043e\u0434 \u0434\u043b\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0430 \u0432\u0430\u0440\u0438\u0430\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438 \u043d\u0430 \u0443\u0440\u043e\u0432\u0435\u043d\u044c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 \u0434\u0430\u043d\u043d\u044b\u0445, \u0430 \u043d\u0435 \u0434\u043b\u044f \u0442\u0438\u0445\u043e\u0433\u043e if \u0432\u043d\u0443\u0442\u0440\u0438 implements.<\/p>\n<p>SOLID \u043d\u0435 \u043e\u0442\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u0434\u0440\u0430\u0432\u044b\u0439 \u0441\u043c\u044b\u0441\u043b. \u0425\u043e\u0440\u043e\u0448\u0430\u044f \u043f\u043e\u0434\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u2014 \u043a\u043e\u0433\u0434\u0430 cron \u043c\u0435\u043d\u044f\u0435\u0442 sender \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0435, \u0430 \u043d\u0435 \u0432 \u043a\u043e\u0434\u0435 \u0441 \u0442\u0440\u0435\u043c\u044f if.<\/p>\n<p>\u0414\u0435\u043b\u0438\u0442\u0435\u0441\u044c \u0432 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u044f\u0445: \u0433\u0434\u0435 \u043f\u043e\u0434\u043c\u0435\u043d\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043b\u043e\u043c\u0430\u043b\u0430 \u043f\u0440\u043e\u0434 \u2014 \u0438 \u043a\u0430\u043a \u0432\u044b \u044d\u0442\u043e \u043f\u043e\u0447\u0438\u043d\u0438\u043b\u0438?<\/p>\n<h3>\u0427\u0442\u043e \u0435\u0449\u0435 \u043f\u043e\u0447\u0438\u0442\u0430\u0442\u044c<\/h3>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/agima\/articles\/1065040\/\">SOLID \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u043c\u0438\u0440\u0435. OCP \u0431\u0435\u0437 \u00ab\u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u0441\u0440\u0430\u0437\u0443 \u0441\u0434\u0435\u043b\u0430\u0435\u043c \u0440\u0430\u0441\u0448\u0438\u0440\u044f\u0435\u043c\u043e\u00bb<\/a><\/p>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/agima\/articles\/1015408\/\">SOLID \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u043c\u0438\u0440\u0435: SRP \u0431\u0435\u0437 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u043d\u044b\u0445 \u043a\u043e\u0441\u043c\u043e\u043b\u0435\u0442\u043e\u0432<\/a><\/p>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/agima\/articles\/884270\/\">\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043d\u0430\u0431\u043e\u0440 \u0442\u0438\u043c\u043b\u0438\u0434\u0430: \u043e\u0442 \u043a\u0430\u043a\u0438\u0445 \u0443\u0431\u0435\u0436\u0434\u0435\u043d\u0438\u0439 \u0441\u0442\u043e\u0438\u0442 \u043e\u0442\u043a\u0430\u0437\u0430\u0442\u044c\u0441\u044f, \u0447\u0442\u043e\u0431\u044b \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u0442\u0435\u0431\u044f \u043d\u0435 \u0432\u043e\u0437\u043d\u0435\u043d\u0430\u0432\u0438\u0434\u0435\u043b\u0430<\/a><\/p>\n<p>\u0410 \u0435\u0449\u0435 \u043f\u043e\u0434\u043f\u0438\u0441\u044b\u0432\u0430\u0439\u0442\u0435\u0441\u044c \u043d\u0430 \u043a\u0430\u043d\u0430\u043b \u043d\u0430\u0448\u0435\u0433\u043e <a href=\"https:\/\/t.me\/cto_neuro\">\u0421\u0422\u041e<\/a> \u0410\u043d\u0434\u0440\u0435\u044f \u041d\u0435\u043f\u0440\u044f\u0445\u0438\u043d\u0430.<\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1070242\/\">https:\/\/habr.com\/ru\/articles\/1070242\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u0414\u0438\u0441\u043a\u043b\u0435\u0439\u043c\u0435\u0440: \u0441\u0442\u0430\u0442\u044c\u044f \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u0432 \u0443\u0440\u043e\u0432\u043d\u044f junior \u0438 middle, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0437\u043d\u0430\u043a\u043e\u043c\u044f\u0442\u0441\u044f \u0441 \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0430\u043c\u0438 \u0447\u0438\u0441\u0442\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u0438 SOLID. \u041f\u0440\u0438\u043c\u0435\u0440\u044b \u043a\u043e\u0434\u0430 \u2014 PHP 8.1+.\u0412\u0441\u0435\u043c \u0434\u043e\u0431\u0440\u043e\u0433\u043e \u0434\u043d\u044f! \u041d\u0430 \u0441\u0432\u044f\u0437\u0438 \u0412\u0430\u043b\u0435\u0432\u0438\u0447 \u0410\u0440\u0442\u0435\u043c \u2014 \u0442\u0438\u043c\u043b\u0438\u0434 \u0432 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 AGIMA.\u0412 \u043f\u0435\u0440\u0432\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 \u0441\u0435\u0440\u0438\u0438 \u043c\u044b \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u043b\u0438 Single Responsibility Principle \u043d\u0430 \u043a\u0435\u0439\u0441\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u0437\u044b\u0432\u043e\u0432. \u0412\u043e \u0432\u0442\u043e\u0440\u043e\u0439 \u2014 Open-Closed Principle: \u043f\u043e\u044f\u0432\u0438\u043b\u0438\u0441\u044c FeedbackReportFormatter, ReportSender, \u0434\u0432\u0435 \u043e\u0441\u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f \u2014 \u0444\u043e\u0440\u043c\u0430\u0442\u044b \u043e\u0442\u0447\u0451\u0442\u043e\u0432 \u0438 \u043a\u0430\u043d\u0430\u043b\u044b \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438.\u041a\u043e\u0434 \u0441\u0442\u0430\u043b \u0440\u0430\u0441\u0448\u0438\u0440\u044f\u0435\u043c\u044b\u043c. \u0418\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044b \u043d\u0430 \u043c\u0435\u0441\u0442\u0435. DI \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438. \u041d\u0430 code review \u0432\u0441\u0451 \u043a\u0440\u0430\u0441\u0438\u0432\u043e.\u0410 \u0432 \u043f\u044f\u0442\u043d\u0438\u0446\u0443 \u0432\u0435\u0447\u0435\u0440\u043e\u043c cron \u0448\u043b\u0451\u0442 \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u044b\u0439 CSV \u0432 Slack \u2014 \u0438 \u0445\u0432\u043e\u0441\u0442 \u043e\u0442\u0447\u0451\u0442\u0430 \u043e\u0431\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0431\u0435\u0437 \u0435\u0434\u0438\u043d\u043e\u0433\u043e \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u044f. \u041f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u00ab\u043f\u043e\u0434\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u0439\u00bb sender \u043e\u043a\u0430\u0437\u0430\u043b\u0441\u044f \u043d\u0435 \u0441\u043e\u0432\u0441\u0435\u043c \u043f\u043e\u0434\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u043c.\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0440\u0430\u0437\u0431\u0438\u0440\u0435\u043c \u2014 Liskov Substitution Principle (LSP, \u043f\u0440\u0438\u043d\u0446\u0438\u043f \u043f\u043e\u0434\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u041b\u0438\u0441\u043a\u043e\u0432). \u0412 \u0443\u0447\u0435\u0431\u043d\u0438\u043a\u0430\u0445 \u0435\u0433\u043e \u043e\u0431\u044a\u044f\u0441\u043d\u044f\u044e\u0442 \u0447\u0435\u0440\u0435\u0437 \u043a\u0432\u0430\u0434\u0440\u0430\u0442 \u0438 \u043f\u0440\u044f\u043c\u043e\u0443\u0433\u043e\u043b\u044c\u043d\u0438\u043a: \u0443 \u043f\u0440\u044f\u043c\u043e\u0443\u0433\u043e\u043b\u044c\u043d\u0438\u043a\u0430 \u043c\u043e\u0436\u043d\u043e \u043c\u0435\u043d\u044f\u0442\u044c \u0448\u0438\u0440\u0438\u043d\u0443 \u0438 \u0432\u044b\u0441\u043e\u0442\u0443 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e, \u0443 \u043a\u0432\u0430\u0434\u0440\u0430\u0442\u0430 \u2014 \u043d\u0435\u0442, \u0437\u043d\u0430\u0447\u0438\u0442 \u043a\u0432\u0430\u0434\u0440\u0430\u0442 \u00ab\u043b\u043e\u043c\u0430\u0435\u0442\u00bb \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043f\u0440\u044f\u043c\u043e\u0443\u0433\u043e\u043b\u044c\u043d\u0438\u043a\u0430.\u041f\u0440\u0438\u043c\u0435\u0440 \u043a\u043b\u0430\u0441\u0441\u0438\u0447\u0435\u0441\u043a\u0438\u0439. \u0418 \u0431\u0435\u0441\u043f\u043e\u043b\u0435\u0437\u043d\u044b\u0439, \u0435\u0441\u043b\u0438 \u0432\u044b \u043a\u0430\u0436\u0434\u044b\u0439 \u0434\u0435\u043d\u044c \u043d\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u0443\u0435\u0442\u0435 \u0433\u0435\u043e\u043c\u0435\u0442\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u0444\u0438\u0433\u0443\u0440\u044b.\u041d\u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0435 LSP \u043b\u043e\u043c\u0430\u0435\u0442\u0441\u044f \u0442\u0430\u043c, \u0433\u0434\u0435 OCP \u0443\u0436\u0435 \u0432\u044b\u0438\u0433\u0440\u0430\u043b: \u0432 \u043f\u043e\u043b\u0438\u043c\u043e\u0440\u0444\u043d\u044b\u0445 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430\u0445, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0432\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u043e \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u043b\u0438.\u0413\u043b\u0430\u0432\u043d\u044b\u0439 \u0432\u043e\u043f\u0440\u043e\u0441: \u043a\u0430\u043a \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0442\u044c LSP, \u043a\u043e\u0433\u0434\u0430 OCP \u0443\u0436\u0435 \u0434\u0430\u043b \u043f\u043e\u043b\u0438\u043c\u043e\u0440\u0444\u0438\u0437\u043c \u2014 \u0438 \u043d\u0435 \u0441\u043b\u043e\u043c\u0430\u0442\u044c \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u00ab\u043f\u043e\u0434\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u043c\u0438\u00bb \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f\u043c\u0438?\u0427\u0442\u043e \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0434\u0435\u043b\u0435 \u043e\u0437\u043d\u0430\u0447\u0430\u0435\u0442 LSP\u041f\u0440\u0438\u043d\u0446\u0438\u043f \u0441\u0444\u043e\u0440\u043c\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043b\u0430 \u0411\u0430\u0440\u0431\u0430\u0440\u0430 \u041b\u0438\u0441\u043a\u043e\u0432 \u0432 \u0434\u043e\u043a\u043b\u0430\u0434\u0435 Data Abstraction and Hierarchy (OOPSLA, 1987). \u041f\u043e\u0437\u0436\u0435, \u0432\u043c\u0435\u0441\u0442\u0435 \u0441 Jeannette Wing, \u0435\u0433\u043e \u0444\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043b\u0438 \u0432 \u0442\u0435\u0440\u043c\u0438\u043d\u0430\u0445 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0447\u0435\u0441\u043a\u0438\u0445 \u043f\u043e\u0434\u0442\u0438\u043f\u043e\u0432 \u2014 A Behavioral Notion of Subtyping (1994): \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043f\u043e\u0434\u0442\u0438\u043f\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0437\u0430\u043c\u0435\u043d\u044f\u0435\u043c\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c\u0438 \u0431\u0430\u0437\u043e\u0432\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u0431\u0435\u0437 \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0441\u0442\u0438 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b.\u0420\u043e\u0431\u0435\u0440\u0442 \u041c\u0430\u0440\u0442\u0438\u043d \u0432\u043a\u043b\u044e\u0447\u0438\u043b LSP \u0432 SOLID \u0438 \u0441\u0444\u043e\u0440\u043c\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043b \u043f\u0440\u043e\u0449\u0435:\u041f\u043e\u0434\u0442\u0438\u043f\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0437\u0430\u043c\u0435\u043d\u044f\u0435\u043c\u044b \u0434\u043b\u044f \u0441\u0432\u043e\u0438\u0445 \u0431\u0430\u0437\u043e\u0432\u044b\u0445 \u0442\u0438\u043f\u043e\u0432.\u0417\u0432\u0443\u0447\u0438\u0442 \u043a\u0430\u043a \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 implements. \u041d\u0430 \u0434\u0435\u043b\u0435 \u2014 \u043d\u0435\u0442.LSP \u2014 \u043f\u0440\u043e \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0438 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u0432\u044b\u0437\u044b\u0432\u0430\u044e\u0449\u0435\u0433\u043e \u043a\u043e\u0434\u0430, \u043d\u0435 \u043f\u0440\u043e \u043a\u043b\u044e\u0447\u0435\u0432\u043e\u0435 \u0441\u043b\u043e\u0432\u043e extends \u0432 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0438 \u043a\u043b\u0430\u0441\u0441\u0430. \u0415\u0441\u043b\u0438 \u043a\u043b\u0438\u0435\u043d\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441 ReportSender, \u043e\u043d \u0432\u043f\u0440\u0430\u0432\u0435 \u043e\u0436\u0438\u0434\u0430\u0442\u044c: \u043b\u044e\u0431\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442 \u043e\u0442\u0447\u0451\u0442 \u043f\u043e \u043e\u0434\u043d\u0438\u043c \u0438 \u0442\u0435\u043c \u0436\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430\u043c. \u041d\u0435 \u00ab\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442, \u0435\u0441\u043b\u0438 \u043f\u043e\u0432\u0435\u0437\u0451\u0442 \u0441 \u0444\u043e\u0440\u043c\u0430\u0442\u043e\u043c\u00bb.\u0421\u0432\u044f\u0437\u043a\u0430 \u0441 OCP \u043f\u0440\u044f\u043c\u0430\u044f. Open-Closed \u043e\u0431\u0435\u0449\u0430\u0435\u0442: \u043d\u043e\u0432\u043e\u0435 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0440\u044f\u0434\u043e\u043c, \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0439 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u043d\u0435 \u0442\u0440\u043e\u0433\u0430\u0435\u043c. \u041d\u043e \u0435\u0441\u043b\u0438 \u043d\u043e\u0432\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u0432\u0435\u0434\u0451\u0442 \u0441\u0435\u0431\u044f \u0438\u043d\u0430\u0447\u0435, \u0447\u0435\u043c \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u2014 OCP \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u0432 \u00ab\u043e\u0442\u043a\u0440\u044b\u0442\u043e \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f, \u0437\u0430\u043a\u0440\u044b\u0442\u043e \u0434\u043b\u044f \u0441\u044e\u0440\u043f\u0440\u0438\u0437\u043e\u0432 \u0432 \u043f\u0440\u043e\u0434\u0435\u00bb.\u041a\u0435\u0439\u0441: \u043e\u0442\u0437\u044b\u0432\u044b \u043f\u043e\u0441\u043b\u0435 OCP\u041d\u0430\u043f\u043e\u043c\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442. \u041f\u0440\u043e\u0434\u0443\u043a\u0442 \u0441 \u0442\u0440\u0435\u043c\u044f \u043a\u0430\u043d\u0430\u043b\u0430\u043c\u0438 \u2014 \u0441\u0430\u0439\u0442, \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, PWA. \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0442\u0437\u044b\u0432 \u0441 \u043e\u0446\u0435\u043d\u043a\u043e\u0439 1\u20135, \u0442\u0435\u043a\u0441\u0442\u043e\u043c \u0438 \u043c\u0435\u0442\u043a\u043e\u0439 \u043a\u0430\u043d\u0430\u043b\u0430. \u041e\u0442\u0437\u044b\u0432\u044b \u043b\u0435\u0436\u0430\u0442 \u0432 PostgreSQL. \u0420\u0430\u0437 \u0432 \u043d\u0435\u0434\u0435\u043b\u044e \u2014 XLSX \u043d\u0430 email, \u043f\u043b\u044e\u0441 \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u044b\u0439 CSV \u0434\u043b\u044f \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u043e\u0432. \u0427\u0430\u0441\u0442\u044c \u043e\u0442\u0447\u0451\u0442\u043e\u0432 \u0443\u0445\u043e\u0434\u0438\u0442 \u0432 Slack.\u0412 OCP-\u0447\u0430\u0441\u0442\u0438 \u043c\u044b \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u043b\u0438 \u0442\u043e\u0447\u043a\u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f \u2014 \u0433\u0434\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u044b \u0438 \u043a\u0430\u043d\u0430\u043b\u044b. \u0427\u0435\u0441\u0442\u043d\u043e\u0441\u0442\u044c \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0445 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0439 \u0442\u0430\u043c \u043d\u0435 \u0431\u044b\u043b\u0430 \u0442\u0435\u043c\u043e\u0439: SlackReportSender \u043d\u0430 \u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0435 \u2014 \u043f\u0440\u043e\u0441\u0442\u043e \u0435\u0449\u0451 \u043e\u0434\u043d\u0430 \u0441\u0442\u0440\u0435\u043b\u043a\u0430 \u043a \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443. \u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0441\u043c\u043e\u0442\u0440\u0438\u043c, \u0447\u0442\u043e \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442, \u043a\u043e\u0433\u0434\u0430 \u0441\u0442\u0440\u0435\u043b\u043a\u0430 \u0432\u0440\u0451\u0442.\u041a \u0444\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0439 \u0442\u043e\u0447\u043a\u0435 OCP \u043c\u044b \u043f\u0440\u0438\u0448\u043b\u0438 \u043a \u0442\u0430\u043a\u043e\u043c\u0443 \u043a\u043e\u0434\u0443:interface FeedbackReportFormatter{    \/** @param Feedback[] $feedback *\/    public function format(array $feedback): Report;}interface ReportSender{    \/**     * \u0414\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u043d\u044b\u0439 \u043e\u0442\u0447\u0451\u0442. \u041e\u0448\u0438\u0431\u043a\u0438 \u0432\u043d\u0435\u0448\u043d\u0435\u0439 \u0441\u0440\u0435\u0434\u044b \u2014 \u0447\u0435\u0440\u0435\u0437     * \u0441\u043e\u0433\u043b\u0430\u0441\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0442\u0438\u043f, \u0430 \u043d\u0435 \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u044b\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438.     *     * @throws ReportDeliveryException     *\/    public function send(Report $report): void;}class FeedbackReportService{    public function __construct(        private FeedbackRepository $repository    ) {}    public function send(        DateRange $period,        FeedbackReportFormatter $formatter,        ReportSender $sender    ): void {        $feedback = $this-&gt;repository-&gt;getForPeriod($period);        $report = $formatter-&gt;format($feedback);        $sender-&gt;send($report);    }}\u041f\u0435\u0440\u0438\u043e\u0434 \u0436\u0438\u0432\u0451\u0442 \u0441\u043d\u0430\u0440\u0443\u0436\u0438 \u2014 cron \u0437\u043d\u0430\u0435\u0442 \u0440\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435. \u041a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0441\u043e\u0431\u0438\u0440\u0430\u044e\u0442\u0441\u044f \u0432 job\u2019\u0430\u0445:\/\/ cron: \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u044b\u0439 CSV \u2192 email$reportService-&gt;send(    DateRange::lastDay(),    new CsvFeedbackReportFormatter(),    new EmailReportSender());\/\/ cron: \u0435\u0436\u0435\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u044b\u0439 XLSX \u2192 Slack$reportService-&gt;send(    DateRange::lastWeek(),    new XlsxFeedbackReportFormatter(),    new SlackReportSender());FeedbackReportService \u043d\u0435 \u0437\u043d\u0430\u0435\u0442 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0445 \u043a\u043b\u0430\u0441\u0441\u043e\u0432. \u041e\u043d \u0434\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0443: \u043b\u044e\u0431\u043e\u0439 \u0444\u043e\u0440\u043c\u0430\u0442\u0442\u0435\u0440 \u0438 \u043b\u044e\u0431\u043e\u0439 sender. \u0418\u043c\u0435\u043d\u043d\u043e \u0437\u0434\u0435\u0441\u044c LSP \u043b\u0438\u0431\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u043b\u0438\u0431\u043e \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043f\u044f\u0442\u043d\u0438\u0447\u043d\u044b\u0439 \u0434\u0435\u043f\u043b\u043e\u0439 \u0432 \u043a\u0432\u0435\u0441\u0442.\u041e\u0431\u0449\u0438\u0435 \u0442\u0438\u043f\u044b, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044f \u043a\u0435\u0439\u0441, \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u0438\u043b\u0438\u0441\u044c \u0441\u043e \u0432\u0440\u0435\u043c\u0451\u043d OCP- \u0438 SRP-\u0441\u0442\u0430\u0442\u0435\u0439:final class DateRange{    public function __construct(        public readonly \\DateTimeImmutable $from,        public readonly \\DateTimeImmutable $to,    ) {}    public static function lastDay(): self    {        $to = new \\DateTimeImmutable(&#8216;today&#8217;);        return new self($to-&gt;modify(&#8216;-1 day&#8217;), $to);    }    public static function lastWeek(): self    {        $to = new \\DateTimeImmutable(&#8216;today&#8217;);        return new self($to-&gt;modify(&#8216;-7 days&#8217;), $to);    }}final class Attachment{    public function __construct(        public readonly string $filename,        public readonly string $content,        public readonly string $mimeType,    ) {}}final class Feedback{    public function __construct(        public readonly int $id,        public readonly int $rating,        public readonly string $text,        public readonly \\DateTimeImmutable $createdAt,        public readonly string $channel, \/\/ site | app | pwa    ) {}}\u0410 \u0432\u043e\u0442 Report \u0432 \u044d\u0442\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 \u043f\u043e\u043c\u0435\u043d\u044f\u043b\u0441\u044f: \u0440\u044f\u0434\u043e\u043c \u0441 format() \u043f\u043e\u044f\u0432\u0438\u043b\u0438\u0441\u044c isTextual() \u0438 body() \u2014 \u043e\u043d\u0438 \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u044f\u0442\u0441\u044f \u0434\u0430\u043b\u044c\u0448\u0435, \u0447\u0442\u043e\u0431\u044b sender \u043c\u043e\u0433 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0442\u044c \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043f\u043e \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0443 \u043e\u0442\u0447\u0451\u0442\u0430, \u0430 \u043d\u0435 \u043f\u043e \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044e \u0444\u043e\u0440\u043c\u0430\u0442\u043e\u0432 \u0432 \u043a\u043e\u0434\u0435:final class Report{    private const TEXTUAL_FORMATS = [&#8216;csv&#8217;];    public function __construct(        private string $format, \/\/ csv | xlsx | pdf        private string $body,        private string $filename,    ) {}    public function format(): string    {        return $this-&gt;format;    }    \/** csv \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0442\u0435\u043a\u0441\u0442; xlsx\/pdf \u2014 \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u0435 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u044b *\/    public function isTextual(): bool    {        return in_array($this-&gt;format, self::TEXTUAL_FORMATS, true);    }    public function body(): string    {        return $this-&gt;body;    }    public function mimeType(): string    {        return match ($this-&gt;format) {            &#8216;csv&#8217; =&gt; &#8216;text\/csv&#8217;,            &#8216;xlsx&#8217; =&gt; &#8216;application\/vnd.openxmlformats-officedocument.spreadsheetml.sheet&#8217;,            &#8216;pdf&#8217; =&gt; &#8216;application\/pdf&#8217;,            default =&gt; &#8216;application\/octet-stream&#8217;,        };    }    public function asAttachment(): Attachment    {        return new Attachment($this-&gt;filename, $this-&gt;body, $this-&gt;mimeType());    }}\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 1: \u00ab\u043f\u043e\u0434\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u0439\u00bb sender \u0441 \u0443\u0436\u0435\u0441\u0442\u043e\u0447\u0451\u043d\u043d\u044b\u043c\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430\u043c\u0438\u0420\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043f\u0438\u0448\u0435\u0442 SlackReportSender. \u0423 Slack \u0435\u0441\u0442\u044c \u043f\u0440\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f \u043d\u0430 \u0440\u0430\u0437\u043c\u0435\u0440 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f (\u0443\u0442\u043e\u0447\u043d\u044f\u0439\u0442\u0435 \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u044b \u2014 \u043e\u043d\u043e \u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f). \u0420\u0430\u0437\u0443\u043c\u043d\u0430\u044f \u043c\u044b\u0441\u043b\u044c. \u041d\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0441\u044f \u0442\u0430\u043a\u043e\u0439:class SlackReportSender implements ReportSender{    private const MAX_MESSAGE_LENGTH = 4000;    public function __construct(        private SlackClient $slack,        private string $channelId,    ) {}    public function send(Report $report): void    {        if (!$report-&gt;isTextual()) {            throw new \\InvalidArgumentException(&#8216;Slack sender \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u044b\u0435 \u043e\u0442\u0447\u0451\u0442\u044b&#8217;);        }        $fullText = $report-&gt;body();        $text = mb_strlen($fullText) &gt; self::MAX_MESSAGE_LENGTH            ? mb_substr($fullText, 0, self::MAX_MESSAGE_LENGTH) \/\/ \u043c\u043e\u043b\u0447\u0430 \u0440\u0435\u0436\u0435\u043c \u0445\u0432\u043e\u0441\u0442            : $fullText;        $this-&gt;slack-&gt;postMessage($this-&gt;channelId, $text);    }}\u0414\u043b\u044f \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u044f \u2014 EmailReportSender, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c cron \u0443\u0436\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0431\u0435\u0437 \u0441\u044e\u0440\u043f\u0440\u0438\u0437\u043e\u0432:class EmailReportSender implements ReportSender{    public function __construct(        private Mailer $mailer,        private string $recipient,    ) {}    public function send(Report $report): void    {        $this-&gt;mailer-&gt;send(            to: $this-&gt;recipient,            subject: &#8216;\u041e\u0442\u0447\u0451\u0442 \u043f\u043e \u043e\u0442\u0437\u044b\u0432\u0430\u043c&#8217;,            attachment: $report-&gt;asAttachment(),        );    }}\u0424\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e SlackReportSender \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u0442 ReportSender. \u041d\u0430 \u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0435 \u2014 \u0435\u0449\u0451 \u043e\u0434\u043d\u0430 \u0441\u0442\u0440\u0435\u043b\u043e\u0447\u043a\u0430 \u043a \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443.\u041f\u0440\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u2014 \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0435 LSP, \u043f\u0440\u0438\u0447\u0451\u043c \u0441\u0440\u0430\u0437\u0443 \u0434\u0432\u043e\u0439\u043d\u043e\u0435. \u0412\u0430\u0436\u043d\u043e: \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0435 \u2014 \u043e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0430 ReportSender (\u00ab\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043b\u044e\u0431\u043e\u0439 Report \u0446\u0435\u043b\u0438\u043a\u043e\u043c\u00bb), \u0430 \u043d\u0435 \u043e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u00ab\u0441\u0440\u0435\u0434\u043d\u0435\u0433\u043e \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u044f\u00bb \u0441\u043e\u0441\u0435\u0434\u043d\u0438\u0445 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0439. \u0421\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u0435 \u0441 EmailReportSender \u043d\u0438\u0436\u0435 \u2014 \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u043a\u043b\u0438\u0435\u043d\u0442 \u0443\u0436\u0435 \u043f\u0440\u0438\u0432\u044b\u043a \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c; \u0441\u0430\u043c \u043f\u043e \u0441\u0435\u0431\u0435 \u0444\u0430\u043a\u0442 \u00abEmail \u0432\u0435\u0434\u0451\u0442 \u0441\u0435\u0431\u044f \u0438\u043d\u0430\u0447\u0435\u00bb \u043d\u0435 \u0437\u0430\u0434\u0430\u0451\u0442 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u0434\u043b\u044f Slack.\u0443\u0436\u0435\u0441\u0442\u043e\u0447\u0430\u0435\u0442 \u043f\u0440\u0435\u0434\u0443\u0441\u043b\u043e\u0432\u0438\u0435 \u043d\u0430 \u0432\u0445\u043e\u0434: \u043e\u0442\u043a\u043b\u043e\u043d\u044f\u0435\u0442 XLSX \u2014 \u0445\u043e\u0442\u044f \u043a\u043b\u0438\u0435\u043d\u0442 ReportSender \u0432\u043f\u0440\u0430\u0432\u0435 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u043b\u044e\u0431\u043e\u0439 Report (\u0438 EmailReportSender \u044d\u0442\u043e \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442);\u043e\u0441\u043b\u0430\u0431\u043b\u044f\u0435\u0442 \u043f\u043e\u0441\u0442\u0443\u0441\u043b\u043e\u0432\u0438\u0435 \u043d\u0430 \u0442\u043e\u043c, \u0447\u0442\u043e \u043f\u0440\u0438\u043d\u044f\u043b: \u043c\u043e\u043b\u0447\u0430 \u043e\u0431\u0440\u0435\u0437\u0430\u0435\u0442 \u0434\u043b\u0438\u043d\u043d\u044b\u0439 CSV \u2014 \u0445\u043e\u0442\u044f \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u043e\u0431\u0435\u0449\u0430\u0435\u0442 \u043f\u043e\u043b\u043d\u0443\u044e \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0443 (\u0438 email \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u0431\u044b \u043e\u0442\u0447\u0451\u0442 \u0446\u0435\u043b\u0438\u043a\u043e\u043c).\u042d\u0442\u043e \u0438 \u0435\u0441\u0442\u044c \u043f\u044f\u0442\u043d\u0438\u0447\u043d\u044b\u0439 \u0438\u043d\u0446\u0438\u0434\u0435\u043d\u0442 \u0438\u0437 \u0438\u043d\u0442\u0440\u043e: \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u044b\u0439 CSV \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044e \u0438 \u0442\u0438\u0445\u043e \u0442\u0435\u0440\u044f\u0435\u0442 \u0445\u0432\u043e\u0441\u0442, \u0435\u0441\u043b\u0438 \u043e\u043d \u0434\u043b\u0438\u043d\u043d\u0435\u0435 \u043b\u0438\u043c\u0438\u0442\u0430 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u2014 \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0438\u043c\u0435\u043d\u043d\u043e \u043f\u043e\u0442\u0435\u0440\u044f\u0435\u0442\u0441\u044f, \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u043e\u0431\u044a\u0451\u043c\u0430 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u043e\u0442\u0447\u0451\u0442\u0430, \u043d\u0435 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u00ab\u043f\u043e\u043b\u043e\u0432\u0438\u043d\u0430\u00bb. \u0410 \u0435\u0436\u0435\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u044b\u0439 XLSX \u0432 Slack \u0432 \u044d\u0442\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043e\u0434\u0430 \u043f\u0430\u0434\u0430\u0435\u0442 \u0441 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u043c \u0441\u0440\u0430\u0437\u0443 \u2014 \u0442\u043e \u0435\u0441\u0442\u044c \u043e\u0442\u0447\u0451\u0442 \u0442\u0435\u0440\u044f\u0435\u0442\u0441\u044f \u0446\u0435\u043b\u0438\u043a\u043e\u043c, \u0430 \u043d\u0435 \u0447\u0430\u0441\u0442\u0438\u0447\u043d\u043e, \u0447\u0442\u043e \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u044f \u043d\u0438\u0447\u0435\u043c \u043d\u0435 \u043b\u0443\u0447\u0448\u0435.\u0427\u0435\u0440\u0435\u0437 \u043d\u0435\u0434\u0435\u043b\u044e \u0432 \u0432\u044b\u0437\u044b\u0432\u0430\u044e\u0449\u0435\u043c \u043a\u043e\u0434\u0435 \u043f\u043e\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f:$formatter = new XlsxFeedbackReportFormatter();$sender = $channel === &#8216;slack&#8217;    ? new SlackReportSender($slack, $channelId)    : new EmailReportSender($mailer, $recipient);if ($sender instanceof SlackReportSender &amp;&amp; $formatter instanceof XlsxFeedbackReportFormatter) {    $sender = new EmailReportSender($mailer, $recipient); \/\/ \u043e\u0431\u0445\u043e\u0434\u043d\u043e\u0439 \u043c\u0430\u043d\u0451\u0432\u0440}$reportService-&gt;send(DateRange::lastWeek(), $formatter, $sender);instanceof \u0432 \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0435 \u2014 \u043a\u043b\u0430\u0441\u0441\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0437\u0430\u043f\u0430\u0445: \u043f\u043e\u0434\u0442\u0438\u043f \u043d\u0435\u043b\u044c\u0437\u044f \u0447\u0435\u0441\u0442\u043d\u043e \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0432\u043c\u0435\u0441\u0442\u043e \u0431\u0430\u0437\u043e\u0432\u043e\u0433\u043e \u0442\u0438\u043f\u0430. \u0418\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043e\u0431\u0435\u0449\u0430\u0435\u0442 \u043e\u0434\u043d\u043e, \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u2014 \u0434\u0440\u0443\u0433\u043e\u0435.\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 2: \u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u0435 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f \u00ab\u0434\u043b\u044f \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430\u00bb\u0412\u0435\u0440\u043d\u0451\u043c\u0441\u044f \u043a \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044e. \u0412 SRP-\u0447\u0430\u0441\u0442\u0438 FeedbackRepository \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0438 \u0447\u0442\u0435\u043d\u0438\u0435:class FeedbackRepository{    public function __construct(private \\PDO $pdo) {}    public function save(Feedback $feedback): void    {        $stmt = $this-&gt;pdo-&gt;prepare(       &#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-490867","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/490867","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=490867"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/490867\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=490867"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=490867"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=490867"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}