blob: 6a43c12042b0153bd7c2b7467e61081f381956f9 [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\VarDumper\Server;
13
14use Psr\Log\LoggerInterface;
15use Symfony\Component\VarDumper\Cloner\Data;
16use Symfony\Component\VarDumper\Cloner\Stub;
17
18/**
19 * A server collecting Data clones sent by a ServerDumper.
20 *
21 * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
22 *
23 * @final
24 */
25class DumpServer
26{
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010027 private string $host;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020028 private $logger;
29
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010030 /**
31 * @var resource|null
32 */
33 private $socket;
34
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020035 public function __construct(string $host, LoggerInterface $logger = null)
36 {
37 if (!str_contains($host, '://')) {
38 $host = 'tcp://'.$host;
39 }
40
41 $this->host = $host;
42 $this->logger = $logger;
43 }
44
45 public function start(): void
46 {
47 if (!$this->socket = stream_socket_server($this->host, $errno, $errstr)) {
48 throw new \RuntimeException(sprintf('Server start failed on "%s": ', $this->host).$errstr.' '.$errno);
49 }
50 }
51
52 public function listen(callable $callback): void
53 {
54 if (null === $this->socket) {
55 $this->start();
56 }
57
58 foreach ($this->getMessages() as $clientId => $message) {
59 if ($this->logger) {
60 $this->logger->info('Received a payload from client {clientId}', ['clientId' => $clientId]);
61 }
62
63 $payload = @unserialize(base64_decode($message), ['allowed_classes' => [Data::class, Stub::class]]);
64
65 // Impossible to decode the message, give up.
66 if (false === $payload) {
67 if ($this->logger) {
68 $this->logger->warning('Unable to decode a message from {clientId} client.', ['clientId' => $clientId]);
69 }
70
71 continue;
72 }
73
74 if (!\is_array($payload) || \count($payload) < 2 || !$payload[0] instanceof Data || !\is_array($payload[1])) {
75 if ($this->logger) {
76 $this->logger->warning('Invalid payload from {clientId} client. Expected an array of two elements (Data $data, array $context)', ['clientId' => $clientId]);
77 }
78
79 continue;
80 }
81
82 [$data, $context] = $payload;
83
84 $callback($data, $context, $clientId);
85 }
86 }
87
88 public function getHost(): string
89 {
90 return $this->host;
91 }
92
93 private function getMessages(): iterable
94 {
95 $sockets = [(int) $this->socket => $this->socket];
96 $write = [];
97
98 while (true) {
99 $read = $sockets;
100 stream_select($read, $write, $write, null);
101
102 foreach ($read as $stream) {
103 if ($this->socket === $stream) {
104 $stream = stream_socket_accept($this->socket);
105 $sockets[(int) $stream] = $stream;
106 } elseif (feof($stream)) {
107 unset($sockets[(int) $stream]);
108 fclose($stream);
109 } else {
110 yield (int) $stream => fgets($stream);
111 }
112 }
113 }
114 }
115}