<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:tt="http://teletype.in/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"><title>@junsenior</title><author><name>@junsenior</name></author><id>https://teletype.in/atom/junsenior</id><link rel="self" type="application/atom+xml" href="https://teletype.in/atom/junsenior?offset=0"></link><link rel="alternate" type="text/html" href="https://teletype.in/@junsenior?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=junsenior"></link><link rel="next" type="application/rss+xml" href="https://teletype.in/atom/junsenior?offset=10"></link><link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></link><updated>2026-10-05T05:21:46.380Z</updated><entry><id>junsenior:i1AQx7BZk</id><link rel="alternate" type="text/html" href="https://teletype.in/@junsenior/i1AQx7BZk?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=junsenior"></link><title>Redux в 30 строк на PHP</title><published>2020-06-18T02:25:25.966Z</published><updated>2020-06-18T02:35:40.524Z</updated><summary type="html">&lt;img src=&quot;https://teletype.in/files/5f/5b/5f5bcd2f-4a26-40a8-8171-49051aecb638.png&quot;&gt;Перевод статьи https://sorin.live/redux-in-50-lines-of-php/</summary><content type="html">
  &lt;p&gt;&lt;em&gt;Перевод статьи &lt;a href=&quot;https://sorin.live/redux-in-50-lines-of-php/&quot; target=&quot;_blank&quot;&gt;https://sorin.live/redux-in-50-lines-of-php/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
  &lt;p&gt;Официальный сайт называет Redux &amp;quot;предсказуемый контейнер состояний для JS приложений&amp;quot;. Основная польза от Redux в том, что он проливает свет на глобальное состояние приложения, позволяя тебе определить: когда, где, почему и как состояние твоего приложения было изменено.&lt;/p&gt;
  &lt;p&gt;Мы убеждены, что работа с глобальным состоянием - это плохо. Среди всех проблем работы с глобальным состоянием наиболее серьёзные заключаются в том, что кто-то со стороны может изменить состояние приложения, и вы не сможете определить, кто это сделал.&lt;/p&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/5f/5b/5f5bcd2f-4a26-40a8-8171-49051aecb638.png&quot; width=&quot;259&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Если вы первый раз встретились с Redux в контексте React Redux приложения, Redux будет казаться чем-то магическим.&lt;/p&gt;
  &lt;p&gt;Так как я обычно стараюсь понять инструмент, который я использую, нижеизложенное - это попытка реализации Redux в PHP, в надежде приобрести глубокое понимание концепции, лежащей внутри Redux. Важно понимать, что мы будем реализовывать абстрактный Redux, просто чтобы понять логику его работы.&lt;/p&gt;
  &lt;p&gt;Давайте начнём и объявим простое состояние, которым мы будем управлять:&lt;/p&gt;
  &lt;pre&gt;$initialState = [
    &amp;#x27;count&amp;#x27; =&amp;gt; [
        &amp;#x27;count&amp;#x27; =&amp;gt; 1
    ],
];&lt;/pre&gt;
  &lt;p&gt;Достаточно просто. Как говорится в Redux соглашении, мы не можем изменять состояние напрямую:&lt;/p&gt;
  &lt;pre&gt;
$initialState[&amp;#x27;count&amp;#x27;][&amp;#x27;count&amp;#x27;] +=1 // так делать нельзя
&lt;/pre&gt;
  &lt;p&gt;Но мы можем взаимодействовать с состоянием используя действия:&lt;/p&gt;
  &lt;pre&gt;const INCREMENTACTION = &amp;#x27;INCREMENT&amp;#x27;;
const DECREMENTACTION = &amp;#x27;DECREMENT&amp;#x27;;

$actions = [
    &amp;#x27;increment&amp;#x27; =&amp;gt; [
        &amp;#x27;type&amp;#x27; =&amp;gt;  INCREMENTACTION
    ],
    &amp;#x27;decrement&amp;#x27; =&amp;gt; [
        &amp;#x27;type&amp;#x27; =&amp;gt; DECREMENTACTION
    ],
];&lt;/pre&gt;
  &lt;p&gt;Состояние и действия в нашем случае - это просто PHP массивы, никакой магии.&lt;/p&gt;
  &lt;p&gt;Нам так же нужен редьюсер - функция, которая берёт наше состояние, выполняет над ним действие и генерирует новое состояние:&lt;/p&gt;
  &lt;pre&gt;
function countReducer(array $state, $action) {
    switch($action[&amp;#x27;type&amp;#x27;]) {
        case INCREMENTACTION:
            return arrayreplace([], $state, [&amp;#x27;count&amp;#x27; =&amp;gt; $state[&amp;#x27;count&amp;#x27;] + 1]);
        case DECREMENT_AACTION:
            return arrayplace([], $state, [&amp;#x27;count&amp;#x27; =&amp;gt; $state[&amp;#x27;count&amp;#x27;] - 1]);
        default:
            return $state;
    }
}&lt;/pre&gt;
  &lt;p&gt;Важно понимать, что менять состояние могут только функции-редьюсеры.&lt;/p&gt;
  &lt;p&gt;Редьюсер генерирует массив с новым состоянием посредством инекремента/декремента предыдущего значения. Пока мы не будем разбираться, как редьюсеры получают состояние, сейчас просто примем, что countReducer получает значение в $initialState[&amp;#x27;count&amp;#x27;]:&lt;/p&gt;
  &lt;pre&gt;
[
	&amp;#x27;count&amp;#x27; =&amp;gt; 1
]&lt;/pre&gt;
  &lt;p&gt;Закончим с абстрактыми примерами и приступим к практике.&lt;/p&gt;
  &lt;p&gt;Если мы посмотрим на API Redux, то начать следует с метода createStore. После его реализации мы сможем реализовать методы dispatch и subscribe, для отправки и подписки на изменение состояния хранилища.&lt;/p&gt;
  &lt;p&gt;Транслируя это в PHP, давайте создадим класс Store:&lt;/p&gt;
  &lt;pre&gt;
class Store 
{
    protected array $state;
    protected Closure $reducer;
    public function __construct(callable $reducer, array $initialState)
    {
        $this-&amp;gt;state = $initialState;
        $this-&amp;gt;reducer = Closure::fromCallable($reducer);
    }
}&lt;/pre&gt;
  &lt;p&gt;Класс принимает начальное состояние и функцию редьюсер, которая может это состояние изменять. В Redux вы можете комбинировать множество редьюсеров в одном и передавать комбинированный результат в метод createStore, позже мы обсудим, как это можно сделать.&lt;/p&gt;
  &lt;p&gt;Отлично, как быть с отправкой и подпиской на события? Даайте начнём с добавления метода для получения состояния хранилища, а затем реализуем метод подписки:&lt;/p&gt;
  &lt;pre&gt;

protected array $listeners = [];

public function getState()
{
	return $this-&amp;gt;state;
}

public function subscribe(callable $listener)
{
	$this-&amp;gt;listeners[] = Closure::fromCallable($listener);
}
&lt;/pre&gt;
  &lt;p&gt;Подписка на хранилище подразумевает добавление callback-функции, которая будет вызвана когда состояние обновится. Как использовать этот метод?&lt;/p&gt;
  &lt;pre&gt;
$store-&amp;gt;subscribe(function($state) {
	print_n($state);
});
&lt;/pre&gt;
  &lt;p&gt;Теперь о том, как  инициировать событие:&lt;/p&gt;
  &lt;pre&gt;
public function dispatch(array $action)
{
    $this-&amp;gt;state = ($this-&amp;gt;reducer)($this-&amp;gt;getState(), $action);
    foreach($this-&amp;gt;listeners as $listener) {
    	$listener($this-&amp;gt;getState());
    }
}&lt;/pre&gt;
  &lt;p&gt;Для отправки события нам нужно сгенерировать новое состояние, вызвав редьюсер, а затем уведомить всех подписанных слушателей.&lt;/p&gt;
  &lt;p&gt;Круто. Нам нужно сделать что-то ещё? Нет, этого достаточно. Мы реализовали базовую версию Redux в PHP. Теперь о том, как это использовать:&lt;/p&gt;
  &lt;pre&gt;
&amp;lt;?php

class Store
{
    protected array $state;
    protected Closure $reducer;
    protected array $listeners = [];
    
    public function __construct( callable $reducer, array $initialState)
    {
        $this-&amp;gt;state = $initialState;
        $this-&amp;gt;reducer = Closure::fromCallable($reducer);
    }
    
    public function getState()
    {
        return $this-&amp;gt;state;
    }
    
    public function subscribe(callable $listener)
    {
        $this-&amp;gt;listeners[] = Closure::fromCallable($listener);
    }
    
    public function dispatch(array $action)
    {
        $this-&amp;gt;state = ($this-&amp;gt;reducer)($this-&amp;gt;getState(), $action);
        foreach($this-&amp;gt;listeners as $listener) {
            $listener($this-&amp;gt;getState());
        }
    }
}

$initialState = [
    &amp;#x27;count&amp;#x27; =&amp;gt; [
        &amp;#x27;count&amp;#x27; =&amp;gt; 1
    ],
];

const INCREMENT_ACTION = &amp;#x27;INCREMENT&amp;#x27;;
const DECREMENT_ACTION = &amp;#x27;DECREMENT&amp;#x27;;

$actions = [
    &amp;#x27;increment&amp;#x27; =&amp;gt; [
        &amp;#x27;type&amp;#x27; =&amp;gt;  INCREMENT_ACTION
    ],
    &amp;#x27;decrement&amp;#x27; =&amp;gt; [
        &amp;#x27;type&amp;#x27; =&amp;gt; DECREMENT_ACTION
    ],
];

function countReducer(array $state, $action) {
    switch($action[&amp;#x27;type&amp;#x27;]) {
        case INCREMENT_ACTION:
            return array_replace([], $state, [&amp;#x27;count&amp;#x27; =&amp;gt; $state[&amp;#x27;count&amp;#x27;] + 1]);
        case DECREMENT_ACTION:
            return array_replace([], $state, [&amp;#x27;count&amp;#x27; =&amp;gt; $state[&amp;#x27;count&amp;#x27;] - 1]);
        default:
            return $state;
    }
}

$store = new Store(&amp;#x27;countReducer&amp;#x27;, $initialState[&amp;#x27;count&amp;#x27;]);

$store-&amp;gt;subscribe(function($state) {
    print_r($state);
});

$store-&amp;gt;dispatch($actions[&amp;#x27;increment&amp;#x27;]);
$store-&amp;gt;dispatch($actions[&amp;#x27;increment&amp;#x27;]);
$store-&amp;gt;dispatch($actions[&amp;#x27;increment&amp;#x27;]);
$store-&amp;gt;dispatch($actions[&amp;#x27;decrement&amp;#x27;]);
&lt;/pre&gt;
  &lt;p&gt;Вывод будет следующим:&lt;/p&gt;
  &lt;pre&gt;
Array
(
    [count] =&amp;gt; 2
)
Array
(
    [count] =&amp;gt; 3
)
Array
(
    [count] =&amp;gt; 4
)
Array
(
    [count] =&amp;gt; 3
)
&lt;/pre&gt;
  &lt;p&gt;Вы так же можете добавлять кастомные данные к действию и изменить редьюсер так, чтобы учитывать эти данные:&lt;/p&gt;
  &lt;pre&gt;
const INCREMENTBYACTION = &amp;#x27;INCREMENT_BY&amp;#x27;;

function countReducer(array $state, $action) {
    switch($action[&amp;#x27;type&amp;#x27;]) {
        case INCREMENT_BY_ACTION;
            $value = $action[&amp;#x27;value&amp;#x27;] ?? 0;
            return array_replace([], $state, [&amp;#x27;count&amp;#x27; =&amp;gt; $state[&amp;#x27;count&amp;#x27;] + $value]);
        case INCREMENT_ACTION:
            return array_replace([], $state, [&amp;#x27;count&amp;#x27; =&amp;gt; $state[&amp;#x27;count&amp;#x27;] + 1]);
        case DECREMENT_ACTION:
            return array_replace([], $state, [&amp;#x27;count&amp;#x27; =&amp;gt; $state[&amp;#x27;count&amp;#x27;] - 1]);
        default:
            return $state;
    }
}

$store-&amp;gt;dispatch([&amp;#x27;type&amp;#x27; =&amp;gt; INCREMENTBYACTION, &amp;#x27;value&amp;#x27; =&amp;gt; 5]);&lt;/pre&gt;
  &lt;p&gt;Прекрасно. Но есть одна проблема - наше хранилище сейчас может работать только с одним редьюсером. Это также относится и к функции createStore в Redux. Способ, которым можно решить эту проблему - реализовать метод combReducers:&lt;/p&gt;
  &lt;pre&gt;

rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer});
&lt;/pre&gt;
  &lt;p&gt;Этот метод принимает список редьюсеров и возвращает редьюсер, который комбинирует все полученные. Как это сделать на PHP?&lt;/p&gt;
  &lt;pre&gt;
function combineReducers(array $reducers) {
    return function(array $state, $action) use ($reducers) {
        $newState = $state;
        foreach($reducers as $stateKey =&amp;gt; $reducer) {
            if(!isset($newState[$stateKey])) {
                $newState[$stateKey] = [];
            }
            $newState[$stateKey] = $reducer($newState[$stateKey], $action);
        }
        
        return $newState;
    };
}
&lt;/pre&gt;
  &lt;p&gt;Использовать это можно так:&lt;br /&gt;&lt;/p&gt;
  &lt;pre&gt;
$initialState = [
    &amp;#x27;count&amp;#x27; =&amp;gt; [
        &amp;#x27;count&amp;#x27; =&amp;gt; 1
    ],
    &amp;#x27;sum&amp;#x27; =&amp;gt; 0
];

function sumReducer($state, $action) {
    switch($action[&amp;#x27;type&amp;#x27;]) {
        case ADD_SUM:
            return $state + 1;
        default:
            return $state;
    }

$store = new Store(combineReducers([
    &amp;#x27;count&amp;#x27; =&amp;gt; &amp;#x27;countReducer&amp;#x27;,
    &amp;#x27;sum&amp;#x27; =&amp;gt; &amp;#x27;sumReducer&amp;#x27;
]), $initialState);
&lt;/pre&gt;
  &lt;p&gt;Вот мы и написали нашу реализацию Redux. Я надеюсь, что несмотря на язык реализации, эта статья помогла вам пролить свет на то, как Redux работает под капотом. &lt;/p&gt;

</content></entry><entry><id>junsenior:gUBsCvOlj</id><link rel="alternate" type="text/html" href="https://teletype.in/@junsenior/gUBsCvOlj?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=junsenior"></link><title>Трюки в PHPStorm в 9 гифках</title><published>2020-06-11T04:38:53.866Z</published><updated>2020-06-11T04:38:53.866Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://teletype.in/files/3a/4b/3a4bbc76-fe4d-4be9-ad79-b9a3da2d8b72.png"></media:thumbnail><summary type="html">&lt;img src=&quot;https://teletype.in/files/8d/7f/8d7f2115-c1a1-4995-940f-028b26dc701b.gif&quot;&gt;Вводите один и тот же код, зависящий от переменной, миллион раз?</summary><content type="html">
  &lt;h3&gt;Добавление свойства через конструктор:&lt;/h3&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/8d/7f/8d7f2115-c1a1-4995-940f-028b26dc701b.gif&quot; width=&quot;650&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3&gt;Post-Fix&lt;/h3&gt;
  &lt;p&gt;Вводите один и тот же код, зависящий от переменной, миллион раз?&lt;/p&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/a8/77/a8777f7b-76ec-4726-a32f-3c01102f3902.gif&quot; width=&quot;650&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3&gt;Забудь $this-&amp;gt;&lt;/h3&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/a4/6b/a46b8144-0c26-4877-9ca2-93bd5b96cfc3.gif&quot; width=&quot;650&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3&gt;Переход от реализации к определению и назад&lt;/h3&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/10/e5/10e5d7b8-7d64-427d-a389-39eedd67d352.gif&quot; width=&quot;650&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3&gt;Создание нового Live Template&lt;/h3&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/65/50/655083b4-ef50-43b1-a8a8-c58c1a20c9cb.gif&quot; width=&quot;650&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3&gt;Быстрая навигация по методам в файле&lt;/h3&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/9c/29/9c296d46-f33e-42ce-8cb7-f277cb8e5de9.gif&quot; width=&quot;650&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3&gt;Копируем путь до класса или File:line&lt;/h3&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/10/ac/10ac8465-c0be-48a8-a064-f6452d946390.gif&quot; width=&quot;650&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3&gt;Навигая по недавним файлам&lt;/h3&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/0a/52/0a52205b-bbb3-4585-a63a-21df4736f389.gif&quot; width=&quot;650&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3&gt;Создание шортката&lt;/h3&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/66/35/66350df2-9399-4d0c-ae4a-5f81eab9b22b.gif&quot; width=&quot;650&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Оригинал - &lt;a href=&quot;https://www.tomasvotruba.com/blog/2018/12/13/kirill-smelov-s-phpstorm-tips-in-9-gifs/&quot; target=&quot;_blank&quot;&gt;https://www.tomasvotruba.com/blog/2018/12/13/kirill-smelov-s-phpstorm-tips-in-9-gifs/&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>junsenior:SkwFqtdES</id><link rel="alternate" type="text/html" href="https://teletype.in/@junsenior/SkwFqtdES?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=junsenior"></link><title>Пишем чат на web-сокетах в Symfony приложении</title><published>2019-08-19T20:45:51.030Z</published><updated>2019-08-19T20:45:51.030Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://teletype.in/files/1c/1c889c91-151f-45df-b194-2b3ee8ec21f3.png"></media:thumbnail><summary type="html">&lt;img src=&quot;https://teletype.in/files/1a/1a0e0e41-05cb-4b1a-bf7b-b014ee4155ce.png&quot;&gt;Первое, что нужно нам сделать это установить Docker, Docker-compose,Composer (если не установлены).</summary><content type="html">
  &lt;p&gt;Первое, что нужно нам сделать это установить Docker, Docker-compose,Composer (если не установлены).&lt;/p&gt;
  &lt;p&gt;Описывать как это сделать особого смысла не имеет, потому что статья не об этом. Посмотреть, как установить именно на вашу ОС это ПО можно тут&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;Docker:&lt;/li&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://docs.docker.com/install/&quot; target=&quot;_blank&quot;&gt;https://docs.docker.com/install/&lt;/a&gt; — слева, в меню выберите свою ОС и следуйте инструкции по установке.&lt;/li&gt;
    &lt;/ul&gt;
    &lt;li&gt;Docker-compose:&lt;/li&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://docs.docker.com/compose/install/&quot; target=&quot;_blank&quot;&gt;https://docs.docker.com/compose/install/&lt;/a&gt; — аналогично, выбираем свою ОС и следуем инструкции&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/ul&gt;
  &lt;p&gt;Composer: &lt;/p&gt;
  &lt;ul&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://getcomposer.org/doc/00-intro.md&quot; target=&quot;_blank&quot;&gt;https://getcomposer.org/doc/00-intro.md&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/ul&gt;
  &lt;p&gt;После того, как всё было установлено, сделайте небольшую проверку:&lt;/p&gt;
  &lt;pre&gt;docker -v
Docker version 18.09.7, build 2d0083d&lt;/pre&gt;
  &lt;pre&gt;docker-compose -v
docker-compose version 1.24.1, build 4667896b&lt;/pre&gt;
  &lt;pre&gt;composer -v
   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ &amp;#x60;__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 1.9.0 2019-08-02 20:55:32
&lt;/pre&gt;
  &lt;p&gt;&lt;/p&gt;
  &lt;p&gt;Вы должны увидеть плюс-минус (версии могут отличаться) тоже самое.&lt;/p&gt;
  &lt;p&gt;Ок-с, после того как необходимое ПО установлено, развернём пустой Symfony проект.&lt;/p&gt;
  &lt;pre&gt;mkdir projects
cd projects
composer create-project symfony/skeleton chat&lt;/pre&gt;
  &lt;p&gt;После этого, в консоли, вы увидите установку пустого приложения symfony. Как установка закончится, может смело открывать эту папку в своём IDE (в моём случае это PhpStorm).&lt;/p&gt;
  &lt;p&gt;Вы должны увидеть плюс-минус следующее:&lt;/p&gt;
  &lt;figure class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/1a/1a0e0e41-05cb-4b1a-bf7b-b014ee4155ce.png&quot; width=&quot;926.5&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;После этого, в основной папке проекта создаём папку docker, содержимое которой берём отсюда: &lt;a href=&quot;https://github.com/ko4a/dockerFiles&quot; target=&quot;_blank&quot;&gt;https://github.com/ko4a/dockerFile&lt;br /&gt;&lt;/a&gt;&lt;/p&gt;
  &lt;p&gt;&lt;strong&gt;КРОМЕ ФАЙЛА docker-compose.yml&lt;/strong&gt;&lt;/p&gt;
  &lt;p&gt;После этого, в основной папке проекта создаём файл: docker-compose.yml, содержимое которого берём c файла docker-compose.yml из репозитория.&lt;/p&gt;
  &lt;p&gt;Ок, теперь разворачиваем наши контейнеры.&lt;/p&gt;
  &lt;pre&gt;docker-compose build&lt;/pre&gt;
  &lt;p&gt;Начнётся сборка, как закончится:&lt;br /&gt;&lt;code&gt;docker-compose up -d&lt;br /&gt;&lt;/code&gt;Поздравляю! Вы развернули своё Symfony приложение из под docker. &lt;/p&gt;
  &lt;p&gt;Сделаем небольшую проверку&lt;/p&gt;
  &lt;pre&gt;docker-compose ps
    Name                  Command              State                 Ports               
chat           docker-php-entrypoint php-fpm   Up      0.0.0.0:8000-&amp;gt;10000/tcp, 9000/tcp 
chat_nginx_1   nginx                           Up      0.0.0.0:8080-&amp;gt;80/tcp              
lesson-mysql   docker-entrypoint.sh mysqld     Up      0.0.0.0:33061-&amp;gt;3306/tcp, 33060/tcp
php            docker-php-entrypoint php-fpm   Up      0.0.0.0:9001-&amp;gt;9000/tcp    &lt;/pre&gt;
  &lt;p&gt;&lt;/p&gt;
  &lt;p&gt;Добавим алиасы.&lt;/p&gt;
  &lt;pre&gt;alias sf=&amp;quot;docker-compose exec --user www-data php php -d memory_limit=4096M bin/console&amp;quot;&lt;/pre&gt;
  &lt;pre&gt;alias ch=&amp;quot;docker-compose exec --user www-data chat php -d memory_limit=4096M bin/console&amp;quot;&lt;/pre&gt;
  &lt;p&gt;Этими алиасами мы будем пользоваться вместо стандартного php bin/console, по сути, они делают тоже самое, только из под контейнера.&lt;/p&gt;
  &lt;p&gt; Теперь добавим maker bundle и библиотеку Ratchet:&lt;/p&gt;
  &lt;pre&gt;docker-compose exec php composer require symfony/maker-bundle --dev
docker-compose exec php composer require cboden/ratchet --dev&lt;/pre&gt;
  &lt;p&gt;Окей, теперь наконец перейдём к написанию самого чата. Для этого, в папке src, создайте папку Sockets, а внутри класс Chat.&lt;/p&gt;
  &lt;p&gt;Этот класс должен реализовывать интерфейс &lt;u&gt;MessageComponentInterface&lt;/u&gt;, который находится в пространстве имён Ratchet.&lt;/p&gt;
  &lt;p&gt;Вот моя реализация:&lt;/p&gt;
  &lt;pre&gt;&amp;lt;?php

namespace App\Sockets;&lt;/pre&gt;
  &lt;pre&gt;use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class Chat implements MessageComponentInterface {
    protected $clients;

    public function __construct() {
        $this-&amp;gt;clients = new \SplObjectStorage;
    }

    public function onOpen(ConnectionInterface $conn) {
        $this-&amp;gt;clients-&amp;gt;attach($conn);
    }

    public function onMessage(ConnectionInterface $from, $msg) {
       foreach ($this-&amp;gt;clients as $client) {
            if ($from !== $client) {
                $client-&amp;gt;send($msg);
            }
        }
    }

    public function onClose(ConnectionInterface $conn) {
        $this-&amp;gt;clients-&amp;gt;detach($conn);
        echo &amp;quot;Connection {$conn-&amp;gt;resourceId} has disconnected\n&amp;quot;;
    }

    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo &amp;quot;An error has occurred: {$e-&amp;gt;getMessage()}\n&amp;quot;;
        $conn-&amp;gt;close();
    }
}&lt;/pre&gt;
  &lt;p&gt;В этом классе, мы описали что будет делать сервер на каждое из соответствующих событий. &lt;/p&gt;
  &lt;p&gt;На открытие сокета на клиенте (метод onOpen) мы будем сохранять соединение в хранилище.&lt;/p&gt;
  &lt;p&gt;При получении сообщения(метод onMessage) мы будем отсылать сообщение на все соединения, кроме своего. &lt;/p&gt;
  &lt;p&gt;При закрытии сокета на клиенте (метод onClose) мы будем убирать соединение из хранилища.&lt;/p&gt;
  &lt;p&gt;При возникновении ошибки (метод onError) мы будем закрывать соответствующее соединение.&lt;/p&gt;
  &lt;p&gt;Теперь нам нужна команда, которая будет запускать чатик с сокетами. &lt;/p&gt;
  &lt;pre&gt; sf make:command&lt;/pre&gt;
  &lt;pre&gt; Choose a command name (e.g. app:tiny-chef):
 &amp;gt; ChatCommand&lt;/pre&gt;
  &lt;pre&gt; created: src/Command/ChatCommand.php           
  Success! &lt;/pre&gt;
  &lt;p&gt;Чтобы сильно не тянуть, сразу покажу свою реализацию:&lt;/p&gt;
  &lt;pre&gt;&amp;lt;?php

namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;

use App\Sockets\Chat;


class ChatCommand extends Command
{
    protected function configure()
    {
        $this
            -&amp;gt;setName(&amp;#x27;sockets:start-chat&amp;#x27;)
            -&amp;gt;setDescription(&amp;#x27;Starts the socket chat demo&amp;#x27;)
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output-&amp;gt;writeln([
            &amp;#x27;Chat socket&amp;#x27;,
            &amp;#x27;============&amp;#x27;,
            &amp;#x27;Starting chat, open your browser.&amp;#x27;,
        ]);

        $server = IoServer::factory(
            new HttpServer(
                new WsServer(
                    new Chat()
                )
            ),
            10000
        );

        $server-&amp;gt;run();

    }
}

           
&lt;/pre&gt;
  &lt;p&gt;Каждая команда состоит как минимум из двух методов. &lt;/p&gt;
  &lt;p&gt;Первый - configure, задаёт конфигурацию. В нашем случае - я задал имя команды, описание. Имя нужно для того, чтобы обращаться к команде из консоли, а описание - это то, что будет стоять справа от имени команды в списке всех команд ( ради интереса напишите в консоль sf и найдите свою команду).&lt;/p&gt;
  &lt;p&gt;Второй - execute, само действие, которое будет делать команда. В нашем случае оно создаёт и запускает наш сервер, на 10000 порту.&lt;/p&gt;
  &lt;p&gt;Попробуйте запустить команду, ведь серверная часть нашего чатика почти готова ( нужен будет ещё контроллер, который возвращает страничку с нашим чатом).&lt;/p&gt;
  &lt;p&gt;Перед тем, как создавать контроллер опишем нашу страничку. Для этого создадим папку templates в папке с проектом. В ней создадим файл base.html.twig&lt;/p&gt;
  &lt;p&gt;Это наша базовая страница (если вы знакомы с наследованием в ООП, то тут это работает плюс минус также, так что &lt;strong&gt;если вы вдруг соберётесь расширять это приложение, то оставьте на базовой странице только то, что нужно на ВСЕХ других страницах&lt;/strong&gt;). Со следующим содержимым:&lt;/p&gt;
  &lt;pre&gt;    &amp;lt;!DOCTYPE html&amp;gt;
    &amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;
        &amp;lt;title&amp;gt;
            {% block title %}Welcome!{% endblock %}
        &amp;lt;/title&amp;gt;
        {% block stylesheets %}
            &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ asset(&amp;#x27;css/chat.css&amp;#x27;) }}&amp;quot;&amp;gt;
        {% endblock %}
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
        {% block body %}{% endblock %}
    {% block javascripts %}
        &amp;lt;script src=&amp;quot;https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
        &amp;lt;script src=&amp;quot;https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/3.0.3/handlebars.min.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
        &amp;lt;script src=&amp;quot;http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.2/moment.min.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
        &amp;lt;script src=&amp;quot;{{ asset(&amp;#x27;js/chat.js&amp;#x27;) }}&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    {% endblock %}
    &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;&lt;/pre&gt;
  &lt;p&gt;Здесь мы подключили наши (пока ещё не написанные стили), и js - скрипты. Первые три - это библиотеки. Jquery, handlebars,moment. О каждой из них вам лучше почитать отдельно, ведь я не js разработчик.&lt;/p&gt;
  &lt;p&gt;Если вкратце: &lt;/p&gt;
  &lt;ul&gt;
    &lt;ul&gt;
      &lt;li&gt;Jquery - позволяет нам довольно просто работать с DOM. &lt;/li&gt;
      &lt;li&gt;Handlebars— это клиентский шаблонизатор для JavaScript.&lt;/li&gt;
      &lt;li&gt;moment - позволяет легко работать со временем.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/ul&gt;
  &lt;p&gt;Теперь создадим нашу страничку с чатом. В той же папке (templates) создайте папку chat, и в ней файл index.html.twig со следующим содержимым:&lt;/p&gt;
  &lt;pre&gt;{% extends&amp;#x27;base.html.twig&amp;#x27; %}
    {% block title %} chatapp {% endblock %}
{% block body %}
    &amp;lt;div id=&amp;quot;wrapper&amp;quot;&amp;gt;
        &amp;lt;div id=&amp;quot;user-container&amp;quot;&amp;gt;
            &amp;lt;label for=&amp;quot;user&amp;quot;&amp;gt;What&amp;#x27;s your name?&amp;lt;/label&amp;gt;
            &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;user&amp;quot; name=&amp;quot;user&amp;quot;&amp;gt;
            &amp;lt;button type=&amp;quot;button&amp;quot; id=&amp;quot;join-chat&amp;quot;&amp;gt;Join Chat&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div id=&amp;quot;main-container&amp;quot; class=&amp;quot;hidden&amp;quot;&amp;gt;
            &amp;lt;button type=&amp;quot;button&amp;quot; id=&amp;quot;leave-room&amp;quot;&amp;gt;Leave&amp;lt;/button&amp;gt;
            &amp;lt;div id=&amp;quot;messages&amp;quot;&amp;gt;

            &amp;lt;/div&amp;gt;

            &amp;lt;div id=&amp;quot;msg-container&amp;quot;&amp;gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;msg&amp;quot; name=&amp;quot;msg&amp;quot;&amp;gt;
                &amp;lt;button type=&amp;quot;button&amp;quot; id=&amp;quot;send-msg&amp;quot;&amp;gt;Send&amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

    &amp;lt;/div&amp;gt;
    {% verbatim %}
    &amp;lt;script id=&amp;quot;messages-template&amp;quot; type=&amp;quot;text/x-handlebars-template&amp;quot;&amp;gt;
        {{#each messages}}
        &amp;lt;div class=&amp;quot;msg&amp;quot;&amp;gt;
            &amp;lt;div class=&amp;quot;time&amp;quot;&amp;gt;{{time}}&amp;lt;/div&amp;gt;
            &amp;lt;div class=&amp;quot;details&amp;quot;&amp;gt;
                &amp;lt;span class=&amp;quot;user&amp;quot;&amp;gt;{{user}}&amp;lt;/span&amp;gt;: &amp;lt;span class=&amp;quot;text&amp;quot;&amp;gt;{{text}}&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        {{/each}}
    &amp;lt;/script&amp;gt;
    {% endverbatim %}
{% endblock %}
&lt;/pre&gt;
  &lt;p&gt;&lt;/p&gt;
  &lt;p&gt;Здесь комментировать особо нечего, кроме twig - блока {% verbatim %}, это блок, который НЕ будет отображаться на странице.&lt;/p&gt;
  &lt;p&gt;Ок-с, теперь в папке проекта public создайте папку css, а в ней файл chat.css&lt;/p&gt;
  &lt;p&gt;Можете стилизовать по своему вкусу, я предложу это (довольно минималистичненько). &lt;/p&gt;
  &lt;pre&gt;.hidden {
    display: none;
}

#wrapper {
    width: 800px;
    margin: 0 auto;
}

#leave-room {
    margin-bottom: 10px;
    float: right;
}

#user-container {
    width: 500px;
    margin: 0 auto;
    text-align: center;
}

#main-container {
    width: 500px;
    margin: 0 auto;
}

#messages {
    height: 300px;
    width: 500px;
    border: 1px solid #ccc;
    padding: 20px;
    text-align: left;
    overflow-y: scroll;
}

#msg-container {
    padding: 20px;
}

#msg {
    width: 400px;
}

.user {
    font-weight: bold;
}

.msg {
    margin-bottom: 10px;
    overflow: hidden;
}

.time {
    float: right;
    color: #939393;
    font-size: 13px;
}

.details {
    margin-top: 20px;
}
&lt;/pre&gt;
  &lt;p&gt;Отлично, теперь JS. В папке public создайте папку js и в ней файл chat.js&lt;/p&gt;
  &lt;p&gt;Наполните его следующим.&lt;/p&gt;
  &lt;pre&gt;(function(){


    var user;
    var messages = [];
    var messages_template = Handlebars.compile($(&amp;#x27;#messages-template&amp;#x27;).html());

    function updateMessages(msg){
        messages.push(msg);
        var messages_html = messages_template({&amp;#x27;messages&amp;#x27;: messages});
        $(&amp;#x27;#messages&amp;#x27;).html(messages_html);
        $(&amp;quot;#messages&amp;quot;).animate({ scrollTop: $(&amp;#x27;#messages&amp;#x27;)[0].scrollHeight}, 1);
    }

    var conn = new WebSocket(&amp;#x27;ws://localhost:8000&amp;#x27;);
    conn.onopen = function(e) {
        console.log(&amp;quot;Connection established!&amp;quot;);
    };

    conn.onmessage = function(e) {
        var msg = JSON.parse(e.data);
        updateMessages(msg);
    };


    $(&amp;#x27;#join-chat&amp;#x27;).click(function(){
        user = $(&amp;#x27;#user&amp;#x27;).val();
        $(&amp;#x27;#user-container&amp;#x27;).addClass(&amp;#x27;hidden&amp;#x27;);
        $(&amp;#x27;#main-container&amp;#x27;).removeClass(&amp;#x27;hidden&amp;#x27;);

        var msg = {
            &amp;#x27;user&amp;#x27;: user,
            &amp;#x27;text&amp;#x27;: user + &amp;#x27; entered the room&amp;#x27;,
            &amp;#x27;time&amp;#x27;: moment().format(&amp;#x27;hh:mm a&amp;#x27;)
        };

        updateMessages(msg);
        conn.send(JSON.stringify(msg));

        $(&amp;#x27;#user&amp;#x27;).val(&amp;#x27;&amp;#x27;);
    });


    $(&amp;#x27;#send-msg&amp;#x27;).click(function(){
        var text = $(&amp;#x27;#msg&amp;#x27;).val();
        var msg = {
            &amp;#x27;user&amp;#x27;: user,
            &amp;#x27;text&amp;#x27;: text,
            &amp;#x27;time&amp;#x27;: moment().format(&amp;#x27;hh:mm a&amp;#x27;)
        };
        updateMessages(msg);
        conn.send(JSON.stringify(msg));

        $(&amp;#x27;#msg&amp;#x27;).val(&amp;#x27;&amp;#x27;);
    });


    $(&amp;#x27;#leave-room&amp;#x27;).click(function(){

        var msg = {
            &amp;#x27;user&amp;#x27;: user,
            &amp;#x27;text&amp;#x27;: user + &amp;#x27; has left the room&amp;#x27;,
            &amp;#x27;time&amp;#x27;: moment().format(&amp;#x27;hh:mm a&amp;#x27;)
        };
        updateMessages(msg);
        conn.send(JSON.stringify(msg));

        $(&amp;#x27;#messages&amp;#x27;).html(&amp;#x27;&amp;#x27;);
        messages = [];

        $(&amp;#x27;#main-container&amp;#x27;).addClass(&amp;#x27;hidden&amp;#x27;);
        $(&amp;#x27;#user-container&amp;#x27;).removeClass(&amp;#x27;hidden&amp;#x27;);

        conn.close();
    });

})();
&lt;/pre&gt;
  &lt;p&gt;&lt;strong&gt;ГЛАВНОЕ ЗДЕСЬ: &lt;/strong&gt;создаём WebSocket с нашим url-ом и портом.&lt;/p&gt;
  &lt;p&gt;Дальше вешаем обработчики на соответствующие события у этого объекта. Всё остальное здесь - вкусовщина, не так важная для понимания того, как это работает.&lt;/p&gt;
  &lt;p&gt;Ок-с, последний штрих. Создаём контроллер, который будет возвращать нам нашу страничку.&lt;/p&gt;
  &lt;p&gt;Для этого: &lt;/p&gt;
  &lt;pre&gt;docker-compose exec php composer require annotations&lt;/pre&gt;
  &lt;p&gt;После установки бандла:&lt;/p&gt;
  &lt;pre&gt;sf make:controller &lt;/pre&gt;
  &lt;pre&gt;Choose a name for your controller class (e.g. FierceGnomeController):&lt;/pre&gt;
  &lt;pre&gt;&amp;gt; ChatController &lt;/pre&gt;
  &lt;pre&gt;created:  src/Controller/ChatController.php          
  Success! 
           
&lt;/pre&gt;
  &lt;p&gt;Переходим в наш контроллер, и чуть -чуть переделываем метод index, чтобы он просто рендерил нашу страничку.&lt;/p&gt;
  &lt;pre&gt;    /**
     * @Route(&amp;quot;/chat&amp;quot;, name=&amp;quot;chat&amp;quot;)
     */
    public function index()
    {
        return $this-&amp;gt;render(&amp;#x27;chat/index.html.twig&amp;#x27;);
    }&lt;/pre&gt;
  &lt;p&gt;После этого устанавливаем ещё кое-какие компоненты symfony.&lt;/p&gt;
  &lt;pre&gt;docker-compose exec php composer require symfony/twig-bundle
docker-compose exec php composer require symfony/asset
&lt;/pre&gt;
  &lt;p&gt;и, наконец, мы можем запускать наш чат.&lt;/p&gt;
  &lt;pre&gt;ch sockets:start-chat
&lt;/pre&gt;
  &lt;p&gt;Переходим в браузер по следующему адресу: 0.0.0.0:8080/chat&lt;/p&gt;
  &lt;p&gt;и, переписываемся сколько нам влезет.&lt;/p&gt;
  &lt;figure class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/b5/b5709945-dae8-4a79-afb0-cb5212fe3ca4.png&quot; width=&quot;960&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Важно заметить, что в принципе&lt;strong&gt; вебсокеты на PHP это плохая идея&lt;/strong&gt;. Если вам нужны сокеты, то лучше использовать другой инструмент: Node.js / Python / C# что-то из этого.&lt;/p&gt;
  &lt;p&gt;Если у вас вдруг что-то не получилось, то можете посмотреть это приложение в гите: &lt;/p&gt;
  &lt;p&gt;&lt;a href=&quot;https://github.com/ko4a/simpleChatSymfony&quot; target=&quot;_blank&quot;&gt;https://github.com/ko4a/simpleChatSymfony&lt;/a&gt;&lt;/p&gt;

</content></entry></feed>