Jekyll2018-05-05T21:47:51+00:00/Юрий Изотов, Ruby on Rails developerПерсональный сайт веб-разработчика Rails из Екатеринбурга, Россия
Сервисы в Rails, Symfony, Yii2 и Zend Expressive: реализуем одну задачу на четырех фреймворках2018-01-12T07:14:00+00:002018-01-12T07:14:00+00:00/services-rails-and-symfony-and-yii2<p>Долгое время муссировалась и продолжает муссироваться в кругах разных каркасов идея сервисов. Во многих фреймворках сервисы стали самостоятельными единицами кода (как скажем, в Symfony о чем мы еще поговорим). В Rails же паттерн Service Object выглядит очень просто, так как никаких «самостоятельных единиц» именуемых сервисами, в нем нет.</p>
<p>В этой статье я покажу сначала разработку страницы, использующей сервис в Symfony 3.3.9 а затем — то же самое в Rails. Сразу скажу читатели раздела Rails будут слегка разочарованы — «он опять одну строчку написал». Ну извините, это же пример.</p>
<p>Несмотря на то что я являюсь ROR разработчиком, каркас Symfony мне очень понравился. Я считаю его документацию и продукты такие как twig — лучшими в нише. Надеюсь, с выходом версии 4 такая тенденция не утеряется. А выход этот уже произошел.</p>
<p>Итак, что будет за задача?</p>
<p>Почешем там где у нас чешется — есть у меня просто папка в локальной файловой системе, там находится куча шахматных книжек. Я хочу сделать страницу где бы они перечислялись.</p>
<p>Для этого мы и используем сервисы — в нашем случае сервис какой возвращает список книг (файлов) в удобном для нас виде. Минуя директории.</p>
<h3 id="делаем-это-на-symfony">Делаем это на Symfony</h3>
<p>Программисты на чистом PHP сразу же бросятся делать readdir и много чего другого в стиле «да я щас за пять минут». Мы тоже не будем усложнять, но использовать мы будем нормальный подход — генератор <a href="http://php.net/manual/ru/class.directoryiterator.php">DirectoryIterator</a>.</p>
<p>Хотя, как уже не единожды заметили симфонисты, лучшей практикой именно для Symfony будет использование специального компонента для нахождения файлов - <a href="http://symfony.com/doc/current/components/finder.html">Finder</a></p>
<p>Итак, у нас есть Symfony 3.3.9. Создадим-ка контроллер книг, какой собственно будет запускаться для отображения страницы</p>
<figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="c1">// src/AppBundle/Controller/BooksController.php
</span>
<span class="k">namespace</span> <span class="nx">AppBundle\Controller</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Sensio\Bundle\FrameworkExtraBundle\Configuration\Route</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Symfony\Bundle\FrameworkBundle\Controller\AbstractController</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">AppBundle\Service\DirectoryLister</span><span class="p">;</span>
<span class="k">class</span> <span class="nc">BooksController</span> <span class="k">extends</span> <span class="nx">AbstractController</span>
<span class="p">{</span>
<span class="sd">/**
* @Route("/books/list", name="books_list")
*/</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">listAction</span><span class="p">(</span><span class="nx">DirectoryLister</span> <span class="nv">$dirService</span><span class="p">)</span>
<span class="p">{</span>
<span class="nv">$files</span> <span class="o">=</span> <span class="nv">$dirService</span><span class="o">-></span><span class="na">getFileList</span><span class="p">();</span>
<span class="k">return</span> <span class="nv">$this</span><span class="o">-></span><span class="na">render</span><span class="p">(</span><span class="s1">'books/list.html.twig'</span><span class="p">,</span> <span class="p">[</span><span class="s1">'files'</span> <span class="o">=></span> <span class="nv">$files</span><span class="p">]);</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>Как видим, контроллер у нас простой. Он обращается к сервису, какой внедряется к нему в качестве зависимости с помощью DI, получает список файлов. А потом просто отображает шаблон, передав туда этот список.</p>
<p>Обратите внимание, что класс контроллера наследован от AbstractController. Это налагает ограничение на вызовы сервисов из сервис-контейнера внутри экшена, и делает разработку более строгой. Теперь, наследовав так, мы можем только совершать инжект зависимости (при помощи type-hint) но не вызывать эту зависимость из сервис-контейнера внутри экшена.</p>
<p>Эти действия предназначены для более строгой разработки, чтобы не было случаев когда у нас есть экшн какой делает 10 дел и распухает в «метод-Бог» а мы этого или не видим, или не желаем видеть. В случае же инжектов сразу ясно — если у вас параметры метода содержат 20 инжектов значит происходит что-то очень нехорошее и вам нужно срочно рефакторить приложение.</p>
<p>Займемся сочинением непосредственно нашего сервиса:</p>
<figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="c1">// src/AppBundle/Service/DirectoryLister.php
</span>
<span class="k">namespace</span> <span class="nx">AppBundle\Service</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">DirectoryIterator</span><span class="p">;</span>
<span class="k">class</span> <span class="nc">DirectoryLister</span>
<span class="p">{</span>
<span class="k">private</span> <span class="nv">$path</span><span class="p">;</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">__construct</span><span class="p">(</span><span class="nv">$path</span><span class="p">)</span>
<span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">path</span> <span class="o">=</span> <span class="nv">$path</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">getFileList</span><span class="p">()</span>
<span class="p">{</span>
<span class="nv">$directoryIteratorInstance</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">DirectoryIterator</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="na">path</span><span class="p">);</span>
<span class="k">foreach</span> <span class="p">(</span><span class="nv">$directoryIteratorInstance</span> <span class="k">as</span> <span class="nv">$fileNode</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$fileNode</span><span class="o">-></span><span class="na">isDir</span><span class="p">())</span> <span class="p">{</span>
<span class="nv">$files</span><span class="p">[]</span> <span class="o">=</span> <span class="nv">$fileNode</span><span class="o">-></span><span class="na">getFilename</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nb">sort</span><span class="p">(</span><span class="nv">$files</span><span class="p">);</span>
<span class="k">return</span> <span class="nv">$files</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>В случае же использования Finder, исполняемый код метода выглядел бы примерно так (подсказал <a href="https://habrahabr.ru/users/SerafimArts/">Кирилл Несмеянов</a>):</p>
<figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="k">public</span> <span class="k">function</span> <span class="nf">getFileList</span><span class="p">()</span><span class="o">:</span> <span class="nx">iterable</span>
<span class="p">{</span>
<span class="k">yield</span> <span class="nx">from</span> <span class="p">(</span><span class="k">new</span> <span class="nx">Finder</span><span class="p">())</span><span class="o">-></span><span class="na">files</span><span class="p">()</span><span class="o">-></span><span class="na">in</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="na">path</span><span class="p">);</span>
<span class="p">}</span></code></pre></figure>
<p>Сервис наш должен быть многоцелевым, применяться неоднократно, повторно использоваться. По этому в него с помощью аргумента конструктора внедряется параметр path какой указывает ИЗ КАКОЙ директории собственно, возвращать список файлов. В Symfony это называется Autowiring. Так же отметьте, что мы используем генератор <a href="http://php.net/manual/ru/class.directoryiterator.php">DirectoryIterator</a>, а не наколенный перебор файлов в директории. Это дает нам удобные методы isDir и другие.</p>
<p>Куда же теперь поместить параметр пути к файлам? Симфони дает однозначный ответ, это файл</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">app/config/parameters.yml</code></pre></figure>
<p>Он по умолчанию не ставится на контроль версий git, и вообще предназначен для «переносных» меняющихся настроек. Вот туда-то мы и положим значение для сервиса, какое он автоматически прочитает:</p>
<figure class="highlight"><pre><code class="language-yml" data-lang="yml"><span class="na">parameters</span><span class="pi">:</span>
<span class="c1">#other params</span>
<span class="na">books_path</span><span class="pi">:</span> <span class="s">/home/izotoff/Документы/Шахматы</span></code></pre></figure>
<p>Но это еще не все. В файле</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">app/config/Services.yml</code></pre></figure>
<p>нам необходимо указать, что наш сервис принимает этот параметр:</p>
<figure class="highlight"><pre><code class="language-yml" data-lang="yml"><span class="s">AppBundle\Service\DirectoryLister</span><span class="pi">:</span>
<span class="na">arguments</span><span class="pi">:</span>
<span class="s">$path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">%books_path%"</span></code></pre></figure>
<p>Отлично! Все почти готово. Если сейчас обратиться по адресу /app_dev.php/books/list вашего хоста, выскочит ошибка о том что не найден шаблон twig. Поправим это:</p>
<script src="https://gist.github.com/d34a929d5a255d475d20f83b5e2986db.js"> </script>
<p>Все теперь работает. Полюбуемся на результат, зайдя на хост (у меня это symfony.local) то есть мой адрес будет http://symfony.local/app_dev.php/books/list а продакшн http://symfony.local/books/list. Не забудьте перед запуском продакшна сделать регенерацию кеша симфони (в том числе он кеширует роуты):</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">bin/console cache:clear <span class="nt">--env</span><span class="o">=</span>prod</code></pre></figure>
<p>Сделав все эти манипуляции с кешем, или зайдя как на скрине — через app_dev.php то есть с поддержкой дебаг тулбара, мы увидим следующую милую картину: (<a href="https://habrastorage.org/webt/fp/m8/pa/fpm8pas0bsrcqosmvzvoomvtryo.png">посмотреть увеличенную</a>):</p>
<p><img src="https://habrastorage.org/webt/fp/m8/pa/fpm8pas0bsrcqosmvzvoomvtryo.png" alt="Список книг локальной ФС с сервисом на Symfony" class="img-responsive" /></p>
<h3 id="делаем-это-на-ruby-on-rails">Делаем это на Ruby on Rails</h3>
<p>Уфф, тут кода будет меньше, и телодвижений тоже. Но это не значит что Symfony чем-то хуже или что он громоздок. Концепция сервисов как практикуемых единиц очень полезна и хороша. В Rails же мы начнем с простого. Сначала создадим само приложение:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">rails new booksdemo <span class="o">&&</span> <span class="nb">cd </span>booksdemo</code></pre></figure>
<p>А теперь создадим-ка контроллер, для простоты с всего одним действием, как и в Symfony:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">rails g controller Books index</code></pre></figure>
<p>Чтобы использовать паттерн Service Object, по сути в Rails достаточно положить файл представляющий «сервис» куда-нибудь в такое место, где он будет автоматически загружен. Создадим файл-сервис в app/services, по умолчанию этой папки нет но сейчас появится:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">mkdir app/services <span class="o">&&</span> touch app/services/directory_list_service.rb</code></pre></figure>
<p>Теперь у нас есть файл, какой будет загружен Rails самостоятельно. Заполним его:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1">#app/services/directory_list_service.rb</span>
<span class="k">class</span> <span class="nc">DirectoryListService</span>
<span class="nb">attr_reader</span> <span class="ss">:files</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
<span class="vi">@files</span> <span class="o">=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">glob</span><span class="p">(</span><span class="n">path</span><span class="p">).</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">filename</span><span class="o">|</span> <span class="no">File</span><span class="p">.</span><span class="nf">file?</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span> <span class="p">}.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">fullpath</span><span class="o">|</span> <span class="no">File</span><span class="p">.</span><span class="nf">basename</span> <span class="n">fullpath</span> <span class="p">}.</span><span class="nf">sort</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Вот и весь сервис. За счет гибкости Ruby мы получили гораздо меньше кода для перебора директории.</p>
<p>Используем его в контроллере:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1">#app/controllers/books_controller.rb</span>
<span class="k">class</span> <span class="nc">BooksController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="k">def</span> <span class="nf">index</span>
<span class="vi">@files</span> <span class="o">=</span> <span class="no">DirectoryListService</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'/home/izotoff/Документы/Шахматы/*'</span><span class="p">).</span><span class="nf">files</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>И наконец, заполним вид (view):</p>
<figure class="highlight"><pre><code class="language-erb" data-lang="erb"><span class="nt"><h1></span>Книги (<span class="cp"><%=</span> <span class="vi">@files</span><span class="p">.</span><span class="nf">count</span> <span class="cp">%></span>)<span class="nt"></h1></span>
<span class="cp"><%</span> <span class="vi">@files</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">file</span><span class="o">|</span> <span class="cp">%></span>
<span class="nt"><div></span><span class="cp"><%=</span> <span class="n">file</span> <span class="cp">%></span><span class="nt"></div></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span></code></pre></figure>
<p>Вот и все! Теперь мы увидим мало отличающуюся от Symfony-реализации картину, запустив</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">rails s</code></pre></figure>
<p>и перейдя на http://localhost:3000/books/index :</p>
<p><img src="https://habrastorage.org/webt/fl/kk/1i/flkk1ir-3vlqg1e6a6vxiphhcws.png" alt="Список книг локальной ФС с сервисом на Ruby on Rails" class="img-responsive" /></p>
<p>Тоже <a href="https://habrastorage.org/webt/fl/kk/1i/flkk1ir-3vlqg1e6a6vxiphhcws.png">можно увеличить</a>.</p>
<p>Теперь поговорим о гибкости решения. Во-первых поскольку мы в сервисе используем Dir.glob то мы ставим звездочку в конце нашего пути к файлам. Во-вторых этот самый путь мы прописываем прямо там, где сервис используется — в контроллере. Это не совсем гибко! Но идеология Rails состоит как раз в том чтобы избавляться от внешних файлов настроек, даже таких безобидных как parameters.yml в Symfony. Да, мы конечно можем все-таки оформить путь к файлам как статичную настройку. Но делать это по крайней мере, в данной статье мы не будем. Не потому что «и так сойдет» а потому что это Rails. И не факт что другой контроллер запросит уже иной путь, тогда наше решение выиграет по гибкости так как путь можно указывать создавая класс объекта-сервиса.</p>
<p>Подведя итоги, скажу что оба каркаса хоть я и за ROR, прекрасно справляются с задачей, просто каждый своим путем. В Symfony можно тоже сделать сервис гибким, чтобы передавать в него параметр при вызове каждый раз но это я оставил «за кулисами» статьи, предпочтя автосвязывание аргументов из файла настроек.</p>
<h3 id="делаем-это-на-yii2">Делаем это на Yii2</h3>
<p>Так как я общаюсь и уважаю многих разработчиков на фреймворке Yii2, камрад <a href="https://github.com/lavros">lavros</a> любезно написал гайд как сделать задачу на yii2. И это оказалось очень интересным, так как он показал сильные стороны фреймворка, например генерация кода консольным gii но об этом ниже. Вот этот гайд, передаю слово автору.</p>
<p>Сделаем тоже самое на <a href="http://www.yiiframework.com/doc-2.0/guide-intro-yii.html">Yii2</a>. Создадим контроллер, действие и представление. Реализуем сервис по получению списка файлов из указанной директории. Внедрим сервис-объект через конструктор в контроллер, получим данные в действии контроллера и отразим в представлении.</p>
<p>Итак, поехали!</p>
<p>В качестве шаблона приложения воспользуемся <a href="https://github.com/yiisoft/yii2-app-basic">yii2-app-basic</a>.</p>
<p>Развернём шаблон yii2-app-basic с помощью <a href="https://getcomposer.org/">composer</a>:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">composer create-project <span class="nt">--prefer-dist</span> yiisoft/yii2-app-basic books.local</code></pre></figure>
<p>Приложение развернули. Создадим контроллер, действие и представление для будущей страницы. В Yii2 есть инструмент для генерации кода — <a href="http://www.yiiframework.com/doc-2.0/guide-start-gii.html">Gii</a>. Имеется web версия и консольная, мы воспользуемся консольной.</p>
<p>Для этого перейдём в каталог с приложением и выполним команду:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nb">cd </span>books.local
./yii gii/controller <span class="nt">--controllerClass</span><span class="o">=</span><span class="s2">"app</span><span class="se">\c</span><span class="s2">ontrollers</span><span class="se">\B</span><span class="s2">ookController"</span> <span class="nt">--actions</span><span class="o">=</span>list</code></pre></figure>
<p>Результатом работы команды будет:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">Running <span class="s1">'Controller Generator'</span>...
The following files will be generated:
<span class="o">[</span>new] controllers/BookController.php
<span class="o">[</span>new] views/book/list.php
Ready to generate the selected files? <span class="o">(</span>yes|no<span class="o">)</span> <span class="o">[</span>yes]:y
Files were generated successfully!
Generating code using template <span class="s2">"/path/to/books.local/vendor/yiisoft/yii2-gii/generators/controller/default"</span>...
generated controllers/BookController.php
generated views/book/list.php
<span class="k">done</span><span class="o">!</span></code></pre></figure>
<p>Страницу создали. Запустим приложение чтобы посмотреть на страницу:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">./yii serve</code></pre></figure>
<p>Откроем браузер по адресу: http://localhost:8080?r=book/list, на странице увидим:</p>
<p><img src="https://habrastorage.org/webt/gn/fc/fp/gnfcfpkocibb3nt-klqksaqi5uq.png" alt="Созданная нами страница в Yii2" class="img-responsive" /></p>
<p>Хорошо, страница есть. Теперь реализуем сервис-объект для получения списка файлов. Создадим каталог для сервисов — services, в нём файл DirectoryListerService.php:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">mkdir services <span class="o">&&</span> <span class="nb">cd </span>services <span class="o">&&</span> touch DirectoryListerService.php</code></pre></figure>
<p>Реализуем метод getFileList() для получения списка файлов. В Yii2 есть помощник по работе с файлами — <a href="http://www.yiiframework.com/doc-2.0/yii-helpers-filehelper.html">FileHelper</a>. Воспользуемся FileHelper для получения файлов в указанной директории.</p>
<p>Отредактируем файл DirectoryListerService.php:</p>
<figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="k">namespace</span> <span class="nx">app\services</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">yii\helpers\FileHelper</span><span class="p">;</span>
<span class="k">class</span> <span class="nc">DirectoryListerService</span>
<span class="p">{</span>
<span class="k">private</span> <span class="nv">$path</span><span class="p">;</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">__construct</span><span class="p">(</span><span class="nv">$path</span><span class="p">)</span>
<span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">path</span> <span class="o">=</span> <span class="nv">$path</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">getFileList</span><span class="p">()</span>
<span class="p">{</span>
<span class="k">return</span> <span class="nx">FileHelper</span><span class="o">::</span><span class="na">findFiles</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="na">path</span><span class="p">,</span> <span class="p">[</span><span class="s1">'recursive'</span> <span class="o">=></span> <span class="kc">false</span><span class="p">]);</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>Сервис-объект реализован, подключим его в нашем контроллере controllers/BookController.php. Можно подключить явно, создав в методе actionList() экземпляр класса app\services\DirectoryListerService, но мы пойдём другим путём — воспользуемся <a href="http://www.yiiframework.com/doc-2.0/guide-concept-di-container.html#constructor-injection">внедрением зависимости через конструктор</a>:</p>
<p>Для начала настроим <a href="http://www.yiiframework.com/doc-2.0/guide-concept-di-container.html">контейнер зависимостей</a> в конфигурации приложения, отредактируем config/web.php:</p>
<figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="c1">//...
</span><span class="nv">$config</span> <span class="o">=</span> <span class="p">[</span>
<span class="c1">// ...
</span> <span class="s1">'container'</span> <span class="o">=></span> <span class="p">[</span>
<span class="s1">'singletons'</span> <span class="o">=></span> <span class="p">[</span>
<span class="s1">'app\services\DirectoryListerService'</span> <span class="o">=></span> <span class="p">[</span>
<span class="p">[</span><span class="s1">'class'</span> <span class="o">=></span> <span class="s1">'app\services\DirectoryListerService'</span><span class="p">],</span>
<span class="p">[</span><span class="s1">'/home/izotoff/Документы/Шахматы'</span><span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="c1">// ...
</span><span class="p">];</span></code></pre></figure>
<p>Объект описали как singleton, то есть, где бы мы не внедряли наш сервис-объект, экземляр будет создаваться один раз.</p>
<p>Внедрим в конструкторе contorllers\BookController.php:</p>
<figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="k">namespace</span> <span class="nx">app\controllers</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">app\services\DirectoryListerService</span><span class="p">;</span>
<span class="k">class</span> <span class="nc">BookController</span> <span class="k">extends</span> <span class="nx">\yii\web\Controller</span>
<span class="p">{</span>
<span class="k">protected</span> <span class="nv">$directoryListerService</span><span class="p">;</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">__construct</span><span class="p">(</span><span class="nv">$id</span><span class="p">,</span> <span class="nv">$module</span><span class="p">,</span> <span class="nx">DirectoryListerService</span> <span class="nv">$directoryListerService</span><span class="p">,</span> <span class="nv">$config</span> <span class="o">=</span> <span class="p">[])</span>
<span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">directoryListerService</span> <span class="o">=</span> <span class="nv">$directoryListerService</span><span class="p">;</span>
<span class="k">parent</span><span class="o">::</span><span class="na">__construct</span><span class="p">(</span><span class="nv">$id</span><span class="p">,</span> <span class="nv">$module</span><span class="p">,</span> <span class="nv">$config</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// ...
</span><span class="p">}</span></code></pre></figure>
<p>Теперь при создании контроллера сервис DirectoryListerService будет создаваться автоматически.</p>
<p>Отредактируем метод BookController::actionList(), получим список файлов, посчитаем количество найденых файлов и передадим в представление:</p>
<figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="c1">//...
</span>
<span class="k">class</span> <span class="nc">BookController</span> <span class="k">extends</span> <span class="nx">\yii\web\Controller</span>
<span class="p">{</span>
<span class="c1">//...
</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">actionList</span><span class="p">()</span>
<span class="p">{</span>
<span class="nv">$files</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="na">directoryListerService</span><span class="o">-></span><span class="na">getFileList</span><span class="p">();</span>
<span class="nv">$numFiles</span> <span class="o">=</span> <span class="nb">count</span><span class="p">(</span><span class="nv">$files</span><span class="p">);</span>
<span class="nb">sort</span><span class="p">(</span><span class="nv">$files</span><span class="p">);</span>
<span class="k">return</span> <span class="nv">$this</span><span class="o">-></span><span class="na">render</span><span class="p">(</span><span class="s1">'list'</span><span class="p">,</span> <span class="p">[</span>
<span class="s1">'files'</span> <span class="o">=></span> <span class="nv">$files</span><span class="p">,</span>
<span class="s1">'numFiles'</span> <span class="o">=></span> <span class="nv">$numFiles</span>
<span class="p">]);</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>Оформим представление действия views/book/list.php:</p>
<figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="k">use</span> <span class="nx">yii\helpers\Html</span><span class="p">;</span>
<span class="cm">/* @var $this yii\web\View */</span>
<span class="cp">?></span>
<span class="nt"><h1></span>Книги (<span class="cp"><?=</span> <span class="nv">$numFiles</span> <span class="cp">?></span>)<span class="nt"></h1></span>
<span class="nt"><ol></span>
<span class="cp"><?php</span> <span class="k">foreach</span> <span class="p">(</span><span class="nv">$files</span> <span class="k">as</span> <span class="nv">$file</span><span class="p">)</span><span class="o">:</span> <span class="cp">?></span>
<span class="nt"><li></span><span class="cp"><?=</span> <span class="nx">Html</span><span class="o">::</span><span class="na">a</span><span class="p">(</span><span class="nb">basename</span><span class="p">(</span><span class="nv">$file</span><span class="p">),</span> <span class="s2">"file://</span><span class="si">{</span><span class="nv">$file</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="cp">?></span><span class="nt"></li></span>
<span class="cp"><?php</span> <span class="k">endforeach</span> <span class="cp">?></span>
<span class="nt"></ol></span></code></pre></figure>
<p>Готово. Обновляем страницу в браузере, смотрим результат.</p>
<p>Он будет таким же, как и в предыдущих примерах, только снабжен yii2 debug toolbar. Похожее мы видели в скриншоте для symfony — там тоже оставлен был тулбар для отладки.</p>
<p>В целом, мне очень понравился подход yii2 и это было действительно не сложно и не долго, при условии что это делает знакомый с документацией и возможностями yii2 человек.</p>
<p>Спасибо <a href="https://github.com/lavros">lavros</a> за важный гайд! Уверен, некоторых это даже подстегнет изучать Yii2.</p>
<h3 id="список-книг-на-zend-expressive">Список книг на Zend Expressive</h3>
<p>Камрад <a href="http://mihaly4.ru/">Ми}{алы4</a> любезно продолжил нашу традицию и во мгновение ока написал гайд как сделать это же на Zend Expressive. Передаю слово ему.</p>
<p>Для решения задачи по отображению списка книг из директории используется <a href="https://docs.zendframework.com/zend-expressive/">Zend Expressive</a> с модульной структорой, Zend ServiceManager, FastRoute, Twig, Whoops. Все эти пакеты можно выбрать на этапе создания нового проекта, когда вы выполняете команду</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">composer create-project zendframework/zend-expressive-skeleton expressive</code></pre></figure>
<p><strong>Создадим сервис в модуле App (модуль App создается по умолчанию после создания пустого проекта
Zend Expressive):</strong></p>
<figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="k">namespace</span> <span class="nx">App\Service</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">DirectoryIterator</span><span class="p">;</span>
<span class="k">class</span> <span class="nc">BookService</span>
<span class="p">{</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">getList</span><span class="p">(</span><span class="nx">string</span> <span class="nv">$path</span><span class="p">)</span><span class="o">:</span> <span class="k">array</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">is_dir</span><span class="p">(</span><span class="nv">$path</span><span class="p">))</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">[];</span>
<span class="p">}</span>
<span class="nv">$dir</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">DirectoryIterator</span><span class="p">(</span><span class="nv">$path</span><span class="p">);</span>
<span class="k">foreach</span> <span class="p">(</span><span class="nv">$dir</span> <span class="k">as</span> <span class="nv">$item</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$item</span><span class="o">-></span><span class="na">isFile</span><span class="p">())</span> <span class="p">{</span>
<span class="nv">$files</span><span class="p">[]</span> <span class="o">=</span> <span class="nv">$item</span><span class="o">-></span><span class="na">getFilename</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nb">sort</span><span class="p">(</span><span class="nv">$files</span><span class="p">);</span>
<span class="k">return</span> <span class="nv">$files</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p><strong>Зарегистрируем наш сервис:</strong></p>
<figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="c1">// src/App/src/ConfigProvider.php
</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">getDependencies</span><span class="p">()</span>
<span class="p">{</span>
<span class="k">return</span> <span class="p">[</span>
<span class="o">...</span>
<span class="s1">'factories'</span> <span class="o">=></span> <span class="p">[</span>
<span class="o">...</span>
<span class="nx">Service\BookService</span><span class="o">::</span><span class="na">class</span> <span class="o">=></span> <span class="nx">\Zend\ServiceManager\Factory\InvokableFactory</span><span class="o">::</span><span class="na">class</span><span class="p">,</span>
<span class="o">...</span>
<span class="p">],</span>
<span class="o">...</span>
<span class="p">];</span>
<span class="p">}</span></code></pre></figure>
<p><strong>Создадим действие <code class="highlighter-rouge">BookAction</code> в модуле App:</strong></p>
<figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="c1">// src/App/src/Action/BooksAction.php
</span>
<span class="k">namespace</span> <span class="nx">App\Action</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">App\Service\BookService</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Interop\Http\ServerMiddleware\DelegateInterface</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Interop\Http\ServerMiddleware\MiddlewareInterface</span> <span class="k">as</span> <span class="nx">ServerMiddlewareInterface</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Zend\Diactoros\Response\HtmlResponse</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Psr\Http\Message\ServerRequestInterface</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Zend\Expressive\Template\TemplateRendererInterface</span><span class="p">;</span>
<span class="k">class</span> <span class="nc">BooksAction</span> <span class="k">implements</span> <span class="nx">ServerMiddlewareInterface</span>
<span class="p">{</span>
<span class="k">private</span> <span class="k">const</span> <span class="no">BOOKS_DIR</span> <span class="o">=</span> <span class="nx">__DIR__</span> <span class="o">.</span> <span class="s1">'/../../../../data/books'</span><span class="p">;</span>
<span class="sd">/**
* @var BookService
*/</span>
<span class="k">private</span> <span class="nv">$bookService</span><span class="p">;</span>
<span class="sd">/**
* @var TemplateRendererInterface
*/</span>
<span class="k">private</span> <span class="nv">$tpl</span><span class="p">;</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">process</span><span class="p">(</span><span class="nx">ServerRequestInterface</span> <span class="nv">$request</span><span class="p">,</span> <span class="nx">DelegateInterface</span> <span class="nv">$delegate</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">HtmlResponse</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="na">tpl</span><span class="o">-></span><span class="na">render</span><span class="p">(</span><span class="s1">'app::books'</span><span class="p">,</span> <span class="p">[</span>
<span class="s1">'books'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="na">bookService</span><span class="o">-></span><span class="na">getList</span><span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="na">BOOKS_DIR</span><span class="p">),</span>
<span class="p">]));</span>
<span class="p">}</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">__construct</span><span class="p">(</span><span class="nx">BookService</span> <span class="nv">$bookService</span><span class="p">,</span> <span class="nx">TemplateRendererInterface</span> <span class="nv">$tpl</span><span class="p">)</span>
<span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">bookService</span> <span class="o">=</span> <span class="nv">$bookService</span><span class="p">;</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">tpl</span> <span class="o">=</span> <span class="nv">$tpl</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p><strong>Зарегистрируем наше действие:</strong></p>
<figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="c1">// src/App/src/ConfigProvider.php
</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">getDependencies</span><span class="p">()</span>
<span class="p">{</span>
<span class="k">return</span> <span class="p">[</span>
<span class="o">...</span>
<span class="s1">'factories'</span> <span class="o">=></span> <span class="p">[</span>
<span class="o">...</span>
<span class="nx">Action\BooksAction</span><span class="o">::</span><span class="na">class</span> <span class="o">=></span> <span class="nx">\Zend\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory</span><span class="o">::</span><span class="na">class</span><span class="p">,</span>
<span class="o">...</span>
<span class="p">],</span>
<span class="o">...</span>
<span class="p">];</span>
<span class="p">}</span></code></pre></figure>
<p><strong>Зарегистрируем маршрут для действия:</strong></p>
<figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="c1">// config/routes.php
</span>
<span class="c1">//...
</span>
<span class="nv">$app</span><span class="o">-></span><span class="na">get</span><span class="p">(</span><span class="s1">'/books'</span><span class="p">,</span> <span class="nx">App\Action\BooksAction</span><span class="o">::</span><span class="na">class</span><span class="p">,</span> <span class="s1">'books'</span><span class="p">);</span></code></pre></figure>
<p><strong>Выполним переход по нашему только что добавленному маршруту http://localhost:8080/books :</strong></p>
<p><img src="https://habrastorage.org/webt/ns/sc/nr/nsscnr_ilv7ysaircqxvww65jvo.png" alt="Список книг локальной ФС с сервисом на Zend Expressive" class="img-responsive" /></p>
<p>(<a href="https://habrastorage.org/webt/ns/sc/nr/nsscnr_ilv7ysaircqxvww65jvo.png">посмотреть увеличенную картинку</a>):</p>
<p><strong>Готово!</strong></p>
<p><strong>P.S.</strong> Процесс можно ускороить используя <a href="https://docs.zendframework.com/zend-expressive/reference/cli-tooling/#expressive-command-line-tool">Command-Line Tool</a>.</p>Долгое время муссировалась и продолжает муссироваться в кругах разных каркасов идея сервисов. Во многих фреймворках сервисы стали самостоятельными единицами кода (как скажем, в Symfony о чем мы еще поговорим). В Rails же паттерн Service Object выглядит очень просто, так как никаких «самостоятельных единиц» именуемых сервисами, в нем нет.Паттерн «Конфигурирование Модуля» в Ruby2017-06-11T00:00:00+00:002017-06-11T00:00:00+00:00/%D0%9F%D0%B0%D1%82%D1%82%D0%B5%D1%80%D0%BD-%D0%9A%D0%BE%D0%BD%D1%84%D0%B8%D0%B3%D1%83%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%9C%D0%BE%D0%B4%D1%83%D0%BB%D1%8F<p>Обсудим паттерн «конфигурирования модуля» а так же его характерные черты, приемы реализации и тест-кейсы (требования).</p>
<p>Для начала придумаем наш сферический модуль в ваккууме, какой делает что-нибудь полезное.</p>
<p>В учебном примере не важно, что модуль делает конкретно. Мы не будем писать его функционал, а применим прием (паттерн) конфигурирования
модуля (или гема).</p>
<p>Он существует в несколько разных вариантах и реализациях, но это нормально для Ruby с его свободой самовыражения.</p>
<p>Назовем наш модуль MomentalPush, допустим он каким-то образом посылает сообщения пользователям в соцсети.</p>
<p>Конечно, хорошо бы иметь возможность его красиво настроить.</p>
<p>Допустим, у модуля есть настройки: в какие соцсети посылать сообщения, а так же какой email использовать для отправки сообщений по почте, помимо соцсетей.</p>
<p>Стандарт де-факто в конфигурировании гемов и отдельных модулей это применение блока.</p>
<p>Вот так:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="no">MomentalPush</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
<span class="n">config</span><span class="p">.</span><span class="nf">social_networks</span> <span class="o">=</span> <span class="p">[</span><span class="ss">:twitter</span><span class="p">,</span> <span class="ss">:vk</span><span class="p">,</span> <span class="ss">:facebook</span><span class="p">]</span>
<span class="n">config</span><span class="p">.</span><span class="nf">sender_email</span> <span class="o">=</span> <span class="s1">'iamservice@gmail.com'</span>
<span class="k">end</span></code></pre></figure>
<p>Безусловно, количество таких настроек зависит от размера и задач Вашего модуля. Он может требовать две-три, но может и гораздо больше,
например 10 настроечных параметров.</p>
<p>Итак, за дело. Я буду использовать тест-инструмент Rspec.</p>
<p>Выработаем самые простые требования к модулю:</p>
<p>Он должен:</p>
<ol>
<li>Существовать вообще</li>
<li>Иметь метод configure</li>
<li>При присвоении настроечных переменных хранить их и при обращении к конфигурации — возвращать</li>
<li>Хорошо бы иметь настройки по умолчанию если пользователь ввел их не все, или пропустил обязательные</li>
<li>Если программист ошибся при настройке и ввел параметр какой в нашем модуле не предусмотрен, нужно чтобы он немедленно получил <strike>леща</strike> исключение. Это очень важный момент! Многие упускают его и их решения не гибки — можно вводить в блоке конфигурации какую угодно белиберду и она «молча срабатывает» что потом вызывает трудности при отладке, ловле ошибок если вместо name ошиблись и ввели настройку с названием names например.</li>
</ol>
<p>Создадим тест на базе этих требований:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require_relative</span> <span class="s1">'../lib/momental_push'</span>
<span class="n">describe</span> <span class="no">MomentalPush</span> <span class="k">do</span>
<span class="n">describe</span> <span class="s1">'#configure'</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:networks</span><span class="p">)</span> <span class="p">{</span> <span class="no">MomentalPush</span><span class="p">.</span><span class="nf">config</span><span class="p">.</span><span class="nf">social_networks</span> <span class="p">}</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:sender_email</span><span class="p">)</span> <span class="p">{</span> <span class="no">MomentalPush</span><span class="p">.</span><span class="nf">config</span><span class="p">.</span><span class="nf">sender_email</span> <span class="p">}</span>
<span class="n">shared_examples</span> <span class="s2">"a true params types"</span> <span class="k">do</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">expect</span><span class="p">(</span><span class="n">networks</span><span class="p">).</span><span class="nf">to</span> <span class="n">be_a</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">expect</span><span class="p">(</span><span class="n">sender_email</span><span class="p">).</span><span class="nf">to</span> <span class="n">be_a</span><span class="p">(</span><span class="no">String</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="n">context</span> <span class="s2">"when true set and get config vars"</span> <span class="k">do</span>
<span class="n">before</span> <span class="ss">:each</span> <span class="k">do</span>
<span class="no">MomentalPush</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
<span class="n">config</span><span class="p">.</span><span class="nf">social_networks</span> <span class="o">=</span> <span class="p">[</span><span class="ss">:twitter</span><span class="p">,</span> <span class="ss">:vk</span><span class="p">,</span> <span class="ss">:facebook</span><span class="p">]</span>
<span class="n">config</span><span class="p">.</span><span class="nf">sender_email</span> <span class="o">=</span> <span class="s1">'iamservice@gmail.com'</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">it_behaves_like</span> <span class="s2">"a true params types"</span>
<span class="n">it</span> <span class="s2">"must return preconfigured values"</span> <span class="k">do</span>
<span class="n">expect</span><span class="p">(</span><span class="n">networks</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span><span class="p">(</span><span class="sx">%i{twitter vk facebook}</span><span class="p">)</span>
<span class="n">expect</span><span class="p">(</span><span class="n">sender_email</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span><span class="p">(</span><span class="s1">'iamservice@gmail.com'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">context</span> <span class="s2">"when not configured module"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"must return default values"</span> <span class="k">do</span>
<span class="no">MomentalPush</span><span class="p">.</span><span class="nf">reset</span>
<span class="n">expect</span><span class="p">(</span><span class="n">networks</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span><span class="p">(</span><span class="sx">%i{telegram}</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">it_behaves_like</span> <span class="s2">"a true params types"</span>
<span class="k">end</span>
<span class="n">context</span> <span class="s2">"when user get unknown attr"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"must be exception"</span> <span class="k">do</span>
<span class="no">MomentalPush</span><span class="p">.</span><span class="nf">reset</span>
<span class="n">expect</span> <span class="p">{</span> <span class="no">MomentalPush</span><span class="p">.</span><span class="nf">configure</span> <span class="p">{</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span> <span class="n">config</span><span class="p">.</span><span class="nf">my_unknown_param</span> <span class="o">=</span> <span class="s2">"yahoo!"</span> <span class="p">}</span> <span class="p">}.</span><span class="nf">to</span> <span class="n">raise_error</span><span class="p">(</span><span class="no">NoMethodError</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Тест конечно можно еще отрефакторить, нет предела совершенству. Но он достаточно неплох.</p>
<p>Обратите внимание — тестируются типы данных с помощью shared_examples, устранено повторение кода путем общих let-деклараций.
Для удобства тестирования (это не обязательно!) введена возможность в решение ресета конфигурации MomentalPush.reset
Так же, проверяется случай когда программист вводит какую-нибудь неизвестную модулю переменную.</p>
<p>А теперь код самого решения, собственно сам паттерн «конфигурация модуля»:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">module</span> <span class="nn">MomentalPush</span>
<span class="k">class</span> <span class="o"><<</span> <span class="nb">self</span>
<span class="nb">attr_accessor</span> <span class="ss">:config</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">configure</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">config</span> <span class="o">||=</span> <span class="no">Config</span><span class="p">.</span><span class="nf">new</span>
<span class="k">yield</span><span class="p">(</span><span class="n">config</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">reset</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">config</span> <span class="o">=</span> <span class="no">Config</span><span class="p">.</span><span class="nf">new</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">Config</span>
<span class="nb">attr_accessor</span> <span class="ss">:social_networks</span><span class="p">,</span> <span class="ss">:sender_email</span>
<span class="k">def</span> <span class="nf">initialize</span>
<span class="vi">@social_networks</span> <span class="o">=</span> <span class="p">[</span><span class="ss">:telegram</span><span class="p">]</span>
<span class="vi">@sender_email</span> <span class="o">=</span> <span class="s1">'noreply@myservice.com'</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Что тут нужно пояснить?
Во-первых трюк с class << self просто сделан из-за того что модуль не может иметь наследников, но может быть «сам по себе» синглетом.
То есть к модулю можно обращаться. Но если бы мы объявили аксесор без раскрытия модуля «на себя» как синглета, то аксесор имели бы
классы и обьекты какие примешиваются к модулю, а не он сам статично. По этому используется трюк «раскрытие синглета на себя».</p>
<p>Далее — статик методы конфигурации лениво возвращают конфигурацию (экземпляр встроенного в модуль класса Config к какому с помощью трюка дан доступ извне для обращений) и вызывают yield
выброс назад конфигурации в контекст блока конфигурирования.</p>
<p>Так как класс Config имеет четко известные поля и их значения по умолчанию, то модуль соответственно выдает как и нужно в тестовых случаях, NoMethodError исключение при неизвестном параметре
так как неизвестный параметр «за кулисами» это обращение к несуществующему методу класса Config. Что нам и нужно.</p>
<p>Есть и альтернативные способы реализации, например без трюка на раскрытие:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">module</span> <span class="nn">MyModule</span>
<span class="no">DefaultConfig</span> <span class="o">=</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:per_page</span><span class="p">)</span> <span class="k">do</span>
<span class="k">def</span> <span class="nf">initialize</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">name</span> <span class="o">=</span> <span class="s1">'test'</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">per_page</span> <span class="o">=</span> <span class="mi">10</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">configure</span>
<span class="vi">@config</span> <span class="o">=</span> <span class="no">DefaultConfig</span><span class="p">.</span><span class="nf">new</span>
<span class="k">yield</span><span class="p">(</span><span class="vi">@config</span><span class="p">)</span> <span class="k">if</span> <span class="nb">block_given?</span>
<span class="vi">@config</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">config</span>
<span class="vi">@config</span> <span class="o">||</span> <span class="n">configure</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Хоть код более читаем и понятен, а вместо аксесора и трюка используется статик переменная модуля, мне больше нравится первый вариант. Хотя это на усмотрение разработчика!</p>
<p>Ruby создан для самовыражения, по этому даже паттерн или прием могут реализоваться по разному.</p>Обсудим паттерн «конфигурирования модуля» а так же его характерные черты, приемы реализации и тест-кейсы (требования).Простое решение по title в Rails 52017-05-28T00:00:00+00:002017-05-28T00:00:00+00:00/%D0%9F%D1%80%D0%BE%D1%81%D1%82%D0%BE%D0%B5-%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BF%D0%BE-title-%D0%B2-ror<p>Как-то раз зашла беседа с товарищем из мира yii2 о тайтлах. В yii2 тайтл устанавливается в самом виде. Это в принципе, нормальная практика (как-никак, title тег это часть ВИДА страницы).</p>
<p>В ROR можно сделать подобное, если Вы не очень заморачиваетесь с SEO и интернационализацией. Способ хорош если нужно “быстро, решительно” расставить тайтлы на базе содержимого, но логики проекта в дальнейшем (и пожелания SEO экспертов) Вы не знаете.</p>
<p>Суть проста. Нам поможет метод content_for, какой позволяет вставлять содержимое в yield-блок вьюхи. Такие блоки должны быть именованы, чтобы техника работала.</p>
<p>В мейн-лейауте сделаем:</p>
<figure class="highlight"><pre><code class="language-erb" data-lang="erb"><span class="nt"><title></span>НашаФирма - <span class="cp"><%=</span> <span class="n">content_for?</span><span class="p">(</span><span class="ss">:title</span><span class="p">)</span> <span class="p">?</span> <span class="k">yield</span><span class="p">(:</span><span class="n">title</span><span class="p">)</span> <span class="p">:</span> <span class="s1">'медицинские товары'</span> <span class="cp">%></span><span class="nt"></title></span></code></pre></figure>
<p>Итак, у нас предварительный кусок статики. Можно спереди, можно сзади его поместить - как хотите. Внутри ERB проверяется предоставлен ли
видом (в будущем! когда вид будет задействован) контент для title-области. Если да, выведем его. Если нет - по умолчанию выведем “медицинские товары” - тоже опциональный элемент.</p>
<p>Идем дальше. В ApplicationHelper создадим хелпер для удобства:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">module</span> <span class="nn">ApplicationHelper</span>
<span class="k">def</span> <span class="nf">title</span><span class="p">(</span><span class="n">page_title</span><span class="p">)</span>
<span class="n">content_for</span><span class="p">(</span><span class="ss">:title</span><span class="p">)</span> <span class="p">{</span> <span class="n">page_title</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Хелпер просто поможет сократить код в видах, чтобы постоянно там не писать content_for.</p>
<p>И наконец, в самих видах теперь мы можем указать тайтл явно, например поставив туда заголовок поста:</p>
<figure class="highlight"><pre><code class="language-erb" data-lang="erb"><span class="cp"><%</span> <span class="n">title</span> <span class="vi">@post</span><span class="p">.</span><span class="nf">title</span> <span class="cp">%></span></code></pre></figure>
<p>Все, “быстрое решение” готово. Но конечно если есть интернационализация, потребуется <a href="https://coderwall.com/p/a1pj7w/rails-page-titles-with-the-right-amount-of-magic">что-нибудь посложнее</a>, на ваш вкус.</p>Как-то раз зашла беседа с товарищем из мира yii2 о тайтлах. В yii2 тайтл устанавливается в самом виде. Это в принципе, нормальная практика (как-никак, title тег это часть ВИДА страницы).