<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>@junsenior</title><generator>teletype.in</generator><description><![CDATA[@junsenior]]></description><link>https://teletype.in/@junsenior?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=junsenior</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/junsenior?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/junsenior?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Mon, 05 Oct 2026 05:21:57 GMT</pubDate><lastBuildDate>Mon, 05 Oct 2026 05:21:57 GMT</lastBuildDate><item><guid isPermaLink="true">https://teletype.in/@junsenior/i1AQx7BZk</guid><link>https://teletype.in/@junsenior/i1AQx7BZk?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=junsenior</link><comments>https://teletype.in/@junsenior/i1AQx7BZk?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=junsenior#comments</comments><dc:creator>junsenior</dc:creator><title>Redux в 30 строк на PHP</title><pubDate>Thu, 18 Jun 2020 02:25:25 GMT</pubDate><description><![CDATA[<img src="https://teletype.in/files/5f/5b/5f5bcd2f-4a26-40a8-8171-49051aecb638.png"></img>Перевод статьи https://sorin.live/redux-in-50-lines-of-php/]]></description><content:encoded><![CDATA[
  <p><em>Перевод статьи <a href="https://sorin.live/redux-in-50-lines-of-php/" target="_blank">https://sorin.live/redux-in-50-lines-of-php/</a></em></p>
  <p>Официальный сайт называет Redux &quot;предсказуемый контейнер состояний для JS приложений&quot;. Основная польза от Redux в том, что он проливает свет на глобальное состояние приложения, позволяя тебе определить: когда, где, почему и как состояние твоего приложения было изменено.</p>
  <p>Мы убеждены, что работа с глобальным состоянием - это плохо. Среди всех проблем работы с глобальным состоянием наиболее серьёзные заключаются в том, что кто-то со стороны может изменить состояние приложения, и вы не сможете определить, кто это сделал.</p>
  <figure class="m_original">
    <img src="https://teletype.in/files/5f/5b/5f5bcd2f-4a26-40a8-8171-49051aecb638.png" width="259" />
  </figure>
  <p>Если вы первый раз встретились с Redux в контексте React Redux приложения, Redux будет казаться чем-то магическим.</p>
  <p>Так как я обычно стараюсь понять инструмент, который я использую, нижеизложенное - это попытка реализации Redux в PHP, в надежде приобрести глубокое понимание концепции, лежащей внутри Redux. Важно понимать, что мы будем реализовывать абстрактный Redux, просто чтобы понять логику его работы.</p>
  <p>Давайте начнём и объявим простое состояние, которым мы будем управлять:</p>
  <pre>$initialState = [
    &#x27;count&#x27; =&gt; [
        &#x27;count&#x27; =&gt; 1
    ],
];</pre>
  <p>Достаточно просто. Как говорится в Redux соглашении, мы не можем изменять состояние напрямую:</p>
  <pre>
$initialState[&#x27;count&#x27;][&#x27;count&#x27;] +=1 // так делать нельзя
</pre>
  <p>Но мы можем взаимодействовать с состоянием используя действия:</p>
  <pre>const INCREMENTACTION = &#x27;INCREMENT&#x27;;
const DECREMENTACTION = &#x27;DECREMENT&#x27;;

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

protected array $listeners = [];

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

namespace App\Sockets;</pre>
  <pre>use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class Chat implements MessageComponentInterface {
    protected $clients;

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

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

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

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

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

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

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

        $server-&gt;run();

    }
}

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

            &lt;/div&gt;

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

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


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

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

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

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


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

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

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

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


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

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


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

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

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

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

        conn.close();
    });

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

]]></content:encoded></item></channel></rss>