Первое, что нужно нам сделать это установить Docker, Docker-compose,Composer (если не установлены).
Описывать как это сделать особого смысла не имеет, потому что статья не об этом. Посмотреть, как установить именно на вашу ОС это ПО можно тут
- Docker:
- https://docs.docker.com/install/ — слева, в меню выберите свою ОС и следуйте инструкции по установке.
- Docker-compose:
- https://docs.docker.com/compose/install/ — аналогично, выбираем свою ОС и следуем инструкции
Composer:
После того, как всё было установлено, сделайте небольшую проверку:
docker -v
Docker version 18.09.7, build 2d0083d
docker-compose -v
docker-compose version 1.24.1, build 4667896b
composer -v
______
/ ____/___ ____ ___ ____ ____ ________ _____
/ / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
/_/
Composer version 1.9.0 2019-08-02 20:55:32
Вы должны увидеть плюс-минус (версии могут отличаться) тоже самое.
Ок-с, после того как необходимое ПО установлено, развернём пустой Symfony проект.
mkdir projects
cd projects
composer create-project symfony/skeleton chat
После этого, в консоли, вы увидите установку пустого приложения symfony. Как установка закончится, может смело открывать эту папку в своём IDE (в моём случае это PhpStorm).
Вы должны увидеть плюс-минус следующее:

После этого, в основной папке проекта создаём папку docker, содержимое которой берём отсюда: https://github.com/ko4a/dockerFile
КРОМЕ ФАЙЛА docker-compose.yml
После этого, в основной папке проекта создаём файл: docker-compose.yml, содержимое которого берём c файла docker-compose.yml из репозитория.
Ок, теперь разворачиваем наши контейнеры.
docker-compose build
Начнётся сборка, как закончится:docker-compose up -d
Поздравляю! Вы развернули своё Symfony приложение из под docker.
Сделаем небольшую проверку
docker-compose ps
Name Command State Ports
chat docker-php-entrypoint php-fpm Up 0.0.0.0:8000->10000/tcp, 9000/tcp
chat_nginx_1 nginx Up 0.0.0.0:8080->80/tcp
lesson-mysql docker-entrypoint.sh mysqld Up 0.0.0.0:33061->3306/tcp, 33060/tcp
php docker-php-entrypoint php-fpm Up 0.0.0.0:9001->9000/tcp
Добавим алиасы.
alias sf="docker-compose exec --user www-data php php -d memory_limit=4096M bin/console"
alias ch="docker-compose exec --user www-data chat php -d memory_limit=4096M bin/console"
Этими алиасами мы будем пользоваться вместо стандартного php bin/console, по сути, они делают тоже самое, только из под контейнера.
Теперь добавим maker bundle и библиотеку Ratchet:
docker-compose exec php composer require symfony/maker-bundle --dev
docker-compose exec php composer require cboden/ratchet --dev
Окей, теперь наконец перейдём к написанию самого чата. Для этого, в папке src, создайте папку Sockets, а внутри класс Chat.
Этот класс должен реализовывать интерфейс MessageComponentInterface, который находится в пространстве имён Ratchet.
Вот моя реализация:
<?php
namespace App\Sockets;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class Chat implements MessageComponentInterface {
protected $clients;
public function __construct() {
$this->clients = new \SplObjectStorage;
}
public function onOpen(ConnectionInterface $conn) {
$this->clients->attach($conn);
}
public function onMessage(ConnectionInterface $from, $msg) {
foreach ($this->clients as $client) {
if ($from !== $client) {
$client->send($msg);
}
}
}
public function onClose(ConnectionInterface $conn) {
$this->clients->detach($conn);
echo "Connection {$conn->resourceId} has disconnected\n";
}
public function onError(ConnectionInterface $conn, \Exception $e) {
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
}
В этом классе, мы описали что будет делать сервер на каждое из соответствующих событий.
На открытие сокета на клиенте (метод onOpen) мы будем сохранять соединение в хранилище.
При получении сообщения(метод onMessage) мы будем отсылать сообщение на все соединения, кроме своего.
При закрытии сокета на клиенте (метод onClose) мы будем убирать соединение из хранилища.
При возникновении ошибки (метод onError) мы будем закрывать соответствующее соединение.
Теперь нам нужна команда, которая будет запускать чатик с сокетами.
sf make:command
Choose a command name (e.g. app:tiny-chef):
> ChatCommand
created: src/Command/ChatCommand.php
Success!
Чтобы сильно не тянуть, сразу покажу свою реализацию:
<?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
->setName('sockets:start-chat')
->setDescription('Starts the socket chat demo')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln([
'Chat socket',
'============',
'Starting chat, open your browser.',
]);
$server = IoServer::factory(
new HttpServer(
new WsServer(
new Chat()
)
),
10000
);
$server->run();
}
}
Каждая команда состоит как минимум из двух методов.
Первый — configure, задаёт конфигурацию. В нашем случае — я задал имя команды, описание. Имя нужно для того, чтобы обращаться к команде из консоли, а описание — это то, что будет стоять справа от имени команды в списке всех команд ( ради интереса напишите в консоль sf и найдите свою команду).
Второй — execute, само действие, которое будет делать команда. В нашем случае оно создаёт и запускает наш сервер, на 10000 порту.
Попробуйте запустить команду, ведь серверная часть нашего чатика почти готова ( нужен будет ещё контроллер, который возвращает страничку с нашим чатом).
Перед тем, как создавать контроллер опишем нашу страничку. Для этого создадим папку templates в папке с проектом. В ней создадим файл base.html.twig
Это наша базовая страница (если вы знакомы с наследованием в ООП, то тут это работает плюс минус также, так что если вы вдруг соберётесь расширять это приложение, то оставьте на базовой странице только то, что нужно на ВСЕХ других страницах). Со следующим содержимым:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
{% block title %}Welcome!{% endblock %}
</title>
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('css/chat.css') }}">
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/3.0.3/handlebars.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.2/moment.min.js"></script>
<script src="{{ asset('js/chat.js') }}"></script>
{% endblock %}
</body>
</html>
Здесь мы подключили наши (пока ещё не написанные стили), и js — скрипты. Первые три — это библиотеки. Jquery, handlebars,moment. О каждой из них вам лучше почитать отдельно, ведь я не js разработчик.
Если вкратце:
-
- Jquery — позволяет нам довольно просто работать с DOM.
- Handlebars— это клиентский шаблонизатор для JavaScript.
- moment — позволяет легко работать со временем.
Теперь создадим нашу страничку с чатом. В той же папке (templates) создайте папку chat, и в ней файл index.html.twig со следующим содержимым:
{% extends'base.html.twig' %}
{% block title %} chatapp {% endblock %}
{% block body %}
<div id="wrapper">
<div id="user-container">
<label for="user">What's your name?</label>
<input type="text" id="user" name="user">
<button type="button" id="join-chat">Join Chat</button>
</div>
<div id="main-container" class="hidden">
<button type="button" id="leave-room">Leave</button>
<div id="messages">
</div>
<div id="msg-container">
<input type="text" id="msg" name="msg">
<button type="button" id="send-msg">Send</button>
</div>
</div>
</div>
{% verbatim %}
<script id="messages-template" type="text/x-handlebars-template">
{{#each messages}}
<div class="msg">
<div class="time">{{time}}</div>
<div class="details">
<span class="user">{{user}}</span>: <span class="text">{{text}}</span>
</div>
</div>
{{/each}}
</script>
{% endverbatim %}
{% endblock %}
Здесь комментировать особо нечего, кроме twig — блока {% verbatim %}, это блок, который НЕ будет отображаться на странице.
Ок-с, теперь в папке проекта public создайте папку css, а в ней файл chat.css
Можете стилизовать по своему вкусу, я предложу это (довольно минималистичненько).
.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;
}
Отлично, теперь JS. В папке public создайте папку js и в ней файл chat.js
Наполните его следующим.
(function(){
var user;
var messages = [];
var messages_template = Handlebars.compile($('#messages-template').html());
function updateMessages(msg){
messages.push(msg);
var messages_html = messages_template({'messages': messages});
$('#messages').html(messages_html);
$("#messages").animate({ scrollTop: $('#messages')[0].scrollHeight}, 1);
}
var conn = new WebSocket('ws://localhost:8000');
conn.onopen = function(e) {
console.log("Connection established!");
};
conn.onmessage = function(e) {
var msg = JSON.parse(e.data);
updateMessages(msg);
};
$('#join-chat').click(function(){
user = $('#user').val();
$('#user-container').addClass('hidden');
$('#main-container').removeClass('hidden');
var msg = {
'user': user,
'text': user + ' entered the room',
'time': moment().format('hh:mm a')
};
updateMessages(msg);
conn.send(JSON.stringify(msg));
$('#user').val('');
});
$('#send-msg').click(function(){
var text = $('#msg').val();
var msg = {
'user': user,
'text': text,
'time': moment().format('hh:mm a')
};
updateMessages(msg);
conn.send(JSON.stringify(msg));
$('#msg').val('');
});
$('#leave-room').click(function(){
var msg = {
'user': user,
'text': user + ' has left the room',
'time': moment().format('hh:mm a')
};
updateMessages(msg);
conn.send(JSON.stringify(msg));
$('#messages').html('');
messages = [];
$('#main-container').addClass('hidden');
$('#user-container').removeClass('hidden');
conn.close();
});
})();
ГЛАВНОЕ ЗДЕСЬ: создаём WebSocket с нашим url-ом и портом.
Дальше вешаем обработчики на соответствующие события у этого объекта. Всё остальное здесь — вкусовщина, не так важная для понимания того, как это работает.
Ок-с, последний штрих. Создаём контроллер, который будет возвращать нам нашу страничку.
Для этого:
docker-compose exec php composer require annotations
После установки бандла:
sf make:controller
Choose a name for your controller class (e.g. FierceGnomeController):
> ChatController
created: src/Controller/ChatController.php
Success!
Переходим в наш контроллер, и чуть -чуть переделываем метод index, чтобы он просто рендерил нашу страничку.
/**
* @Route("/chat", name="chat")
*/
public function index()
{
return $this->render('chat/index.html.twig');
}
После этого устанавливаем ещё кое-какие компоненты symfony.
docker-compose exec php composer require symfony/twig-bundle
docker-compose exec php composer require symfony/asset
и, наконец, мы можем запускать наш чат.
ch sockets:start-chat
Переходим в браузер по следующему адресу: 0.0.0.0:8080/chat
и, переписываемся сколько нам влезет.

Важно заметить, что в принципе вебсокеты на PHP это плохая идея. Если вам нужны сокеты, то лучше использовать другой инструмент: Node.js / Python / C# что-то из этого.
Если у вас вдруг что-то не получилось, то можете посмотреть это приложение в гите: