承接 machinateur/php-sse 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

machinateur/php-sse

Composer 安装命令:

composer require machinateur/php-sse

包简介

This package implements Server-sent events in PHP, in a framework-agnostic, low-dependency and object-oriented way.

README 文档

README

This package implements Server-sent events in PHP, in a framework-agnostic, low-dependency and object-oriented way.

Prerequisites

Yes, you've read that right, PHP 5.6 is the minimum version requirement to be able to use this package. That way, even legacy projects can provide SSE support to client applications.

It might also be a good idea to gobble up all information on SSE from the MDN web docs to get yourself up to speed on the technology. I found it to be an excellent starting point on this topic along with low-level examples.

Installation

Via composer:

# install the latest version
composer require "machinateur/php-sse"

Usage

The following is a simple use-case in plain PHP. For a full example (with client code), have a look around the demo directory.

<?php

namespace App;

use Machinateur\SSE\MessageStream;
use Machinateur\SSE\Exception\TimeoutException;

// ...

$stream = new MessageStream();

// ...

// Count to 10, then quit at 5.
$stream->run(function () {
    foreach (\range(1, 10, 1) as $i) {
        if ($i > 5) {
            throw TimeoutException::toTriggerStreamShutdown();
        }

        yield $i;

        \sleep(1);
    }
});

Make sure to read about when and why to use this package, to make sure it fits your use-case.

To check out the demo, execute php -S 127.0.0.1:8001 -t ./demo in the terminal and open a new tab http://127.0.0.1:8001/message_stream.html. Make sure to check out the network tab of the browser developer tools.

Below are additional explanations of common use-case scenarios.

Recommended headers

Recommended headers can be retrieved from the default implementation of \Machinateur\SSE\MessageStreamInterface.

$recommendedHeaders = \Machinateur\SSE\MessageStream::getRecommendedHeaders();

Logger support

A custom logger can be set using the setter.

$stream = new \Machinateur\SSE\MessageStream();
$stream->setLogger($myLogger);
// ...

A note on logger support: The \Machinateur\SSE\MessageStream class supports a psr logger to inform about shutdown signals (notice) and any output sent to the client (debug). A logger implementation that logs to the message stream itself is available as part of the demo application.

Custom message stream

It's easily possible to create a custom message stream by implementing the \Machinateur\SSE\MessageStreamInterface interface.

<?php

namespace App;

use Machinateur\SSE\Exception\TimeoutException;
use Machinateur\SSE\MessageStream;
use Machinateur\SSE\MessageStreamInterface;
use Machinateur\SSE\Message\MessageInterface;

/**
 * A naive implementation of {@see MessageStreamInterface}.
 */
class CustomMessageStream extends MessageStream implements MessageStreamInterface
{
    /**
     * @inheritDoc
     */
    public function run(callable $callback)
    {
        try {
            foreach ($callback() as $message) {
                \assert($message instanceof MessageInterface);

                $this->printOutput($message->getMessageFormat());
                $this->checkConnection();
            }
        } catch (TimeoutException $exception) {
        }
    }
}

// ...

$stream = new CustomMessageStream();

When implementing a custom message stream, bear in mind the requirement to support array result and generator function, as imposed by the interface.

Any implementation must support passing a callback function with array result or a generator function in its
 stead. In the latter case, a {@see \Machinateur\SSE\Exception\TimeoutException} throw must shut down the
 stream (due to time-out). A proper client implementation will resume the connection after its `retry` period.

Custom messages

It's also possible to create custom messages by simply implementing the \Machinateur\SSE\Message\MessageInterface interface.

<?php

namespace App;

use Machinateur\SSE\Format\StreamFormat;
use Machinateur\SSE\Message\MessageInterface;

/**
 * Custom message to be yielded by a generator function. It holds a data array, which is given to `json_encode()`
 *  when sent by the message stream.
 */
class CustomMessage implements MessageInterface
{
    const FLAGS = \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_PRESERVE_ZERO_FRACTION;

    /** @var array */
    private $data = array();
    
    /**
     * @param array $data
     */
    public function setData($data)
    {
        $this->data = $data;
    }

    /**
     * @inheritDoc
     */
    public function getStreamFormat()
    {
        return [
            StreamFormat::FIELD_COMMENT => 'source: ' . self::class,
            StreamFormat::FIELD_DATA => \json_encode($this->data, self::FLAGS)
        ];
    }
}

Array vs generator

The callback function, which is passed to the \Machinateur\SSE\MessageStreamInterface::run() method, may return an array or be itself a generator function. That freedom of implementation naturally raises the question of werther to use a generator function or not.

It really depends on your use-case, I'd say. For polling situations on the server-side (e.g. consume a message queue) the obvious choice would be the latter, since collecting a bunch of messages and returning them all at once, would make the use of SSE redundant. Why then event support an array return value than? Supporting both could ease the integration and adoption as well as making testing somewhat easier.

It deserves to be mentioned the \Machinateur\SSE\MessageStream (a simple but sufficient implementation of the interface), internally converts any array return value to a generator anyway.

Framework integration

The library does not automatically integrate with frameworks on purpose, to keep it simple. That does not mean it's impossible to use it with a framework.

Using \Symfony\Component\HttpFoundation\Response (symfony) or \Illuminate\Http\Response (laravel) is pretty straightforward.

  • Example #1: \Symfony\Component\HttpFoundation\Response
use Machinateur\SSE\MessageStream;

// ...

$headers = array();

foreach (MessageStream::getRecommendedHeaders() as $header) {
    list($key, $value) = \explode(':', $header, 2);
    $headers[$key] = $value;
}

$response->headers->add($headers);
  • Example #2: \Illuminate\Http\Response (extends \Symfony\Component\HttpFoundation\Response)
use Machinateur\SSE\MessageStream;

// ...

$headers = array();

foreach (MessageStream::getRecommendedHeaders() as $header) {
    list($key, $value) = \explode(':', $header, 2);
    $headers[$key] = $value;
}

$response->withHeaders($headers);
// or
$response->headers->add($headers);

Usage with other SSE implementations

This library was inspired and influenced by hhxsv5/php-sse, so here is how to achieve interoperability between the two. This example is based on the existing php-fpm example from hhxsv5/php-sse.

<?php

namespace App;

use Hhxsv5\SSE\Event;
use Hhxsv5\SSE\StopSSEException;
use Machinateur\SSE\Exception\TimeoutException;
use Machinateur\SSE\Format\StreamFormat;
use Machinateur\SSE\MessageStream;

foreach (MessageStream::getRecommendedHeaders() as $header) {
    \header($header);
}

// The callback for `hhxsv5/php-sse`.
$callback = function () {
    $id = \mt_rand(1, 1000);

    // Get news from database or service.
    $news = [
        [
            'id' => $id,
            'title' => 'title ' . $id,
            'content' => 'content ' . $id,
        ],
    ];

    // Stop here when no news available.
    if (empty($news)) {
        return false;
    }
    
    // In case something went wrong.
    $shouldStop = false;
    if ($shouldStop) {
        throw new StopSSEException();
    }
    
    return \json_encode(\compact('news'));
    // return ['event' => 'ping', 'data' => 'ping data'];
    // return ['id' => uniqid(), 'data' => json_encode(compact('news'))];
};

$event = new Event($callback, 'news');
unset($callback);

/**
 * Wrapper for better access to protected fields of `\Hhxsv5\SSE\Event`.
 * 
 * @property string $id
 * @property string $event
 * @property string $data
 * @property string $retry
 * @property string $comment
 */
class EventWrapper extends Event
{
    /** @var Event */
    protected $eventObject;

    public function __construct(Event $event)
    {
        $this->eventObject = $event;
    }

    /**
     * @inheritDoc
     */
    public function __get($name)
    {
        if (\in_array($name, ['id', 'event', 'data', 'retry', 'comment']) && \property_exists($this, $name)) {
            return $this->eventObject->{$name};
        }

        throw new LogicException(\sprintf('Unknown property: %s', $name));
    }

    /**
     * @inheritDoc
     */
    public function fill()
    {
        $this->eventObject->fill();
    }

    public function __toString()
    {
        return $this->eventObject->__toString();
    }
}

$event = new EventWrapper($event);

// The callback for `machinateur/php-sse`.
$callback = function () use ($event) {
    try {
        $event->fill();
        yield [
            StreamFormat::FIELD_COMMENT => $event->comment;
            StreamFormat::FIELD_ID => $event->id;
            StreamFormat::FIELD_RETRY => $event->retry;
            StreamFormat::FIELD_EVENT => $event->event;
            StreamFormat::FIELD_DATA => $event->data;
        ];
    } catch (StopSSEException $exception) {
        throw TimeoutException::toTriggerStreamShutdown();
    }
};

$messageStream = new MessageStream();
$messageStream->setLogger($myLogger);
$messageStream->run($callback);

About

Here's some basic information on this package itself and the intent behind it.

What is SSE actually?

Traditionally, a web page has to send a request to the server to receive new data; that is, the page requests data from the server. With server-sent events, it's possible for a server to send new data to a web page at any time, by pushing messages to the web page.

(from "Server-sent events" on the MDN web docs)

[...] server-sent events are unidirectional; that is, data messages are delivered in one direction, from the server to the client (such as a user's web browser). That makes them an excellent choice when there's no need to send data from the client to the server in message form. For example, EventSource is a useful approach for handling things like social media status updates, news feeds, or delivering data into a client-side storage [...].

(from "EventSource" on the MDN web docs)

Simply put, SSE allows us to send events to the client web page from the server.

Why use this package?

This package...

  • ... is framework-agnostic.
  • ... avoids dependencies.
  • ... uses an object-oriented approach.
  • ... is extensible.
  • ... is compatible with PHP >= 5.6.

When to use this package?

This package can be used to implement simple SSE on the server-side.

The client-side may use an SSE polyfill, like Remy Sharp's EventSource polyfill) or Yaffle's EventSource polyfill, the native browser implementation (see caniuse).

For more complex use-cases, a more flexible alternative like Mercure might be preferable thought.

Useful read

License

It's MIT.

machinateur/php-sse 适用场景与选型建议

machinateur/php-sse 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 13 次下载、GitHub Stars 达 1, 最近一次更新时间为 2022 年 05 月 22 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「event-source」 「sse」 「Server-Sent Events」 「event-stream」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 machinateur/php-sse 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 machinateur/php-sse 我们能提供哪些服务?
定制开发 / 二次开发

基于 machinateur/php-sse 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 13
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 8
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 2
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2022-05-22