├── src
├── Exception
│ ├── InvalidConfigException.php
│ ├── ExtensionNotLoadedException.php
│ ├── RuntimeException.php
│ ├── InvalidArgumentException.php
│ ├── ExceptionInterface.php
│ └── InvalidStaticResourceMiddlewareException.php
├── Log
│ ├── AccessLogFormatterInterface.php
│ ├── LoggerResolvingTrait.php
│ ├── AccessLogInterface.php
│ ├── SwooleLoggerFactory.php
│ ├── AccessLogFactory.php
│ ├── StdoutLogger.php
│ ├── Psr3AccessLogDecorator.php
│ ├── AccessLogFormatter.php
│ └── AccessLogDataMap.php
├── Command
│ ├── StartCommandFactory.php
│ ├── StopCommandFactory.php
│ ├── StatusCommandFactory.php
│ ├── ReloadCommandFactory.php
│ ├── IsRunningTrait.php
│ ├── StatusCommand.php
│ ├── StopCommand.php
│ ├── ReloadCommand.php
│ └── StartCommand.php
├── HotCodeReload
│ ├── FileWatcherInterface.php
│ ├── ReloaderFactory.php
│ ├── FileWatcher
│ │ └── InotifyFileWatcher.php
│ └── Reloader.php
├── PidManagerFactory.php
├── StaticResourceHandler
│ ├── MiddlewareInterface.php
│ ├── HeadMiddleware.php
│ ├── OptionsMiddleware.php
│ ├── MethodNotAllowedMiddleware.php
│ ├── MiddlewareQueue.php
│ ├── ValidateRegexTrait.php
│ ├── ClearStatCacheMiddleware.php
│ ├── LastModifiedMiddleware.php
│ ├── GzipMiddleware.php
│ ├── ETagMiddleware.php
│ ├── CacheControlMiddleware.php
│ ├── ContentTypeFilterMiddleware.php
│ └── StaticResourceResponse.php
├── StaticResourceHandlerInterface.php
├── WhoopsPrettyPageHandlerDelegator.php
├── ServerRequestSwooleFactory.php
├── PidManager.php
├── SwooleRequestHandlerRunnerFactory.php
├── StaticResourceHandler.php
├── HttpServerFactory.php
├── SwooleEmitter.php
├── ConfigProvider.php
├── SwooleStream.php
├── StaticResourceHandlerFactory.php
└── SwooleRequestHandlerRunner.php
├── LICENSE.md
├── bin
└── zend-expressive-swoole
├── composer.json
├── README.md
└── CHANGELOG.md
/src/Exception/InvalidConfigException.php:
--------------------------------------------------------------------------------
1 | get(PidManager::class));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Command/StatusCommandFactory.php:
--------------------------------------------------------------------------------
1 | get(PidManager::class));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Log/LoggerResolvingTrait.php:
--------------------------------------------------------------------------------
1 | has(SwooleLoggerFactory::SWOOLE_LOGGER)
20 | ? $container->get(SwooleLoggerFactory::SWOOLE_LOGGER)
21 | : (new SwooleLoggerFactory())($container);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/HotCodeReload/FileWatcherInterface.php:
--------------------------------------------------------------------------------
1 | has('config') ? $container->get('config') : [];
21 | $mode = $config['zend-expressive-swoole']['swoole-http-server']['mode'] ?? SWOOLE_BASE;
22 |
23 | return new ReloadCommand($mode);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Log/AccessLogInterface.php:
--------------------------------------------------------------------------------
1 | get('config');
21 | return new PidManager(
22 | $config['zend-expressive-swoole']['swoole-http-server']['options']['pid_file']
23 | ?? sys_get_temp_dir() . '/zend-swoole.pid'
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/StaticResourceHandler/MiddlewareInterface.php:
--------------------------------------------------------------------------------
1 | server;
20 | if ($server['request_method'] !== 'HEAD') {
21 | return $response;
22 | }
23 | $response->disableContent();
24 | return $response;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/StaticResourceHandler/OptionsMiddleware.php:
--------------------------------------------------------------------------------
1 | server;
21 | if ($server['request_method'] !== 'OPTIONS') {
22 | return $response;
23 | }
24 |
25 | $response->addHeader('Allow', 'GET, HEAD, OPTIONS');
26 | $response->disableContent();
27 | return $response;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Exception/InvalidStaticResourceMiddlewareException.php:
--------------------------------------------------------------------------------
1 | server;
21 | if (in_array($server['request_method'], ['GET', 'HEAD', 'OPTIONS'], true)) {
22 | return $next($request, $filename);
23 | }
24 |
25 | return new StaticResourceResponse(
26 | 405,
27 | ['Allow' => 'GET, HEAD, OPTIONS'],
28 | false
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/HotCodeReload/ReloaderFactory.php:
--------------------------------------------------------------------------------
1 | has('config') ? $container->get('config') : [];
22 | $swooleConfig = $config['zend-expressive-swoole'] ?? [];
23 | $hotCodeReloadConfig = $swooleConfig['hot-code-reload'] ?? [];
24 |
25 | return new Reloader(
26 | $container->get(FileWatcherInterface::class),
27 | $this->getLogger($container),
28 | $hotCodeReloadConfig['interval'] ?? 500
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/StaticResourceHandlerInterface.php:
--------------------------------------------------------------------------------
1 | middleware = $middleware;
29 | }
30 |
31 | public function __invoke(Request $request, string $filename) : StaticResourceResponse
32 | {
33 | if ([] === $this->middleware) {
34 | return new StaticResourceResponse();
35 | }
36 |
37 | $middleware = array_shift($this->middleware);
38 | return $middleware($request, $filename, $this);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Log/SwooleLoggerFactory.php:
--------------------------------------------------------------------------------
1 | has('config') ? $container->get('config') : [];
22 | $loggerConfig = $config['zend-expressive-swoole']['swoole-http-server']['logger'] ?? [];
23 |
24 | if (isset($loggerConfig['logger-name'])) {
25 | return $container->get($loggerConfig['logger-name']);
26 | }
27 |
28 | return $container->has(LoggerInterface::class) ? $container->get(LoggerInterface::class) : new StdoutLogger();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Command/IsRunningTrait.php:
--------------------------------------------------------------------------------
1 | pidManager->read();
23 |
24 | if ([] === $pids) {
25 | return false;
26 | }
27 |
28 | [$masterPid, $managerPid] = $pids;
29 |
30 | if ($managerPid) {
31 | // Swoole process mode
32 | return $masterPid && $managerPid && SwooleProcess::kill((int) $managerPid, 0);
33 | }
34 |
35 | // Swoole base mode, no manager process
36 | return $masterPid && SwooleProcess::kill((int) $masterPid, 0);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/WhoopsPrettyPageHandlerDelegator.php:
--------------------------------------------------------------------------------
1 | handleUnconditionally(true);
30 | return $pageHandler;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/StaticResourceHandler/ValidateRegexTrait.php:
--------------------------------------------------------------------------------
1 | isValidRegex($regex)) {
37 | throw new Exception\InvalidArgumentException(sprintf(
38 | 'The %s regex "%s" is invalid',
39 | $type,
40 | $regex
41 | ));
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018, Zend Technologies USA, Inc.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | - Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | - Redistributions in binary form must reproduce the above copyright notice, this
11 | list of conditions and the following disclaimer in the documentation and/or
12 | other materials provided with the distribution.
13 |
14 | - Neither the name of Zend Technologies USA, Inc. nor the names of its
15 | contributors may be used to endorse or promote products derived from this
16 | software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/src/StaticResourceHandler/ClearStatCacheMiddleware.php:
--------------------------------------------------------------------------------
1 | interval = $interval;
38 | }
39 |
40 | /**
41 | * {@inheritDoc}
42 | */
43 | public function __invoke(Request $request, string $filename, callable $next): StaticResourceResponse
44 | {
45 | $now = time();
46 | if (1 > $this->interval
47 | || $this->lastCleared
48 | || ($this->lastCleared + $this->interval < $now)
49 | ) {
50 | clearstatcache();
51 | $this->lastCleared = $now;
52 | }
53 |
54 | return $next($request, $filename);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Command/StatusCommand.php:
--------------------------------------------------------------------------------
1 | pidManager = $pidManager;
37 | parent::__construct($name);
38 | }
39 |
40 | protected function configure() : void
41 | {
42 | $this->setDescription('Get the status of the web server.');
43 | $this->setHelp(self::HELP);
44 | }
45 |
46 | protected function execute(InputInterface $input, OutputInterface $output) : int
47 | {
48 | $message = $this->isRunning()
49 | ? 'Server is running'
50 | : 'Server is not running';
51 |
52 | $output->writeln($message);
53 |
54 | return 0;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/ServerRequestSwooleFactory.php:
--------------------------------------------------------------------------------
1 | get ?? [];
33 | $post = $request->post ?? [];
34 | $cookie = $request->cookie ?? [];
35 | $files = $request->files ?? [];
36 | $server = $request->server ?? [];
37 | $headers = $request->header ?? [];
38 |
39 | // Normalize SAPI params
40 | $server = array_change_key_case($server, CASE_UPPER);
41 |
42 | return new ServerRequest(
43 | $server,
44 | normalizeUploadedFiles($files),
45 | marshalUriFromSapi($server, $headers),
46 | marshalMethodFromSapi($server),
47 | new SwooleStream($request),
48 | $headers,
49 | $cookie,
50 | $get,
51 | $post,
52 | marshalProtocolVersionFromSapi($server)
53 | );
54 | };
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/PidManager.php:
--------------------------------------------------------------------------------
1 | pidFile = $pidFile;
30 | }
31 |
32 | /**
33 | * Write master pid and manager pid to pid file
34 | *
35 | * @throws Exception\RuntimeException When $pidFile is not writable
36 | */
37 | public function write(int $masterPid, int $managerPid) : void
38 | {
39 | if (! is_writable($this->pidFile) && ! is_writable(dirname($this->pidFile))) {
40 | throw new Exception\RuntimeException(sprintf('Pid file "%s" is not writable', $this->pidFile));
41 | }
42 | file_put_contents($this->pidFile, $masterPid . ',' . $managerPid);
43 | }
44 |
45 | /**
46 | * Read master pid and manager pid from pid file
47 | *
48 | * @return string[] {
49 | * @var string $masterPid
50 | * @var string $managerPid
51 | * }
52 | */
53 | public function read() : array
54 | {
55 | $pids = [];
56 | if (is_readable($this->pidFile)) {
57 | $content = file_get_contents($this->pidFile);
58 | $pids = explode(',', $content);
59 | }
60 | return $pids;
61 | }
62 |
63 | /**
64 | * Delete pid file
65 | */
66 | public function delete() : bool
67 | {
68 | if (is_writable($this->pidFile)) {
69 | return unlink($this->pidFile);
70 | }
71 | return false;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Log/AccessLogFactory.php:
--------------------------------------------------------------------------------
1 |
22 | * 'zend-expressive-swoole' => [
23 | * 'swoole-http-server' => [
24 | * 'logger' => [
25 | * 'logger-name' => string, // the name of a service resolving a Psr\Log\LoggerInterface instance
26 | * 'format' => string, // one of the AccessLogFormatter::FORMAT_* constants
27 | * 'use-hostname-lookups' => bool, // Set to true to enable hostname lookups
28 | * ],
29 | * ],
30 | * ],
31 | *
32 | */
33 | class AccessLogFactory
34 | {
35 | use LoggerResolvingTrait;
36 |
37 | public function __invoke(ContainerInterface $container) : AccessLogInterface
38 | {
39 | $config = $container->has('config') ? $container->get('config') : [];
40 | $config = $config['zend-expressive-swoole']['swoole-http-server']['logger'] ?? [];
41 |
42 | return new Psr3AccessLogDecorator(
43 | $this->getLogger($container),
44 | $this->getFormatter($container, $config),
45 | $config['use-hostname-lookups'] ?? false
46 | );
47 | }
48 |
49 | private function getFormatter(ContainerInterface $container, array $config) : AccessLogFormatterInterface
50 | {
51 | if ($container->has(AccessLogFormatterInterface::class)) {
52 | return $container->get(AccessLogFormatterInterface::class);
53 | }
54 |
55 | return new AccessLogFormatter(
56 | $config['format'] ?? AccessLogFormatter::FORMAT_COMMON
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/bin/zend-expressive-swoole:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | setAutoExit(true);
67 | $commandLine->setCommandLoader(new ContainerCommandLoader($container, [
68 | 'reload' => ReloadCommand::class,
69 | 'start' => StartCommand::class,
70 | 'status' => StatusCommand::class,
71 | 'stop' => StopCommand::class,
72 | ]));
73 | $commandLine->run();
74 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zendframework/zend-expressive-swoole",
3 | "description": "Swoole support for Expressive",
4 | "license": "BSD-3-Clause",
5 | "keywords": [
6 | "components",
7 | "zf",
8 | "zendframework",
9 | "expressive",
10 | "swoole",
11 | "psr-7",
12 | "psr-15",
13 | "psr-17"
14 | ],
15 | "support": {
16 | "docs": "https://docs.zendframework.com/zend-expressive-swoole/",
17 | "issues": "https://github.com/zendframework/zend-expressive-swoole/issues",
18 | "source": "https://github.com/zendframework/zend-expressive-swoole",
19 | "rss": "https://github.com/zendframework/zend-expressive-swoole/releases.atom",
20 | "chat": "https://zendframework-slack.herokuapp.com",
21 | "forum": "https://discourse.zendframework.com/c/questions/components"
22 | },
23 | "require": {
24 | "php": "^7.1",
25 | "ext-swoole": "*",
26 | "dflydev/fig-cookies": "^1.0 || ^2.0",
27 | "ocramius/package-versions": "^1.3",
28 | "psr/container": "^1.0",
29 | "psr/http-message": "^1.0",
30 | "psr/http-message-implementation": "^1.0",
31 | "psr/http-server-handler": "^1.0",
32 | "psr/log": "^1.0",
33 | "symfony/console": "^4.1 || ^5.0",
34 | "zendframework/zend-diactoros": "^1.8 || ^2.0",
35 | "zendframework/zend-expressive": "^3.0.2",
36 | "zendframework/zend-httphandlerrunner": "^1.0.1"
37 | },
38 | "require-dev": {
39 | "filp/whoops": "^2.1",
40 | "phpunit/phpunit": "^7.5.17 || ^8.4.3",
41 | "zendframework/zend-coding-standard": "~1.0.0",
42 | "zendframework/zend-servicemanager": "^3.3"
43 | },
44 | "suggest": {
45 | "ext-inotify": "To use inotify based file watcher. Required for hot code reloading."
46 | },
47 | "autoload": {
48 | "psr-4": {
49 | "Zend\\Expressive\\Swoole\\": "src/"
50 | }
51 | },
52 | "autoload-dev": {
53 | "psr-4": {
54 | "ZendTest\\Expressive\\Swoole\\": "test/"
55 | }
56 | },
57 | "bin": [
58 | "bin/zend-expressive-swoole"
59 | ],
60 | "config": {
61 | "sort-packages": true
62 | },
63 | "extra": {
64 | "branch-alias": {
65 | "dev-master": "2.5.x-dev",
66 | "dev-develop": "2.6.x-dev"
67 | }
68 | },
69 | "scripts": {
70 | "check": [
71 | "@cs-check",
72 | "@test"
73 | ],
74 | "cs-check": "phpcs",
75 | "cs-fix": "phpcbf",
76 | "test": "phpunit --colors=always",
77 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/HotCodeReload/FileWatcher/InotifyFileWatcher.php:
--------------------------------------------------------------------------------
1 | inotify = $resource;
43 | }
44 |
45 | /**
46 | * Add a file path to be monitored for changes by this watcher.
47 | *
48 | * @param string $path
49 | */
50 | public function addFilePath(string $path) : void
51 | {
52 | $wd = inotify_add_watch($this->inotify, $path, IN_MODIFY);
53 | $this->filePathByWd[$wd] = $path;
54 | }
55 |
56 | public function readChangedFilePaths() : array
57 | {
58 | $events = inotify_read($this->inotify);
59 | $paths = [];
60 | if (is_array($events)) {
61 | foreach ($events as $event) {
62 | $wd = $event['wd'] ?? null;
63 | if (null === $wd) {
64 | throw new RuntimeException('Missing watch descriptor from inotify event');
65 | }
66 | $path = $this->filePathByWd[$wd] ?? null;
67 | if (null === $path) {
68 | throw new RuntimeException("Unrecognized watch descriptor: \"{$wd}\"");
69 | }
70 | $paths[$path] = $path;
71 | }
72 | }
73 |
74 | return array_values($paths);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/SwooleRequestHandlerRunnerFactory.php:
--------------------------------------------------------------------------------
1 | has(Log\AccessLogInterface::class)
24 | ? $container->get(Log\AccessLogInterface::class)
25 | : null;
26 |
27 | $expressiveSwooleConfig = $container->has('config')
28 | ? $container->get('config')['zend-expressive-swoole']
29 | : [];
30 |
31 | $swooleHttpServerConfig = $expressiveSwooleConfig['swoole-http-server'] ?? [];
32 |
33 | return new SwooleRequestHandlerRunner(
34 | $container->get(ApplicationPipeline::class),
35 | $container->get(ServerRequestInterface::class),
36 | $container->get(ServerRequestErrorResponseGenerator::class),
37 | $container->get(PidManager::class),
38 | $container->get(SwooleHttpServer::class),
39 | $this->retrieveStaticResourceHandler($container, $swooleHttpServerConfig),
40 | $logger,
41 | $swooleHttpServerConfig['process-name'] ?? SwooleRequestHandlerRunner::DEFAULT_PROCESS_NAME,
42 | $this->retrieveHotCodeReloader($container, $expressiveSwooleConfig)
43 | );
44 | }
45 |
46 | private function retrieveStaticResourceHandler(
47 | ContainerInterface $container,
48 | array $config
49 | ) : ?StaticResourceHandlerInterface {
50 | $config = $config['static-files'] ?? [];
51 | $enabled = isset($config['enable']) && true === $config['enable'];
52 |
53 | return $enabled && $container->has(StaticResourceHandlerInterface::class)
54 | ? $container->get(StaticResourceHandlerInterface::class)
55 | : null;
56 | }
57 |
58 | private function retrieveHotCodeReloader(
59 | ContainerInterface $container,
60 | array $config
61 | ) : ?Reloader {
62 | $config = $config['hot-code-reload'] ?? [];
63 | $enabled = isset($config['enable']) && true === $config['enable'];
64 |
65 | return $enabled && $container->has(Reloader::class)
66 | ? $container->get(Reloader::class)
67 | : null;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Log/StdoutLogger.php:
--------------------------------------------------------------------------------
1 | log(LogLevel::EMERGENCY, $message, $context);
37 | }
38 |
39 | /**
40 | * {@inheritDoc}
41 | */
42 | public function alert($message, array $context = [])
43 | {
44 | $this->log(LogLevel::ALERT, $message, $context);
45 | }
46 |
47 | /**
48 | * {@inheritDoc}
49 | */
50 | public function critical($message, array $context = [])
51 | {
52 | $this->log(LogLevel::CRITICAL, $message, $context);
53 | }
54 |
55 | /**
56 | * {@inheritDoc}
57 | */
58 | public function error($message, array $context = [])
59 | {
60 | $this->log(LogLevel::ERROR, $message, $context);
61 | }
62 |
63 | /**
64 | * {@inheritDoc}
65 | */
66 | public function warning($message, array $context = [])
67 | {
68 | $this->log(LogLevel::WARNING, $message, $context);
69 | }
70 |
71 | /**
72 | * {@inheritDoc}
73 | */
74 | public function notice($message, array $context = [])
75 | {
76 | $this->log(LogLevel::NOTICE, $message, $context);
77 | }
78 |
79 | /**
80 | * {@inheritDoc}
81 | */
82 | public function info($message, array $context = [])
83 | {
84 | $this->log(LogLevel::INFO, $message, $context);
85 | }
86 |
87 | /**
88 | * {@inheritDoc}
89 | */
90 | public function debug($message, array $context = [])
91 | {
92 | $this->log(LogLevel::DEBUG, $message, $context);
93 | }
94 |
95 | /**
96 | * {@inheritDoc}
97 | */
98 | public function log($level, $message, array $context = [])
99 | {
100 | foreach ($context as $key => $value) {
101 | $search = sprintf('{%s}', $key);
102 | $message = str_replace($search, $value, $message);
103 | }
104 | printf('%s%s', $message, PHP_EOL);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/StaticResourceHandler/LastModifiedMiddleware.php:
--------------------------------------------------------------------------------
1 | validateRegexList($lastModifiedDirectives, 'Last-Modified');
36 | $this->lastModifiedDirectives = $lastModifiedDirectives;
37 | }
38 |
39 | public function __invoke(Request $request, string $filename, callable $next) : StaticResourceResponse
40 | {
41 | $response = $next($request, $filename);
42 |
43 | if (! $this->getLastModifiedFlagForPath($request->server['request_uri'])) {
44 | return $response;
45 | }
46 |
47 | $lastModified = filemtime($filename) ?? 0;
48 | $formattedLastModified = trim(gmstrftime('%A %d-%b-%y %T %Z', $lastModified));
49 |
50 | $response->addHeader('Last-Modified', $formattedLastModified);
51 |
52 | if ($this->isUnmodified($request, $formattedLastModified)) {
53 | $response->setStatus(304);
54 | $response->disableContent();
55 | }
56 |
57 | return $response;
58 | }
59 |
60 | private function getLastModifiedFlagForPath(string $path) : bool
61 | {
62 | foreach ($this->lastModifiedDirectives as $regexp) {
63 | if (preg_match($regexp, $path)) {
64 | return true;
65 | }
66 | }
67 | return false;
68 | }
69 |
70 | /**
71 | * @return bool Returns true if the If-Modified-Since request header matches
72 | * the $lastModifiedTime value; in such cases, no content is returned.
73 | */
74 | private function isUnmodified(Request $request, string $lastModified) : bool
75 | {
76 | $ifModifiedSince = $request->header['if-modified-since'] ?? '';
77 | if ('' === $ifModifiedSince) {
78 | return false;
79 | }
80 |
81 | if (new DateTimeImmutable($ifModifiedSince) < new DateTimeImmutable($lastModified)) {
82 | return false;
83 | }
84 |
85 | return true;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/StaticResourceHandler.php:
--------------------------------------------------------------------------------
1 | validateMiddleware($middleware);
47 |
48 | $this->docRoot = $docRoot;
49 | $this->middleware = $middleware;
50 | }
51 |
52 | public function processStaticResource(
53 | SwooleHttpRequest $request,
54 | SwooleHttpResponse $response
55 | ) : ?StaticResourceHandler\StaticResourceResponse {
56 | $filename = $this->docRoot . $request->server['request_uri'];
57 |
58 | $middleware = new StaticResourceHandler\MiddlewareQueue($this->middleware);
59 | $staticResourceResponse = $middleware($request, $filename);
60 | if ($staticResourceResponse->isFailure()) {
61 | return null;
62 | }
63 |
64 | $staticResourceResponse->sendSwooleResponse($response, $filename);
65 | return $staticResourceResponse;
66 | }
67 |
68 | /**
69 | * Validate that each middleware provided is callable.
70 | *
71 | * @throws Exception\InvalidStaticResourceMiddlewareException for any
72 | * non-callable middleware encountered.
73 | */
74 | private function validateMiddleware(array $middlewareList) : void
75 | {
76 | foreach ($middlewareList as $position => $middleware) {
77 | if (! is_callable($middleware)) {
78 | throw Exception\InvalidStaticResourceMiddlewareException::forMiddlewareAtPosition(
79 | $middleware,
80 | $position
81 | );
82 | }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/HotCodeReload/Reloader.php:
--------------------------------------------------------------------------------
1 | fileWatcher = $fileWatcher;
46 | $this->interval = $interval;
47 | $this->logger = $logger;
48 | }
49 |
50 | public function onWorkerStart(SwooleServer $server, int $workerId) : void
51 | {
52 | // This method will be called for each started worker.
53 | // We will register our tick function on the first worker.
54 | if (0 === $workerId) {
55 | $server->tick($this->interval, $this->generateTickCallback($server));
56 | }
57 | }
58 |
59 | public function onTick(SwooleServer $server) : void
60 | {
61 | $this->watchIncludedFiles();
62 | $changedFilePaths = $this->fileWatcher->readChangedFilePaths();
63 | if ($changedFilePaths) {
64 | foreach ($changedFilePaths as $path) {
65 | $this->logger->notice("Reloading due to file change: {path}", ['path' => $path]);
66 | }
67 | $server->reload();
68 | }
69 | }
70 |
71 | /**
72 | * Generates a callable which will call this instance's onTick method with
73 | * the given swoole server instance. By doing this, we forgo the need to
74 | * have a swoole server property, and handle the case in which it wouldn't
75 | * exist.
76 | */
77 | private function generateTickCallback(SwooleServer $server) : callable
78 | {
79 | $reloader = $this;
80 |
81 | return function () use ($reloader, $server) {
82 | $reloader->onTick($server);
83 | };
84 | }
85 |
86 | /**
87 | * Start watching included files.
88 | */
89 | private function watchIncludedFiles() : void
90 | {
91 | foreach (array_diff(get_included_files(), $this->watchedFilePaths) as $filePath) {
92 | $this->fileWatcher->addFilePath($filePath);
93 | $this->watchedFilePaths[] = $filePath;
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # zend-expressive-swoole
2 |
3 | > ## Repository abandoned 2019-12-31
4 | >
5 | > This repository has moved to [mezzio/mezzio-swoole](https://github.com/mezzio/mezzio-swoole).
6 |
7 | [](https://secure.travis-ci.org/zendframework/zend-expressive-swoole)
8 | [](https://coveralls.io/github/zendframework/zend-expressive-swoole?branch=master)
9 |
10 | This library provides the support of [Swoole](https://www.swoole.co.uk/) into
11 | an [Expressive](https://getexpressive.org/) application. This means you can
12 | execute your Expressive application using Swoole directly from the command line.
13 |
14 |
15 | ## Installation
16 |
17 | Run the following to install this library:
18 |
19 | ```bash
20 | $ composer require zendframework/zend-expressive-swoole
21 | ```
22 |
23 | ## Configuration
24 |
25 | After installing zend-expressive-swoole, you will need to first enable the
26 | component, and then optionally configure it.
27 |
28 | We recommend adding a new configuration file to your autoload directory,
29 | `config/autoload/swoole.local.php`. To begin with, use the following contents:
30 |
31 | ```php
32 | [
52 | 'swoole-http-server' => [
53 | 'host' => 'insert hostname to use here',
54 | 'port' => 80, // use an integer value here
55 | ],
56 | ],
57 | ]);
58 | ```
59 |
60 | > ### Expressive skeleton 3.1.0 and later
61 | >
62 | > If you have built your application on the 3.1.0 or later version of the
63 | > Expressive skeleton, you do not need to instantiate and invoke the package's
64 | > `ConfigProvider`, as the skeleton supports it out of the box.
65 | >
66 | > You will only need to provide any additional configuration of the HTTP server.
67 |
68 | ## Execute
69 |
70 | Once you have performed the configuration steps as outlined above, you can run
71 | an Expressive application with Swoole using the following command:
72 |
73 | ```bash
74 | $ ./vendor/bin/zend-expressive-swoole start
75 | ```
76 |
77 | Call the command without arguments to get a list of available commands, and use
78 | the `help` meta-argument to get help on individual commands:
79 |
80 | ```bash
81 | $ ./vendor/bin/zend-expressive-swoole help start
82 | ```
83 |
84 | ## Documentation
85 |
86 | Browse the documentation online at https://docs.zendframework.com/zend-expressive-swoole/
87 |
88 | ## Support
89 |
90 | * [Issues](https://github.com/zendframework/zend-expressive-swoole/issues/)
91 | * [Chat](https://zendframework-slack.herokuapp.com/)
92 | * [Forum](https://discourse.zendframework.com/)
93 |
--------------------------------------------------------------------------------
/src/Command/StopCommand.php:
--------------------------------------------------------------------------------
1 | killProcess = Closure::fromCallable([SwooleProcess::class, 'kill']);
56 | $this->pidManager = $pidManager;
57 | parent::__construct($name);
58 | }
59 |
60 | protected function configure() : void
61 | {
62 | $this->setDescription('Stop the web server.');
63 | $this->setHelp(self::HELP);
64 | }
65 |
66 | protected function execute(InputInterface $input, OutputInterface $output) : int
67 | {
68 | if (! $this->isRunning()) {
69 | $output->writeln('Server is not running');
70 | return 0;
71 | }
72 |
73 | $output->writeln('Stopping server ...');
74 |
75 | if (! $this->stopServer()) {
76 | $output->writeln('Error stopping server; check logs for details');
77 | return 1;
78 | }
79 |
80 | $output->writeln('Server stopped');
81 | return 0;
82 | }
83 |
84 | private function stopServer() : bool
85 | {
86 | [$masterPid, ] = $this->pidManager->read();
87 | $startTime = time();
88 | $result = ($this->killProcess)((int) $masterPid);
89 |
90 | while (! $result) {
91 | if (! ($this->killProcess)((int) $masterPid, 0)) {
92 | continue;
93 | }
94 | if (time() - $startTime >= $this->waitThreshold) {
95 | $result = false;
96 | break;
97 | }
98 | usleep(10000);
99 | }
100 |
101 | if (! $result) {
102 | return false;
103 | }
104 |
105 | $this->pidManager->delete();
106 |
107 | return true;
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/Command/ReloadCommand.php:
--------------------------------------------------------------------------------
1 | serverMode = $serverMode;
41 | parent::__construct($name);
42 | }
43 |
44 | protected function configure() : void
45 | {
46 | $this->setDescription('Reload the web server.');
47 | $this->setHelp(self::HELP);
48 | $this->addOption(
49 | 'num-workers',
50 | 'w',
51 | InputOption::VALUE_REQUIRED,
52 | 'Number of worker processes to use after reloading.'
53 | );
54 | }
55 |
56 | protected function execute(InputInterface $input, OutputInterface $output) : int
57 | {
58 | if ($this->serverMode !== SWOOLE_PROCESS) {
59 | $output->writeln(
60 | 'Server is not configured to run in SWOOLE_PROCESS mode;'
61 | . ' cannot reload'
62 | );
63 | return 1;
64 | }
65 |
66 | $output->writeln('Reloading server ...');
67 |
68 | $application = $this->getApplication();
69 |
70 | $stop = $application->find('stop');
71 | $result = $stop->run(new ArrayInput([
72 | 'command' => 'stop',
73 | ]), $output);
74 |
75 | if (0 !== $result) {
76 | $output->writeln('Cannot reload server: unable to stop current server');
77 | return $result;
78 | }
79 |
80 | $output->write('Waiting for 5 seconds to ensure server is stopped...');
81 | for ($i = 0; $i < 5; $i += 1) {
82 | $output->write('.');
83 | sleep(1);
84 | }
85 | $output->writeln('[DONE]');
86 | $output->writeln('Starting server');
87 |
88 | $start = $application->find('start');
89 | $result = $start->run(new ArrayInput([
90 | 'command' => 'start',
91 | '--daemonize' => true,
92 | '--num-workers' => $input->getOption('num-workers') ?? StartCommand::DEFAULT_NUM_WORKERS,
93 | ]), $output);
94 |
95 | if (0 !== $result) {
96 | $output->writeln('Cannot reload server: unable to start server');
97 | return $result;
98 | }
99 |
100 | return 0;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/HttpServerFactory.php:
--------------------------------------------------------------------------------
1 | get('config');
64 | $swooleConfig = $config['zend-expressive-swoole'] ?? [];
65 | $serverConfig = $swooleConfig['swoole-http-server'] ?? [];
66 |
67 | $host = $serverConfig['host'] ?? static::DEFAULT_HOST;
68 | $port = $serverConfig['port'] ?? static::DEFAULT_PORT;
69 | $mode = $serverConfig['mode'] ?? SWOOLE_BASE;
70 | $protocol = $serverConfig['protocol'] ?? SWOOLE_SOCK_TCP;
71 |
72 | if ($port < 1 || $port > 65535) {
73 | throw new Exception\InvalidArgumentException('Invalid port');
74 | }
75 |
76 | if (! in_array($mode, static::MODES, true)) {
77 | throw new Exception\InvalidArgumentException('Invalid server mode');
78 | }
79 |
80 | $validProtocols = static::PROTOCOLS;
81 | if (defined('SWOOLE_SSL')) {
82 | $validProtocols[] = SWOOLE_SOCK_TCP | SWOOLE_SSL;
83 | $validProtocols[] = SWOOLE_SOCK_TCP6 | SWOOLE_SSL;
84 | }
85 |
86 | if (! in_array($protocol, $validProtocols, true)) {
87 | throw new Exception\InvalidArgumentException('Invalid server protocol');
88 | }
89 |
90 | $enableCoroutine = $swooleConfig['enable_coroutine'] ?? false;
91 | if ($enableCoroutine && method_exists(SwooleRuntime::class, 'enableCoroutine')) {
92 | SwooleRuntime::enableCoroutine(true);
93 | }
94 |
95 | $httpServer = new SwooleHttpServer($host, $port, $mode, $protocol);
96 | $serverOptions = $serverConfig['options'] ?? [];
97 | $httpServer->set($serverOptions);
98 |
99 | return $httpServer;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/SwooleEmitter.php:
--------------------------------------------------------------------------------
1 | swooleResponse = $response;
35 | }
36 |
37 | /**
38 | * Emits a response for the Swoole environment.
39 | *
40 | * @return void
41 | */
42 | public function emit(ResponseInterface $response) : bool
43 | {
44 | if (PHP_SAPI !== 'cli' || ! extension_loaded('swoole')) {
45 | return false;
46 | }
47 | $this->emitStatusCode($response);
48 | $this->emitHeaders($response);
49 | $this->emitCookies($response);
50 | $this->emitBody($response);
51 | return true;
52 | }
53 |
54 | /**
55 | * Emit the status code
56 | *
57 | * @return void
58 | */
59 | private function emitStatusCode(ResponseInterface $response)
60 | {
61 | $this->swooleResponse->status($response->getStatusCode());
62 | }
63 |
64 | /**
65 | * Emit the headers
66 | *
67 | * @return void
68 | */
69 | private function emitHeaders(ResponseInterface $response)
70 | {
71 | foreach ($response->withoutHeader(SetCookies::SET_COOKIE_HEADER)->getHeaders() as $name => $values) {
72 | $name = $this->filterHeader($name);
73 | $this->swooleResponse->header($name, implode(', ', $values));
74 | }
75 | }
76 |
77 | /**
78 | * Emit the message body.
79 | *
80 | * @return void
81 | */
82 | private function emitBody(ResponseInterface $response)
83 | {
84 | $body = $response->getBody();
85 | $body->rewind();
86 |
87 | if ($body->getSize() <= static::CHUNK_SIZE) {
88 | $this->swooleResponse->end($body->getContents());
89 | return;
90 | }
91 |
92 | while (! $body->eof()) {
93 | $this->swooleResponse->write($body->read(static::CHUNK_SIZE));
94 | }
95 | $this->swooleResponse->end();
96 | }
97 |
98 | /**
99 | * Emit the cookies
100 | *
101 | * @param \Psr\Http\Message\ResponseInterface $response
102 | *
103 | * @return void
104 | */
105 | private function emitCookies(ResponseInterface $response): void
106 | {
107 | foreach (SetCookies::fromResponse($response)->getAll() as $cookie) {
108 | $this->swooleResponse->cookie(
109 | $cookie->getName(),
110 | $cookie->getValue(),
111 | $cookie->getExpires(),
112 | $cookie->getPath() ?: '/',
113 | $cookie->getDomain() ?: '',
114 | $cookie->getSecure(),
115 | $cookie->getHttpOnly()
116 | );
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/Command/StartCommand.php:
--------------------------------------------------------------------------------
1 | container = $container;
52 | parent::__construct($name);
53 | }
54 |
55 | protected function configure() : void
56 | {
57 | $this->setDescription('Start the web server.');
58 | $this->setHelp(self::HELP);
59 | $this->addOption(
60 | 'daemonize',
61 | 'd',
62 | InputOption::VALUE_NONE,
63 | 'Daemonize the web server (run as a background process).'
64 | );
65 | $this->addOption(
66 | 'num-workers',
67 | 'w',
68 | InputOption::VALUE_REQUIRED,
69 | 'Number of worker processes to use.'
70 | );
71 | }
72 |
73 | protected function execute(InputInterface $input, OutputInterface $output) : int
74 | {
75 | $this->pidManager = $this->container->get(PidManager::class);
76 | if ($this->isRunning()) {
77 | $output->writeln('Server is already running!');
78 | return 1;
79 | }
80 |
81 | $serverOptions = [];
82 | $daemonize = $input->getOption('daemonize');
83 | $numWorkers = $input->getOption('num-workers');
84 | if ($daemonize) {
85 | $serverOptions['daemonize'] = $daemonize;
86 | }
87 | if (null !== $numWorkers) {
88 | $serverOptions['worker_num'] = $numWorkers;
89 | }
90 |
91 | if ([] !== $serverOptions) {
92 | $server = $this->container->get(SwooleHttpServer::class);
93 | $server->set($serverOptions);
94 | }
95 |
96 | /** @var \Zend\Expressive\Application $app */
97 | $app = $this->container->get(Application::class);
98 |
99 | /** @var \Zend\Expressive\MiddlewareFactory $factory */
100 | $factory = $this->container->get(MiddlewareFactory::class);
101 |
102 | // Execute programmatic/declarative middleware pipeline and routing
103 | // configuration statements, if they exist
104 | foreach (self::PROGRAMMATIC_CONFIG_FILES as $configFile) {
105 | if (file_exists($configFile)) {
106 | (require $configFile)($app, $factory, $this->container);
107 | }
108 | }
109 |
110 | // Run the application
111 | $app->run();
112 |
113 | return 0;
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/Log/Psr3AccessLogDecorator.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
43 | $this->formatter = $formatter;
44 | $this->useHostnameLookups = $useHostnameLookups;
45 | }
46 |
47 | public function logAccessForStaticResource(Request $request, StaticResourceResponse $response) : void
48 | {
49 | $message = $this->formatter->format(
50 | AccessLogDataMap::createWithStaticResource($request, $response, $this->useHostnameLookups)
51 | );
52 | $response->getStatus() >= 400
53 | ? $this->logger->error($message)
54 | : $this->logger->info($message);
55 | }
56 |
57 | public function logAccessForPsr7Resource(Request $request, ResponseInterface $response) : void
58 | {
59 | $message = $this->formatter->format(
60 | AccessLogDataMap::createWithPsrResponse($request, $response, $this->useHostnameLookups)
61 | );
62 | $response->getStatusCode() >= 400
63 | ? $this->logger->error($message)
64 | : $this->logger->info($message);
65 | }
66 |
67 | /**
68 | * {@inheritDoc}
69 | */
70 | public function emergency($message, array $context = [])
71 | {
72 | $this->logger->emergency($message, $context);
73 | }
74 |
75 | /**
76 | * {@inheritDoc}
77 | */
78 | public function alert($message, array $context = [])
79 | {
80 | $this->logger->alert($message, $context);
81 | }
82 |
83 | /**
84 | * {@inheritDoc}
85 | */
86 | public function critical($message, array $context = [])
87 | {
88 | $this->logger->critical($message, $context);
89 | }
90 |
91 | /**
92 | * {@inheritDoc}
93 | */
94 | public function error($message, array $context = [])
95 | {
96 | $this->logger->error($message, $context);
97 | }
98 |
99 | /**
100 | * {@inheritDoc}
101 | */
102 | public function warning($message, array $context = [])
103 | {
104 | $this->logger->warning($message, $context);
105 | }
106 |
107 | /**
108 | * {@inheritDoc}
109 | */
110 | public function notice($message, array $context = [])
111 | {
112 | $this->logger->notice($message, $context);
113 | }
114 |
115 | /**
116 | * {@inheritDoc}
117 | */
118 | public function info($message, array $context = [])
119 | {
120 | $this->logger->info($message, $context);
121 | }
122 |
123 | /**
124 | * {@inheritDoc}
125 | */
126 | public function debug($message, array $context = [])
127 | {
128 | $this->logger->debug($message, $context);
129 | }
130 |
131 | /**
132 | * {@inheritDoc}
133 | */
134 | public function log($level, $message, array $context = [])
135 | {
136 | $this->logger->log($level, $message, $context);
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/ConfigProvider.php:
--------------------------------------------------------------------------------
1 | $this->getDependencies()]
27 | : [];
28 |
29 | $config['zend-expressive-swoole'] = $this->getDefaultConfig();
30 |
31 | return $config;
32 | }
33 |
34 | public function getDefaultConfig() : array
35 | {
36 | return [
37 | 'swoole-http-server' => [
38 | // A prefix for the process name of the master process and workers.
39 | // By default the master process will be named `expressive-master`,
40 | // each http worker `expressive-worker-n` and each task worker
41 | // `expressive-task-worker-n` where n is the id of the worker
42 | 'process-name' => 'expressive',
43 | 'options' => [
44 | // We set a default for this. Without one, Swoole\Http\Server
45 | // defaults to the value of `ulimit -n`. Unfortunately, in
46 | // virtualized or containerized environments, this often
47 | // reports higher than the host container allows. 1024 is a
48 | // sane default; users should check their host system, however,
49 | // and set a production value to match.
50 | 'max_conn' => 1024,
51 | ],
52 | 'static-files' => [
53 | 'enable' => true,
54 | ],
55 | ],
56 | ];
57 | }
58 |
59 | public function getDependencies() : array
60 | {
61 | return [
62 | 'factories' => [
63 | Command\ReloadCommand::class => Command\ReloadCommandFactory::class,
64 | Command\StartCommand::class => Command\StartCommandFactory::class,
65 | Command\StatusCommand::class => Command\StatusCommandFactory::class,
66 | Command\StopCommand::class => Command\StopCommandFactory::class,
67 | Log\AccessLogInterface::class => Log\AccessLogFactory::class,
68 | Log\SwooleLoggerFactory::SWOOLE_LOGGER => Log\SwooleLoggerFactory::class,
69 | PidManager::class => PidManagerFactory::class,
70 | SwooleRequestHandlerRunner::class => SwooleRequestHandlerRunnerFactory::class,
71 | ServerRequestInterface::class => ServerRequestSwooleFactory::class,
72 | StaticResourceHandler::class => StaticResourceHandlerFactory::class,
73 | SwooleHttpServer::class => HttpServerFactory::class,
74 | Reloader::class => ReloaderFactory::class,
75 | ],
76 | 'aliases' => [
77 | RequestHandlerRunner::class => SwooleRequestHandlerRunner::class,
78 | StaticResourceHandlerInterface::class => StaticResourceHandler::class,
79 | FileWatcherInterface::class => InotifyFileWatcher::class,
80 | ],
81 | 'delegators' => [
82 | 'Zend\Expressive\WhoopsPageHandler' => [
83 | WhoopsPrettyPageHandlerDelegator::class,
84 | ],
85 | ],
86 | ];
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/StaticResourceHandler/GzipMiddleware.php:
--------------------------------------------------------------------------------
1 | 'deflate',
36 | ZLIB_ENCODING_GZIP => 'gzip',
37 | ];
38 |
39 | /**
40 | * @var int
41 | */
42 | private $compressionLevel;
43 |
44 | /**
45 | * @param int $compressionLevel Compression level to use. Values less than
46 | * 1 indicate no compression should occur.
47 | * @throws Exception\InvalidArgumentException for $compressionLevel values
48 | * greater than 9.
49 | */
50 | public function __construct(int $compressionLevel = 0)
51 | {
52 | if ($compressionLevel > 9) {
53 | throw new Exception\InvalidArgumentException(sprintf(
54 | '%s only allows compression levels up to 9; received %d',
55 | __CLASS__,
56 | $compressionLevel
57 | ));
58 | }
59 | $this->compressionLevel = $compressionLevel;
60 | }
61 |
62 | /**
63 | * {@inheritDoc}
64 | */
65 | public function __invoke(Request $request, string $filename, callable $next): StaticResourceResponse
66 | {
67 | $response = $next($request, $filename);
68 |
69 | if (! $this->shouldCompress($request)) {
70 | return $response;
71 | }
72 |
73 | $compressionEncoding = $this->getCompressionEncoding($request);
74 | if (null === $compressionEncoding) {
75 | return $response;
76 | }
77 |
78 | $response->setResponseContentCallback(
79 | function (Response $swooleResponse, string $filename) use ($compressionEncoding, $response) : void {
80 | $swooleResponse->header(
81 | 'Content-Encoding',
82 | GzipMiddleware::COMPRESSION_CONTENT_ENCODING_MAP[$compressionEncoding],
83 | true
84 | );
85 | $swooleResponse->header('Connection', 'close', true);
86 |
87 | $handle = fopen($filename, 'rb');
88 | $params = [
89 | 'level' => $this->compressionLevel,
90 | 'window' => $compressionEncoding,
91 | 'memory' => 9
92 | ];
93 | stream_filter_append($handle, 'zlib.deflate', STREAM_FILTER_READ, $params);
94 |
95 | $countBytes = function_exists('mb_strlen') ? 'mb_strlen' : 'strlen';
96 | $length = 0;
97 | while (feof($handle) !== true) {
98 | $line = fgets($handle, 4096);
99 | $length += $countBytes($line);
100 | $swooleResponse->write($line);
101 | }
102 |
103 | fclose($handle);
104 | $response->setContentLength($length);
105 | $swooleResponse->header('Content-Length', (string) $length, true);
106 | $swooleResponse->end();
107 | }
108 | );
109 | return $response;
110 | }
111 |
112 | /**
113 | * Is gzip available for current request
114 | */
115 | private function shouldCompress(Request $request): bool
116 | {
117 | return $this->compressionLevel > 0
118 | && isset($request->header['accept-encoding']);
119 | }
120 |
121 | /**
122 | * Get gzcompress compression encoding.
123 | */
124 | private function getCompressionEncoding(Request $request) : ?int
125 | {
126 | foreach (explode(',', $request->header['accept-encoding']) as $acceptEncoding) {
127 | $acceptEncoding = trim($acceptEncoding);
128 | if ('gzip' === $acceptEncoding) {
129 | return ZLIB_ENCODING_GZIP;
130 | }
131 |
132 | if ('deflate' === $acceptEncoding) {
133 | return ZLIB_ENCODING_DEFLATE;
134 | }
135 | }
136 | return null;
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/StaticResourceHandler/ETagMiddleware.php:
--------------------------------------------------------------------------------
1 | validateRegexList($etagDirectives, 'ETag');
60 | if (! in_array($etagValidationType, $this->allowedETagValidationTypes, true)) {
61 | throw new Exception\InvalidArgumentException(sprintf(
62 | 'ETag validation type must be one of [%s]; received "%s"',
63 | implode(', ', $this->allowedETagValidationTypes),
64 | $etagValidationType
65 | ));
66 | }
67 |
68 | $this->etagDirectives = $etagDirectives;
69 | $this->etagValidationType = $etagValidationType;
70 | }
71 |
72 | /**
73 | * {@inheritDoc}
74 | */
75 | public function __invoke(Request $request, string $filename, callable $next) : StaticResourceResponse
76 | {
77 | $response = $next($request, $filename);
78 |
79 | if (! $this->getETagFlagForPath($request->server['request_uri'])) {
80 | return $response;
81 | }
82 |
83 | return $this->prepareETag($request, $filename, $response);
84 | }
85 |
86 | private function getETagFlagForPath(string $path) : bool
87 | {
88 | foreach ($this->etagDirectives as $regexp) {
89 | if (preg_match($regexp, $path)) {
90 | return true;
91 | }
92 | }
93 | return false;
94 | }
95 |
96 | /**
97 | * @return bool Returns true if the request issued an if-match and/or
98 | * if-none-match header with a matching ETag; in such cases, a 304
99 | * status is emitted with no content. Boolean false indicates the file
100 | * content should be provided, assuming other conditions require it as
101 | * well.
102 | */
103 | private function prepareETag(
104 | Request $request,
105 | string $filename,
106 | StaticResourceResponse $response
107 | ) : StaticResourceResponse {
108 | $etag = '';
109 | $lastModified = filemtime($filename) ?? 0;
110 | switch ($this->etagValidationType) {
111 | case self::ETAG_VALIDATION_WEAK:
112 | $filesize = filesize($filename) ?? 0;
113 | if (! $lastModified || ! $filesize) {
114 | return $response;
115 | }
116 | $etag = sprintf('W/"%x-%x"', $lastModified, $filesize);
117 | break;
118 | case self::ETAG_VALIDATION_STRONG:
119 | $etag = md5_file($filename);
120 | break;
121 | default:
122 | return $response;
123 | }
124 |
125 | $response->addHeader('ETag', $etag);
126 |
127 | // Determine if ETag the client expects matches calculated ETag
128 | $ifMatch = $request->header['if-match'] ?? '';
129 | $ifNoneMatch = $request->header['if-none-match'] ?? '';
130 | $clientEtags = explode(',', $ifMatch ?: $ifNoneMatch);
131 | array_walk($clientEtags, 'trim');
132 |
133 | if (in_array($etag, $clientEtags, true)) {
134 | $response->setStatus(304);
135 | $response->disableContent();
136 | }
137 |
138 | return $response;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/StaticResourceHandler/CacheControlMiddleware.php:
--------------------------------------------------------------------------------
1 | validateCacheControlDirectives($cacheControlDirectives);
49 | $this->cacheControlDirectives = $cacheControlDirectives;
50 | }
51 |
52 | public function __invoke(Request $request, string $filename, callable $next) : StaticResourceResponse
53 | {
54 | $response = $next($request, $filename);
55 | $cacheControl = $this->getCacheControlForPath($request->server['request_uri']);
56 | if ($cacheControl) {
57 | $response->addHeader('Cache-Control', $cacheControl);
58 | }
59 | return $response;
60 | }
61 |
62 | /**
63 | * @throws Exception\InvalidArgumentException if any Cache-Control regex is invalid
64 | * @throws Exception\InvalidArgumentException if any individual directive
65 | * associated with a regex is invalid.
66 | */
67 | private function validateCacheControlDirectives(array $cacheControlDirectives) : void
68 | {
69 | foreach ($cacheControlDirectives as $regex => $directives) {
70 | if (! $this->isValidRegex($regex)) {
71 | throw new Exception\InvalidArgumentException(sprintf(
72 | 'The Cache-Control regex "%s" is invalid',
73 | $regex
74 | ));
75 | }
76 |
77 | if (! is_array($directives)) {
78 | throw new Exception\InvalidArgumentException(sprintf(
79 | 'The Cache-Control directives associated with the regex "%s" are invalid;'
80 | . ' each must be an array of strings',
81 | $regex
82 | ));
83 | }
84 |
85 | array_walk($directives, function ($directive) use ($regex) {
86 | if (! is_string($directive)) {
87 | throw new Exception\InvalidArgumentException(sprintf(
88 | 'One or more Cache-Control directives associated with the regex "%s" are invalid;'
89 | . ' each must be a string',
90 | $regex
91 | ));
92 | }
93 | $this->validateCacheControlDirective($regex, $directive);
94 | });
95 | }
96 | }
97 |
98 | /**
99 | * @throws Exception\InvalidArgumentException if any regexp is invalid
100 | */
101 | private function validateCacheControlDirective(string $regex, string $directive) : void
102 | {
103 | if (in_array($directive, self::CACHECONTROL_DIRECTIVES, true)) {
104 | return;
105 | }
106 | if (preg_match('/^max-age=\d+$/', $directive)) {
107 | return;
108 | }
109 | throw new Exception\InvalidArgumentException(sprintf(
110 | 'The Cache-Control directive "%s" associated with regex "%s" is invalid.'
111 | . ' Must be one of [%s] or match /^max-age=\d+$/',
112 | $directive,
113 | $regex,
114 | implode(', ', self::CACHECONTROL_DIRECTIVES)
115 | ));
116 | }
117 |
118 | /**
119 | * @return null|string Returns null if the path does not have any
120 | * associated cache-control directives; otherwise, it will
121 | * return a string representing the entire Cache-Control
122 | * header value to emit.
123 | */
124 | private function getCacheControlForPath(string $path) : ?string
125 | {
126 | foreach ($this->cacheControlDirectives as $regexp => $values) {
127 | if (preg_match($regexp, $path)) {
128 | return implode(', ', $values);
129 | }
130 | }
131 | return null;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/StaticResourceHandler/ContentTypeFilterMiddleware.php:
--------------------------------------------------------------------------------
1 | 'application/x-7z-compressed',
40 | 'aac' => 'audio/aac',
41 | 'arc' => 'application/octet-stream',
42 | 'avi' => 'video/x-msvideo',
43 | 'azw' => 'application/vnd.amazon.ebook',
44 | 'bin' => 'application/octet-stream',
45 | 'bmp' => 'image/bmp',
46 | 'bz' => 'application/x-bzip',
47 | 'bz2' => 'application/x-bzip2',
48 | 'css' => 'text/css',
49 | 'csv' => 'text/csv',
50 | 'doc' => 'application/msword',
51 | 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
52 | 'eot' => 'application/vnd.ms-fontobject',
53 | 'epub' => 'application/epub+zip',
54 | 'es' => 'application/ecmascript',
55 | 'gif' => 'image/gif',
56 | 'htm' => 'text/html',
57 | 'html' => 'text/html',
58 | 'ico' => 'image/x-icon',
59 | 'jpg' => 'image/jpg',
60 | 'jpeg' => 'image/jpg',
61 | 'js' => 'text/javascript',
62 | 'json' => 'application/json',
63 | 'mp4' => 'video/mp4',
64 | 'mpeg' => 'video/mpeg',
65 | 'odp' => 'application/vnd.oasis.opendocument.presentation',
66 | 'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
67 | 'odt' => 'application/vnd.oasis.opendocument.text',
68 | 'oga' => 'audio/ogg',
69 | 'ogv' => 'video/ogg',
70 | 'ogx' => 'application/ogg',
71 | 'otf' => 'font/otf',
72 | 'pdf' => 'application/pdf',
73 | 'png' => 'image/png',
74 | 'ppt' => 'application/vnd.ms-powerpoint',
75 | 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
76 | 'rar' => 'application/x-rar-compressed',
77 | 'rtf' => 'application/rtf',
78 | 'svg' => 'image/svg+xml',
79 | 'swf' => 'application/x-shockwave-flash',
80 | 'tar' => 'application/x-tar',
81 | 'tif' => 'image/tiff',
82 | 'tiff' => 'image/tiff',
83 | 'ts' => 'application/typescript',
84 | 'ttf' => 'font/ttf',
85 | 'txt' => 'text/plain',
86 | 'wav' => 'audio/wav',
87 | 'weba' => 'audio/webm',
88 | 'webm' => 'video/webm',
89 | 'webp' => 'image/webp',
90 | 'woff' => 'font/woff',
91 | 'woff2' => 'font/woff2',
92 | 'xhtml' => 'application/xhtml+xml',
93 | 'xls' => 'application/vnd.ms-excel',
94 | 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
95 | 'xml' => 'application/xml',
96 | 'xul' => 'application/vnd.mozilla.xul+xml',
97 | 'zip' => 'application/zip',
98 | ];
99 |
100 | /**
101 | * Cache the file extensions (type) for valid static file
102 | *
103 | * @var array
104 | */
105 | private $cacheTypeFile = [];
106 |
107 | /**
108 | * @var array[string, string] Extension => mimetype map
109 | */
110 | private $typeMap;
111 |
112 | /**
113 | * @param null|array[string, string] $typeMap Map of extensions to Content-Type
114 | * values. If `null` is provided, the default list in TYPE_MAP_DEFAULT will
115 | * be used. Otherwise, the list provided is used verbatim.
116 | */
117 | public function __construct(array $typeMap = null)
118 | {
119 | $this->typeMap = null === $typeMap ? self::TYPE_MAP_DEFAULT : $typeMap;
120 | }
121 |
122 | /**
123 | * {@inheritDoc}
124 | */
125 | public function __invoke(Request $request, string $filename, callable $next): StaticResourceResponse
126 | {
127 | if (! isset($this->cacheTypeFile[$filename])
128 | && ! $this->cacheFile($filename)
129 | ) {
130 | $response = new StaticResourceResponse();
131 | $response->markAsFailure();
132 | return $response;
133 | }
134 |
135 | $response = $next($request, $filename);
136 | $response->addHeader('Content-Type', $this->cacheTypeFile[$filename]);
137 | return $response;
138 | }
139 |
140 | /**
141 | * Attempt to cache a static file resource.
142 | */
143 | private function cacheFile(string $fileName) : bool
144 | {
145 | $type = pathinfo($fileName, PATHINFO_EXTENSION);
146 | if (! isset($this->typeMap[$type])) {
147 | return false;
148 | }
149 |
150 | if (! file_exists($fileName)) {
151 | return false;
152 | }
153 |
154 | $this->cacheTypeFile[$fileName] = $this->typeMap[$type];
155 | return true;
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/StaticResourceHandler/StaticResourceResponse.php:
--------------------------------------------------------------------------------
1 | status = $status;
64 | $this->headers = $headers;
65 | $this->sendContent = $sendContent;
66 | $this->responseContentCallback = $responseContentCallback
67 | ?: function (SwooleHttpResponse $response, string $filename) : void {
68 | $this->contentLength = filesize($filename);
69 | $response->header('Content-Length', (string) $this->contentLength, true);
70 | $response->sendfile($filename);
71 | };
72 | }
73 |
74 | public function addHeader(string $name, string $value) : void
75 | {
76 | $this->headers[$name] = $value;
77 | }
78 |
79 | /**
80 | * Retrieve the content length that was sent via sendSwooleResponse()
81 | *
82 | * This is exposed to allow logging the content emitted.
83 | */
84 | public function getContentLength() : int
85 | {
86 | return $this->contentLength;
87 | }
88 |
89 | public function disableContent() : void
90 | {
91 | $this->sendContent = false;
92 | }
93 |
94 | /**
95 | * Retrieve a single named header
96 | *
97 | * This is exposed to allow logging specific response headers when present.
98 | */
99 | public function getHeader(string $name) : string
100 | {
101 | return $this->headers[$name] ?? '';
102 | }
103 |
104 | /**
105 | * Retrieve the aggregated length of all headers.
106 | *
107 | * This is exposed for logging purposes.
108 | */
109 | public function getHeaderSize() : int
110 | {
111 | $headers = [];
112 | foreach ($this->headers as $header => $value) {
113 | $headers[] = sprintf('%s: %s', $header, $value);
114 | }
115 |
116 | $strlen = function_exists('mb_strlen') ? 'mb_strlen' : 'strlen';
117 |
118 | return $strlen(implode("\r\n", $headers));
119 | }
120 |
121 | /**
122 | * Retrieve the response status code that was sent via sendSwooleResponse()
123 | *
124 | * This is exposed to allow logging the status code emitted.
125 | */
126 | public function getStatus() : int
127 | {
128 | return $this->status;
129 | }
130 |
131 | /**
132 | * Can the requested resource be served?
133 | */
134 | public function isFailure() : bool
135 | {
136 | return $this->isFailure;
137 | }
138 |
139 | /**
140 | * Indicate that the requested resource cannot be served.
141 | *
142 | * Call this method if the requested resource does not exist, or if it
143 | * fails certain validation checks.
144 | */
145 | public function markAsFailure() : void
146 | {
147 | $this->isFailure = true;
148 | }
149 |
150 | /**
151 | * Send the Swoole HTTP Response representing the static resource.
152 | *
153 | * Emits the status code and all headers, and then uses the composed
154 | * content callback to emit the content.
155 | *
156 | * If content has been disabled, it calls $response->end() instead of the
157 | * content callback.
158 | */
159 | public function sendSwooleResponse(SwooleHttpResponse $response, string $filename) : void
160 | {
161 | $response->status($this->status);
162 | foreach ($this->headers as $header => $value) {
163 | $response->header($header, $value, true);
164 | }
165 |
166 | $contentSender = $this->responseContentCallback;
167 |
168 | $this->sendContent ? $contentSender($response, $filename) : $response->end();
169 | }
170 |
171 | public function setContentLength(int $length) : void
172 | {
173 | $this->contentLength = $length;
174 | }
175 |
176 | /**
177 | * @param callable $responseContentCallback Callback to use when emitting
178 | * the response body content via Swoole. Must have the signature:
179 | * function (SwooleHttpResponse $response, string $filename) : void
180 | */
181 | public function setResponseContentCallback(callable $callback) : void
182 | {
183 | $this->responseContentCallback = $callback;
184 | }
185 |
186 | public function setStatus(int $status) : void
187 | {
188 | $this->status = $status;
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/Log/AccessLogFormatter.php:
--------------------------------------------------------------------------------
1 | s %b';
23 | public const FORMAT_COMMON_VHOST = '%v %h %l %u %t "%r" %>s %b';
24 | public const FORMAT_COMBINED = '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"';
25 | public const FORMAT_REFERER = '%{Referer}i -> %U';
26 | public const FORMAT_AGENT = '%{User-Agent}i';
27 |
28 | /**
29 | * @link https://httpd.apache.org/docs/2.4/logs.html#virtualhost
30 | */
31 | public const FORMAT_VHOST = '%v %l %u %t "%r" %>s %b';
32 |
33 | /**
34 | * @link https://anonscm.debian.org/cgit/pkg-apache/apache2.git/tree/debian/config-dir/apache2.conf.in#n212
35 | * @codingStandardsIgnoreStart
36 | * phpcs:disable
37 | */
38 | public const FORMAT_COMMON_DEBIAN = '%h %l %u %t “%r” %>s %O';
39 | public const FORMAT_COMBINED_DEBIAN = '%h %l %u %t “%r” %>s %O “%{Referer}i” “%{User-Agent}i”';
40 | public const FORMAT_VHOST_COMBINED_DEBIAN = '%v:%p %h %l %u %t “%r” %>s %O “%{Referer}i” “%{User-Agent}i"';
41 | // @codingStandardsIgnoreEnd
42 | // phpcs:enable
43 |
44 | /**
45 | * Message format to use when generating a log message.
46 | *
47 | * @var string
48 | */
49 | private $format;
50 |
51 | public function __construct(string $format = self::FORMAT_COMMON)
52 | {
53 | $this->format = $format;
54 | }
55 |
56 | /**
57 | * Transform a log format to the final string to log.
58 | */
59 | public function format(AccessLogDataMap $map) : string
60 | {
61 | $message = $this->replaceConstantDirectives($this->format, $map);
62 | $message = $this->replaceVariableDirectives($message, $map);
63 | return $message;
64 | }
65 |
66 | private function replaceConstantDirectives(
67 | string $format,
68 | AccessLogDataMap $map
69 | ) : string {
70 | return preg_replace_callback(
71 | '/%(?:[<>])?([%aABbDfhHklLmpPqrRstTuUvVXIOS])/',
72 | function (array $matches) use ($map) {
73 | switch ($matches[1]) {
74 | case '%':
75 | return '%';
76 | case 'a':
77 | return $map->getClientIp();
78 | case 'A':
79 | return $map->getLocalIp();
80 | case 'B':
81 | return $map->getBodySize('0');
82 | case 'b':
83 | return $map->getBodySize('-');
84 | case 'D':
85 | return $map->getRequestDuration('ms');
86 | case 'f':
87 | return $map->getFilename();
88 | case 'h':
89 | return $map->getRemoteHostname();
90 | case 'H':
91 | return $map->getProtocol();
92 | case 'm':
93 | return $map->getMethod();
94 | case 'p':
95 | return $map->getPort('canonical');
96 | case 'q':
97 | return $map->getQuery();
98 | case 'r':
99 | return $map->getRequestLine();
100 | case 's':
101 | return $map->getStatus();
102 | case 't':
103 | return $map->getRequestTime('begin:%d/%b/%Y:%H:%M:%S %z');
104 | case 'T':
105 | return $map->getRequestDuration('s');
106 | case 'u':
107 | return $map->getRemoteUser();
108 | case 'U':
109 | return $map->getPath();
110 | case 'v':
111 | return $map->getHost();
112 | case 'V':
113 | return $map->getServerName();
114 | case 'I':
115 | return $map->getRequestMessageSize('-');
116 | case 'O':
117 | return $map->getResponseMessageSize('-');
118 | case 'S':
119 | return $map->getTransferredSize();
120 | //NOT IMPLEMENTED
121 | case 'k':
122 | case 'l':
123 | case 'L':
124 | case 'P':
125 | case 'R':
126 | case 'X':
127 | default:
128 | return '-';
129 | }
130 | },
131 | $format
132 | );
133 | }
134 |
135 | private function replaceVariableDirectives(
136 | string $format,
137 | AccessLogDataMap $map
138 | ): string {
139 | return preg_replace_callback(
140 | '/%(?:[<>])?{([^}]+)}([aCeinopPtT])/',
141 | function (array $matches) use ($map) {
142 | switch ($matches[2]) {
143 | case 'a':
144 | return $map->getClientIp();
145 | case 'C':
146 | return $map->getCookie($matches[1]);
147 | case 'e':
148 | return $map->getEnv($matches[1]);
149 | case 'i':
150 | return $map->getRequestHeader($matches[1]);
151 | case 'o':
152 | return $map->getResponseHeader($matches[1]);
153 | case 'p':
154 | return $map->getPort($matches[1]);
155 | case 't':
156 | return $map->getRequestTime($matches[1]);
157 | case 'T':
158 | return $map->getRequestDuration($matches[1]);
159 | //NOT IMPLEMENTED
160 | case 'n':
161 | case 'P':
162 | default:
163 | return '-';
164 | }
165 | },
166 | $format
167 | );
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/SwooleStream.php:
--------------------------------------------------------------------------------
1 | request = $request;
55 | }
56 |
57 | /**
58 | * {@inheritdoc}
59 | */
60 | public function getContents()
61 | {
62 | // If we're at the end of the string, return an empty string.
63 | if ($this->eof()) {
64 | return '';
65 | }
66 |
67 | $size = $this->getSize();
68 | // If we have not content, return an empty string
69 | if ($size === 0) {
70 | return '';
71 | }
72 |
73 | // Memoize index so we can use it to get a substring later,
74 | // if required.
75 | $index = $this->index;
76 |
77 | // Set the internal index to the end of the string
78 | $this->index = $size - 1;
79 |
80 | if ($index) {
81 | // Per PSR-7 spec, if we have seeked or read to a given position in
82 | // the string, we should only return the contents from that position
83 | // forward.
84 | return substr($this->body, $index);
85 | }
86 |
87 | // If we're at the start of the content, return all of it.
88 | return $this->body;
89 | }
90 |
91 | /**
92 | * {@inheritdoc}
93 | */
94 | public function __toString()
95 | {
96 | $this->body !== null || $this->initRawContent();
97 | return $this->body;
98 | }
99 |
100 | /**
101 | * {@inheritdoc}
102 | */
103 | public function getSize()
104 | {
105 | if (null === $this->bodySize) {
106 | $this->body !== null || $this->initRawContent();
107 | $this->bodySize = strlen($this->body);
108 | }
109 | return $this->bodySize;
110 | }
111 |
112 | /**
113 | * {@inheritdoc}
114 | */
115 | public function tell()
116 | {
117 | return $this->index;
118 | }
119 |
120 | /**
121 | * {@inheritdoc}
122 | */
123 | public function eof()
124 | {
125 | return $this->index >= $this->getSize() - 1;
126 | }
127 |
128 | /**
129 | * {@inheritdoc}
130 | */
131 | public function isReadable()
132 | {
133 | return true;
134 | }
135 |
136 | /**
137 | * {@inheritdoc}
138 | */
139 | public function read($length)
140 | {
141 | $this->body !== null || $this->initRawContent();
142 | $result = substr($this->body, $this->index, $length);
143 |
144 | // Reset index based on legnth; should not be > EOF position.
145 | $size = $this->getSize();
146 | $this->index = $this->index + $length >= $size
147 | ? $size - 1
148 | : $this->index + $length;
149 |
150 | return $result;
151 | }
152 |
153 | /**
154 | * {@inheritdoc}
155 | */
156 | public function isSeekable()
157 | {
158 | return true;
159 | }
160 |
161 | /**
162 | * {@inheritdoc}
163 | */
164 | public function seek($offset, $whence = SEEK_SET)
165 | {
166 | $size = $this->getSize();
167 | switch ($whence) {
168 | case SEEK_SET:
169 | if ($offset >= $size) {
170 | throw new RuntimeException(
171 | 'Offset cannot be longer than content size'
172 | );
173 | }
174 | $this->index = $offset;
175 | break;
176 | case SEEK_CUR:
177 | if ($offset + $this->index >= $size) {
178 | throw new RuntimeException(
179 | 'Offset + current position cannot be longer than content size when using SEEK_CUR'
180 | );
181 | }
182 | $this->index += $offset;
183 | break;
184 | case SEEK_END:
185 | if ($offset + $size >= $size) {
186 | throw new RuntimeException(
187 | 'Offset must be a negative number to be under the content size when using SEEK_END'
188 | );
189 | }
190 | $this->index = ($size - 1) + $offset;
191 | break;
192 | default:
193 | throw new InvalidArgumentException(
194 | 'Invalid $whence argument provided; must be one of SEEK_CUR,'
195 | . 'SEEK_END, or SEEK_SET'
196 | );
197 | }
198 | }
199 |
200 | /**
201 | * {@inheritdoc}
202 | */
203 | public function rewind()
204 | {
205 | $this->index = 0;
206 | }
207 |
208 | /**
209 | * {@inheritdoc}
210 | */
211 | public function isWritable()
212 | {
213 | return false;
214 | }
215 |
216 | /**
217 | * {@inheritdoc}
218 | */
219 | public function write($string)
220 | {
221 | throw new RuntimeException('Stream is not writable');
222 | }
223 |
224 | /**
225 | * {@inheritdoc}
226 | */
227 | public function getMetadata($key = null)
228 | {
229 | return $key ? null : [];
230 | }
231 |
232 | /**
233 | * {@inheritdoc}
234 | */
235 | public function detach()
236 | {
237 | return $this->request;
238 | }
239 |
240 | /**
241 | * {@inheritdoc}
242 | */
243 | public function close()
244 | {
245 | }
246 |
247 | /**
248 | * Memoize the request raw content in the $body property, if not already done.
249 | */
250 | private function initRawContent() : void
251 | {
252 | if ($this->body) {
253 | return;
254 | }
255 | $this->body = $this->request->rawContent() ?: '';
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/src/StaticResourceHandlerFactory.php:
--------------------------------------------------------------------------------
1 |
23 | * 'zend-expressive-swoole' => [
24 | * 'swoole-http-server' => [
25 | * 'static-files' => [
26 | * 'document-root' => '/path/to/static/files/to/serve', // usu getcwd() . /public/
27 | * 'type-map' => [
28 | * // extension => mimetype pairs of types to cache.
29 | * // A default list exists if none is provided.
30 | * ],
31 | * 'clearstatcache-interval' => 3600, // How often a worker should clear the
32 | * // filesystem stat cache. If not provided,
33 | * // it will never clear it. Value should be
34 | * // an integer indicating number of seconds
35 | * // between clear operations. 0 or negative
36 | * // values will clear on every request.
37 | * 'etag-type' => 'weak|strong', // ETag algorithm type to use, if any
38 | * 'gzip' => [
39 | * 'level' => 4, // Integer between 1 and 9 indicating compression level to use.
40 | * // Values less than 1 disable compression.
41 | * ],
42 | * 'directives' => [
43 | * // Rules governing which server-side caching headers are emitted.
44 | * // Each key must be a valid regular expression, and should match
45 | * // typically only file extensions, but potentially full paths.
46 | * // When a static resource matches, all associated rules will apply.
47 | * 'regex' => [
48 | * 'cache-control' => [
49 | * // one or more valid Cache-Control directives:
50 | * // - must-revalidate
51 | * // - no-cache
52 | * // - no-store
53 | * // - no-transform
54 | * // - public
55 | * // - private
56 | * // - max-age=\d+
57 | * ],
58 | * 'last-modified' => bool, // Emit a Last-Modified header?
59 | * 'etag' => bool, // Emit an ETag header?
60 | * ],
61 | * ],
62 | * ],
63 | * ],
64 | * ],
65 | *
66 | */
67 | class StaticResourceHandlerFactory
68 | {
69 | public function __invoke(ContainerInterface $container) : StaticResourceHandler
70 | {
71 | $config = $container->get('config')['zend-expressive-swoole']['swoole-http-server']['static-files'] ?? [];
72 |
73 | return new StaticResourceHandler(
74 | $config['document-root'] ?? getcwd() . '/public',
75 | $this->configureMiddleware($config)
76 | );
77 | }
78 |
79 | /**
80 | * Prepare the list of middleware based on configuration provided.
81 | *
82 | * Examines the configuration provided and uses it to create the list of
83 | * middleware to return. By default, the following are always present:
84 | *
85 | * - MethodNotAllowedMiddleware
86 | * - OptionsMiddleware
87 | * - HeadMiddleware
88 | *
89 | * If the clearstatcache-interval setting is present and non-false, it is
90 | * used to seed a ClearStatCacheMiddleware instance.
91 | *
92 | * If any cache-control directives are discovered, they are used to seed a
93 | * CacheControlMiddleware instance.
94 | *
95 | * If any last-modified directives are discovered, they are used to seed a
96 | * LastModifiedMiddleware instance.
97 | *
98 | * If any etag directives are discovered, they are used to seed a
99 | * ETagMiddleware instance.
100 | *
101 | * This method is marked protected to allow users to extend this factory
102 | * in order to provide their own middleware and/or configuration schema.
103 | *
104 | * @return StaticResourceHandler\MiddlewareInterface[]
105 | */
106 | protected function configureMiddleware(array $config) : array
107 | {
108 | $middleware = [
109 | new StaticResourceHandler\ContentTypeFilterMiddleware(
110 | $config['type-map'] ?? StaticResourceHandler\ContentTypeFilterMiddleware::TYPE_MAP_DEFAULT
111 | ),
112 | new StaticResourceHandler\MethodNotAllowedMiddleware(),
113 | new StaticResourceHandler\OptionsMiddleware(),
114 | new StaticResourceHandler\HeadMiddleware(),
115 | ];
116 |
117 | $compressionLevel = $config['gzip']['level'] ?? 0;
118 | if ($compressionLevel > 0) {
119 | $middleware[] = new StaticResourceHandler\GzipMiddleware($compressionLevel);
120 | }
121 |
122 | $clearStatCacheInterval = $config['clearstatcache-interval'] ?? false;
123 | if ($clearStatCacheInterval) {
124 | $middleware[] = new StaticResourceHandler\ClearStatCacheMiddleware((int) $clearStatCacheInterval);
125 | }
126 |
127 | $directiveList = $config['directives'] ?? [];
128 | $cacheControlDirectives = [];
129 | $lastModifiedDirectives = [];
130 | $etagDirectives = [];
131 |
132 | foreach ($directiveList as $regex => $directives) {
133 | if (isset($directives['cache-control'])) {
134 | $cacheControlDirectives[$regex] = $directives['cache-control'];
135 | }
136 | if (isset($directives['last-modified'])) {
137 | $lastModifiedDirectives[] = $regex;
138 | }
139 | if (isset($directives['etag'])) {
140 | $etagDirectives[] = $regex;
141 | }
142 | }
143 |
144 | if ($cacheControlDirectives !== []) {
145 | $middleware[] = new StaticResourceHandler\CacheControlMiddleware($cacheControlDirectives);
146 | }
147 |
148 | if ($lastModifiedDirectives !== []) {
149 | $middleware[] = new StaticResourceHandler\LastModifiedMiddleware($lastModifiedDirectives);
150 | }
151 |
152 | if ($etagDirectives !== []) {
153 | $middleware[] = new StaticResourceHandler\ETagMiddleware(
154 | $etagDirectives,
155 | $config['etag-type'] ?? StaticResourceHandler\ETagMiddleware::ETAG_VALIDATION_WEAK
156 | );
157 | }
158 |
159 | return $middleware;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/SwooleRequestHandlerRunner.php:
--------------------------------------------------------------------------------
1 | handler = $handler;
129 |
130 | // Factories are cast as Closures to ensure return type safety.
131 | $this->serverRequestFactory = function ($request) use ($serverRequestFactory) : ServerRequestInterface {
132 | return $serverRequestFactory($request);
133 | };
134 |
135 | $this->serverRequestErrorResponseGenerator =
136 | function (Throwable $exception) use ($serverRequestErrorResponseGenerator) : ResponseInterface {
137 | return $serverRequestErrorResponseGenerator($exception);
138 | };
139 |
140 | // The HTTP server should not yet be running
141 | if ($httpServer->master_pid > 0 || $httpServer->manager_pid > 0) {
142 | throw new Exception\InvalidArgumentException('The Swoole server has already been started');
143 | }
144 | $this->httpServer = $httpServer;
145 | $this->pidManager = $pidManager;
146 | $this->staticResourceHandler = $staticResourceHandler;
147 | $this->logger = $logger ?: new Log\Psr3AccessLogDecorator(
148 | new Log\StdoutLogger(),
149 | new Log\AccessLogFormatter()
150 | );
151 | $this->processName = $processName;
152 | $this->hotCodeReloader = $hotCodeReloader;
153 | $this->cwd = getcwd();
154 | }
155 |
156 | /**
157 | * Run the application
158 | *
159 | * Determines which action was requested from the command line, and then
160 | * executes the task associated with it. If no action was provided, it
161 | * assumes "start".
162 | */
163 | public function run() : void
164 | {
165 | $this->httpServer->on('start', [$this, 'onStart']);
166 | $this->httpServer->on('workerstart', [$this, 'onWorkerStart']);
167 | $this->httpServer->on('request', [$this, 'onRequest']);
168 | $this->httpServer->on('shutdown', [$this, 'onShutdown']);
169 | $this->httpServer->start();
170 | }
171 |
172 | /**
173 | * Handle a start event for swoole HTTP server manager process.
174 | *
175 | * Writes the master and manager PID values to the PidManager, and ensures
176 | * the manager and/or workers use the same PWD as the master process.
177 | */
178 | public function onStart(SwooleHttpServer $server) : void
179 | {
180 | $this->pidManager->write($server->master_pid, $server->manager_pid);
181 |
182 | // Reset CWD
183 | chdir($this->cwd);
184 | $this->setProcessName(sprintf('%s-master', $this->processName));
185 |
186 | $this->logger->notice('Swoole is running at {host}:{port}, in {cwd}', [
187 | 'host' => $server->host,
188 | 'port' => $server->port,
189 | 'cwd' => getcwd(),
190 | ]);
191 | }
192 |
193 | /**
194 | * Handle a workerstart event for swoole HTTP server worker process
195 | *
196 | * Ensures workers all use the same PWD as the master process.
197 | */
198 | public function onWorkerStart(SwooleHttpServer $server, int $workerId) : void
199 | {
200 | // Reset CWD
201 | chdir($this->cwd);
202 |
203 | $processName = $workerId >= $server->setting['worker_num']
204 | ? sprintf('%s-task-worker-%d', $this->processName, $workerId)
205 | : sprintf('%s-worker-%d', $this->processName, $workerId);
206 | $this->setProcessName($processName);
207 |
208 | if ($this->hotCodeReloader) {
209 | $this->hotCodeReloader->onWorkerStart($server, $workerId);
210 | }
211 |
212 | $this->logger->notice('Worker started in {cwd} with ID {pid}', [
213 | 'cwd' => getcwd(),
214 | 'pid' => $workerId,
215 | ]);
216 | }
217 |
218 | /**
219 | * Handle an incoming HTTP request
220 | */
221 | public function onRequest(
222 | SwooleHttpRequest $request,
223 | SwooleHttpResponse $response
224 | ) : void {
225 | $staticResourceResponse = $this->staticResourceHandler
226 | ? $this->staticResourceHandler->processStaticResource($request, $response)
227 | : null;
228 | if ($staticResourceResponse) {
229 | // Eventually: emit a request log here
230 | $this->logger->logAccessForStaticResource($request, $staticResourceResponse);
231 | return;
232 | }
233 |
234 | $emitter = new SwooleEmitter($response);
235 |
236 | try {
237 | $psr7Request = ($this->serverRequestFactory)($request);
238 | } catch (Throwable $e) {
239 | // Error in generating the request
240 | $this->emitMarshalServerRequestException($emitter, $e, $request);
241 | return;
242 | }
243 |
244 | $psr7Response = $this->handler->handle($psr7Request);
245 | $emitter->emit($psr7Response);
246 | $this->logger->logAccessForPsr7Resource($request, $psr7Response);
247 | }
248 |
249 | /**
250 | * Handle the shutting down of the server
251 | */
252 | public function onShutdown(SwooleHttpServer $server) : void
253 | {
254 | $this->pidManager->delete();
255 | $this->logger->notice('Swoole HTTP has been terminated.');
256 | }
257 |
258 | /**
259 | * Emit marshal server request exception
260 | */
261 | private function emitMarshalServerRequestException(
262 | EmitterInterface $emitter,
263 | Throwable $exception,
264 | SwooleHttpRequest $request
265 | ) : void {
266 | $psr7Response = ($this->serverRequestErrorResponseGenerator)($exception);
267 | $emitter->emit($psr7Response);
268 | $this->logger->logAccessForPsr7Resource($request, $psr7Response);
269 | }
270 |
271 | /**
272 | * Set the process name, only if the current OS supports the operation
273 | *
274 | * @param string $name
275 | */
276 | private function setProcessName(string $name) : void
277 | {
278 | if (PHP_OS === 'Darwin') {
279 | return;
280 | }
281 | swoole_set_process_name($name);
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/src/Log/AccessLogDataMap.php:
--------------------------------------------------------------------------------
1 | .*?)((?\d+))?$/';
42 |
43 | /**
44 | * Timestamp when created, indicating end of request processing.
45 | *
46 | * @var float
47 | */
48 | private $endTime;
49 |
50 | /**
51 | * @var SwooleHttpRequest
52 | */
53 | private $request;
54 |
55 | /**
56 | * @var ?PsrResponse
57 | */
58 | private $psrResponse;
59 |
60 | /**
61 | * Whether or not to do a hostname lookup when retrieving the remote host name
62 | *
63 | * @var bool
64 | */
65 | private $useHostnameLookups;
66 |
67 | /**
68 | * @var StaticResourceResponse
69 | */
70 | private $staticResource;
71 |
72 | public static function createWithPsrResponse(
73 | SwooleHttpRequest $request,
74 | PsrResponse $response,
75 | bool $useHostnameLookups = false
76 | ) : self {
77 | $map = new self($request, $useHostnameLookups);
78 | $map->psrResponse = $response;
79 | return $map;
80 | }
81 |
82 | public static function createWithStaticResource(
83 | SwooleHttpRequest $request,
84 | StaticResourceResponse $response,
85 | bool $useHostnameLookups = false
86 | ) : self {
87 | $map = new self($request, $useHostnameLookups);
88 | $map->staticResource = $response;
89 | return $map;
90 | }
91 |
92 | /**
93 | * Client IP address of the request (%a)
94 | */
95 | public function getClientIp() : string
96 | {
97 | return $this->getLocalIp();
98 | }
99 |
100 | /**
101 | * Local IP-address (%A)
102 | */
103 | public function getLocalIp() : string
104 | {
105 | return $this->getServerParamIp('REMOTE_ADDR');
106 | }
107 |
108 | /**
109 | * Filename (%f)
110 | *
111 | * @todo We likely need a way of injecting the gateway script, instead of
112 | * assuming it's getcwd() . /public/index.php.
113 | * @todo We likely need a way of injecting the document root, instead of
114 | * assuming it's getcwd() . /public.
115 | */
116 | public function getFilename() : string
117 | {
118 | if ($this->psrResponse) {
119 | return getcwd() . '/public/index.php';
120 | }
121 | return getcwd() . '/public' . $this->getServerParam('PATH_INFO');
122 | }
123 |
124 | /**
125 | * Size of the message in bytes, excluding HTTP headers (%B, %b)
126 | */
127 | public function getBodySize(string $default) : string
128 | {
129 | if ($this->psrResponse) {
130 | return (string) $this->psrResponse->getBody()->getSize() ?: $default;
131 | }
132 | return (string) $this->staticResource->getContentLength() ?: $default;
133 | }
134 |
135 | /**
136 | * Remote hostname (%h)
137 | * Will log the IP address if hostnameLookups is false.
138 | */
139 | public function getRemoteHostname() : string
140 | {
141 | $ip = $this->getServerParamIp('REMOTE_ADDR');
142 |
143 | return $ip !== '-' && $this->useHostnameLookups
144 | ? gethostbyaddr($ip)
145 | : $ip;
146 | }
147 |
148 | /**
149 | * The message protocol (%H)
150 | */
151 | public function getProtocol() : string
152 | {
153 | return $this->getServerParam('server_protocol');
154 | }
155 |
156 | /**
157 | * The request method (%m)
158 | */
159 | public function getMethod() : string
160 | {
161 | return $this->getServerParam('request_method');
162 | }
163 |
164 | /**
165 | * Returns a message header
166 | */
167 | public function getRequestHeader(string $name) : string
168 | {
169 | return $this->request->header[strtolower($name)] ?? '-';
170 | }
171 |
172 | /**
173 | * Returns a message header
174 | */
175 | public function getResponseHeader(string $name) : string
176 | {
177 | if ($this->psrResponse) {
178 | return $this->psrResponse->getHeaderLine($name) ?: '-';
179 | }
180 | return $this->staticResource->getHeader($name) ?: '-';
181 | }
182 |
183 | /**
184 | * Returns a environment variable (%e)
185 | */
186 | public function getEnv(string $name) : string
187 | {
188 | return getenv($name) ?: '-';
189 | }
190 |
191 | /**
192 | * Returns a cookie value (%{VARNAME}C)
193 | */
194 | public function getCookie(string $name) : string
195 | {
196 | return $this->request->cookie[$name] ?? '-';
197 | }
198 |
199 | /**
200 | * The canonical port of the server serving the request. (%p)
201 | */
202 | public function getPort(string $format) : string
203 | {
204 | switch ($format) {
205 | case 'canonical':
206 | case 'local':
207 | preg_match(self::HOST_PORT_REGEX, $this->request->header['host'] ?? '', $matches);
208 | $port = $matches['port'] ?? null;
209 | $port = $port ?: $this->getServerParam('server_port', '80');
210 | $scheme = $this->getServerParam('https', '');
211 | return $scheme && $port === '80' ? '443' : $port;
212 | default:
213 | return '-';
214 | }
215 | }
216 |
217 | /**
218 | * The query string (%q)
219 | * (prepended with a ? if a query string exists, otherwise an empty string).
220 | */
221 | public function getQuery() : string
222 | {
223 | $query = $this->request->get ?? [];
224 | return [] === $query ? '' : sprintf('?%s', http_build_query($query));
225 | }
226 |
227 | /**
228 | * Status. (%s)
229 | */
230 | public function getStatus() : string
231 | {
232 | return $this->psrResponse
233 | ? (string) $this->psrResponse->getStatusCode()
234 | : (string) $this->staticResource->getStatus();
235 | }
236 |
237 | /**
238 | * Remote user if the request was authenticated. (%u)
239 | */
240 | public function getRemoteUser() : string
241 | {
242 | return $this->getServerParam('REMOTE_USER');
243 | }
244 |
245 | /**
246 | * The URL path requested, not including any query string. (%U)
247 | */
248 | public function getPath() : string
249 | {
250 | return $this->getServerParam('PATH_INFO');
251 | }
252 |
253 | /**
254 | * The canonical ServerName of the server serving the request. (%v)
255 | */
256 | public function getHost() : string
257 | {
258 | return $this->getRequestHeader('host');
259 | }
260 |
261 | /**
262 | * The server name according to the UseCanonicalName setting. (%V)
263 | */
264 | public function getServerName() : string
265 | {
266 | return gethostname();
267 | }
268 |
269 | /**
270 | * First line of request. (%r)
271 | */
272 | public function getRequestLine() : string
273 | {
274 | return sprintf(
275 | '%s %s%s %s',
276 | $this->getMethod(),
277 | $this->getPath(),
278 | $this->getQuery(),
279 | $this->getProtocol()
280 | );
281 | }
282 |
283 | /**
284 | * Returns the response status line
285 | */
286 | public function getResponseLine() : string
287 | {
288 | $reasonPhrase = '';
289 | if ($this->psrResponse && $this->psrResponse->getReasonPhrase()) {
290 | $reasonPhrase .= sprintf(' %s', $this->psrResponse->getReasonPhrase());
291 | }
292 | return sprintf(
293 | '%s %d%s',
294 | $this->getProtocol(),
295 | $this->getStatus(),
296 | $reasonPhrase
297 | );
298 | }
299 |
300 | /**
301 | * Bytes transferred (received and sent), including request and headers (%S)
302 | */
303 | public function getTransferredSize() : string
304 | {
305 | return (string) ($this->getRequestMessageSize(0) + $this->getResponseMessageSize(0)) ?: '-';
306 | }
307 |
308 | /**
309 | * Get the request message size (including first line and headers)
310 | */
311 | public function getRequestMessageSize($default = null) : ?int
312 | {
313 | $strlen = function_exists('mb_strlen') ? 'mb_strlen' : 'strlen';
314 |
315 | $bodySize = $strlen($this->request->rawContent());
316 |
317 | if (null === $bodySize) {
318 | return $default;
319 | }
320 |
321 | $firstLine = $this->getRequestLine();
322 |
323 | $headers = [];
324 |
325 | foreach ($this->request->header as $header => $value) {
326 | if (is_string($value)) {
327 | $headers[] = sprintf('%s: %s', $header, $value);
328 | continue;
329 | }
330 |
331 | foreach ($value as $line) {
332 | $headers[] = sprintf('%s: %s', $header, $line);
333 | }
334 | }
335 |
336 | $headersSize = $strlen(implode("\r\n", $headers));
337 |
338 | return $strlen($firstLine) + 2 + $headersSize + 4 + $bodySize;
339 | }
340 |
341 | /**
342 | * Get the response message size (including first line and headers)
343 | */
344 | public function getResponseMessageSize($default = null) : ?int
345 | {
346 | $bodySize = $this->psrResponse
347 | ? $this->psrResponse->getBody()->getSize()
348 | : $this->staticResource->getContentLength();
349 |
350 | if (null === $bodySize) {
351 | return $default;
352 | }
353 |
354 | $strlen = function_exists('mb_strlen') ? 'mb_strlen' : 'strlen';
355 | $firstLineSize = $strlen($this->getResponseLine($message));
356 |
357 | $headerSize = $this->psrResponse
358 | ? $this->getPsrResponseHeaderSize()
359 | : $this->staticResource->getHeaderSize();
360 |
361 | return $firstLineSize + 2 + $headerSize + 4 + $bodySize;
362 | }
363 |
364 | /**
365 | * Returns the request time (%t, %{format}t)
366 | */
367 | public function getRequestTime(string $format) : string
368 | {
369 | $begin = $this->getServerParam('request_time_float');
370 | $time = $begin;
371 |
372 | if (strpos($format, 'begin:') === 0) {
373 | $format = substr($format, 6);
374 | } elseif (strpos($format, 'end:') === 0) {
375 | $time = $this->endTime;
376 | $format = substr($format, 4);
377 | }
378 |
379 | switch ($format) {
380 | case 'sec':
381 | return sprintf('[%s]', round($time));
382 | case 'msec':
383 | return sprintf('[%s]', round($time * 1E3));
384 | case 'usec':
385 | return sprintf('[%s]', round($time * 1E6));
386 | default:
387 | return sprintf('[%s]', strftime($format, (int) $time));
388 | }
389 | }
390 |
391 | /**
392 | * The time taken to serve the request. (%T, %{format}T)
393 | */
394 | public function getRequestDuration(string $format) : string
395 | {
396 | $begin = $this->getServerParam('request_time_float');
397 | switch ($format) {
398 | case 'us':
399 | return (string) round(($this->endTime - $begin) * 1E6);
400 | case 'ms':
401 | return (string) round(($this->endTime - $begin) * 1E3);
402 | default:
403 | return (string) round($this->endTime - $begin);
404 | }
405 | }
406 |
407 | private function __construct(SwooleHttpRequest $request, bool $useHostnameLookups)
408 | {
409 | $this->endTime = microtime(true);
410 | $this->request = $request;
411 | $this->useHostnameLookups = $useHostnameLookups;
412 | }
413 |
414 | /**
415 | * Returns an server parameter value
416 | */
417 | private function getServerParam(string $key, string $default = '-') : string
418 | {
419 | $value = $this->request->server[strtolower($key)] ?? $default;
420 | return (string) $value;
421 | }
422 |
423 | /**
424 | * Returns an ip from the server params
425 | */
426 | private function getServerParamIp(string $key) : string
427 | {
428 | $ip = $this->getServerParam($key);
429 |
430 | return false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)
431 | ? '-'
432 | : $ip;
433 | }
434 |
435 | private function getPsrResponseHeaderSize() : int
436 | {
437 | if (! $this->psrResponse) {
438 | return 0;
439 | }
440 |
441 | $headers = [];
442 |
443 | foreach ($this->psrResponse->getHeaders() as $header => $values) {
444 | foreach ($values as $value) {
445 | $headers[] = sprintf('%s: %s', $header, $value);
446 | }
447 | }
448 |
449 | $strlen = function_exists('mb_strlen') ? 'mb_strlen' : 'strlen';
450 | return $strlen(implode("\r\n", $headers));
451 | }
452 | }
453 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file, in reverse chronological order by release.
4 |
5 | ## 2.5.1 - TBD
6 |
7 | ### Added
8 |
9 | - Nothing.
10 |
11 | ### Changed
12 |
13 | - Nothing.
14 |
15 | ### Deprecated
16 |
17 | - Nothing.
18 |
19 | ### Removed
20 |
21 | - Nothing.
22 |
23 | ### Fixed
24 |
25 | - Nothing.
26 |
27 | ## 2.5.0 - 2019-11-22
28 |
29 | ### Added
30 |
31 | - [#73](https://github.com/zendframework/zend-expressive-swoole/pull/73) adds
32 | compatibility with symfony/console `^5.0`.
33 |
34 | ### Changed
35 |
36 | - Nothing.
37 |
38 | ### Deprecated
39 |
40 | - Nothing.
41 |
42 | ### Removed
43 |
44 | - Nothing.
45 |
46 | ### Fixed
47 |
48 | - Nothing.
49 |
50 | ## 2.4.1 - 2019-11-13
51 |
52 | ### Added
53 |
54 | - Nothing.
55 |
56 | ### Changed
57 |
58 | - Nothing.
59 |
60 | ### Deprecated
61 |
62 | - Nothing.
63 |
64 | ### Removed
65 |
66 | - Nothing.
67 |
68 | ### Fixed
69 |
70 | - [#67](https://github.com/zendframework/zend-expressive-swoole/pull/67) fixes the `HttpServerFactory` to properly support Swoole coroutines.
71 |
72 | ## 2.4.0 - 2019-03-05
73 |
74 | ### Added
75 |
76 | - [#64](https://github.com/zendframework/zend-expressive-swoole/pull/64) updates the hot reloading feature so that it logs messages using the same
77 | logger configured for access logging.
78 |
79 | ### Changed
80 |
81 | - Nothing.
82 |
83 | ### Deprecated
84 |
85 | - Nothing.
86 |
87 | ### Removed
88 |
89 | - Nothing.
90 |
91 | ### Fixed
92 |
93 | - Nothing.
94 |
95 | ## 2.3.0 - 2019-02-07
96 |
97 | ### Added
98 |
99 | - [#60](https://github.com/zendframework/zend-expressive-swoole/pull/60) adds a new configuration, `zend-expressive-swoole.hot-code-reload`.
100 | Configuring hot-code-reload allows the Swoole HTTP server to monitor for
101 | changes in included PHP files, and reload accordingly.
102 |
103 | ### Changed
104 |
105 | - Nothing.
106 |
107 | ### Deprecated
108 |
109 | - Nothing.
110 |
111 | ### Removed
112 |
113 | - Nothing.
114 |
115 | ### Fixed
116 |
117 | - Nothing.
118 |
119 | ## 2.2.1 - 2019-02-07
120 |
121 | ### Added
122 |
123 | - [#56](https://github.com/zendframework/zend-expressive-swoole/pull/56) adds support for PHP 7.3.
124 |
125 | ### Changed
126 |
127 | - Nothing.
128 |
129 | ### Deprecated
130 |
131 | - Nothing.
132 |
133 | ### Removed
134 |
135 | - Nothing.
136 |
137 | ### Fixed
138 |
139 | - Nothing.
140 |
141 | ## 2.2.0 - 2018-12-03
142 |
143 | ### Added
144 |
145 | - [#55](https://github.com/zendframework/zend-expressive-swoole/pull/55) adds a new configuration key, `zend-expressive-swoole.swoole-http-server.logger.logger-name`.
146 | It allows a custom service name which resolves to a `Psr\Log\LoggerInterface`
147 | instance to be provided, in order to be wrapped in the
148 | `Zend\Expressive\Swoole\Log\Psr3AccessLogDecorator`:
149 |
150 | ```php
151 | return [
152 | 'zend-expressive-swoole' => [
153 | 'swoole-http-server' => [
154 | 'logger' => [
155 | 'logger-name' => 'my_logger',
156 | ],
157 | ],
158 | ],
159 | ];
160 | ```
161 |
162 | ### Changed
163 |
164 | - Nothing.
165 |
166 | ### Deprecated
167 |
168 | - Nothing.
169 |
170 | ### Removed
171 |
172 | - Nothing.
173 |
174 | ### Fixed
175 |
176 | - Nothing.
177 |
178 | ## 2.1.0 - 2018-11-28
179 |
180 | ### Added
181 |
182 | - [#54](https://github.com/zendframework/zend-expressive-swoole/pull/54) adds a new configuration key, `zend-expressive-swoole.swoole-http-server.process-name`.
183 | This value will be used as a prefix for the process name of all processes
184 | created by the `Swoole\Http\Server` instance, including the master process,
185 | worker processes, and all task worker processes. The value defaults to
186 | `expressive`. As an example:
187 |
188 | ```php
189 | return [
190 | 'zend-expressive-swoole' => [
191 | 'swoole-http-server' => [
192 | 'process-name' => 'myapp',
193 | ],
194 | ],
195 | ];
196 | ```
197 |
198 | - [#50](https://github.com/zendframework/zend-expressive-swoole/pull/50) adds a new configuration flag for toggling serving of static files:
199 | `zend-expressive-swoole.swoole-http-server.static-files.enable`. The flag is
200 | enabled by default; set it to boolean `false` to disable static file serving:
201 |
202 | ```php
203 | return [
204 | 'zend-expressive-swoole' => [
205 | 'swoole-http-server' => [
206 | 'static-files' => [
207 | 'enable' => false,
208 | ],
209 | ],
210 | ],
211 | ];
212 | ```
213 |
214 | ### Changed
215 |
216 | - Nothing.
217 |
218 | ### Deprecated
219 |
220 | - Nothing.
221 |
222 | ### Removed
223 |
224 | - Nothing.
225 |
226 | ### Fixed
227 |
228 | - Nothing.
229 |
230 | ## 2.0.1 - 2018-11-28
231 |
232 | ### Added
233 |
234 | - Nothing.
235 |
236 | ### Changed
237 |
238 | - Nothing.
239 |
240 | ### Deprecated
241 |
242 | - Nothing.
243 |
244 | ### Removed
245 |
246 | - Nothing.
247 |
248 | ### Fixed
249 |
250 | - [#48](https://github.com/zendframework/zend-expressive-swoole/pull/48) adds a `shutdown` handler to the Swoole HTTP server that clears the PID
251 | manager, ensuring the PID file is cleared.
252 |
253 | - [#52](https://github.com/zendframework/zend-expressive-swoole/pull/52) fixes an error thrown by the `start` command when using this component in
254 | configuration-driven expressive applications, due to the fact that the command
255 | always tried to require the `config/pipeline.php` and `config/routes.php`
256 | files.
257 |
258 | ## 2.0.0 - 2018-11-15
259 |
260 | ### Added
261 |
262 | - [#46](https://github.com/zendframework/zend-expressive-swoole/pull/46) adds a new command for the command line tooling, `status`; the command
263 | simply tells you if the server is running or not.
264 |
265 | - [#43](https://github.com/zendframework/zend-expressive-swoole/pull/43) adds the class `Zend\Expressive\Swoole\WhoopsPrettyPageHandlerDelegator`,
266 | and registers it to the service `Zend\Expressive\WhoopsPageHandler`. The
267 | delegator calls `handleUnconditionally()` on the handler in order to ensure it
268 | will operate under the CLI SAPI that Swoole runs under.
269 |
270 | - [#40](https://github.com/zendframework/zend-expressive-swoole/pull/40) adds the class `Zend\Expressive\Swoole\HttpServerFactory`, which
271 | generates a `Swoole\Http\Server` instance based on provided configuration; it
272 | replaces the former `Zend\Expressive\Swoole\ServerFactory` (as well as the
273 | factory for that class). The new factory class is now registered as a factory
274 | for the `Swoole\Http\Server` class, which allows users to further configure
275 | the Swoole server instance via delegators, specifically for the purpose of
276 | [enabling async task workers](https://docs.zendframework.com/zend-expressive-swoole/v2/async-tasks/).
277 |
278 | ### Changed
279 |
280 | - [#46](https://github.com/zendframework/zend-expressive-swoole/pull/46) moves the command line utilities for controlling the web server out of
281 | the application runner, and into a new vendor binary, `zend-expressive-swoole`
282 | (called via `./vendor/bin/zend-expressive-swoole`). This change was required
283 | to allow us to expose the `Swoole\Http\Server` instance as a service, and has
284 | the added benefit that `reload` operations now will fully stop and start the
285 | server, allowing it to pick up configuration and code changes. **You will need
286 | to update any deployment scripts to use the new vendor binary.**
287 |
288 | - [#40](https://github.com/zendframework/zend-expressive-swoole/pull/40) changes how you configure Swoole's coroutine support. Previously, you
289 | would toggle the configuration flag `zend-expressive-swoole.swoole-http-server.options.enable_coroutine`;
290 | you should now use the flag `zend-expressive-swoole.enable_coroutine`. The
291 | original flag still exists, but is now used to toggle coroutine support in the
292 | `Swoole\Http\Server` instance specifically.
293 |
294 | - [#42](https://github.com/zendframework/zend-expressive-swoole/pull/42) adds a discrete factory service for the `SwooleRequestHandlerRunner`, and now aliases
295 | `Zend\HttpHandlerRunner\RequestHandlerRunner` to that service.
296 |
297 | ### Deprecated
298 |
299 | - Nothing.
300 |
301 | ### Removed
302 |
303 | - [#40](https://github.com/zendframework/zend-expressive-swoole/pull/40) removes the `Zend\Expressive\Swoole\ServerFactory` and
304 | `ServerFactoryFactory` classes, as well as the `Zend\Expressive\Swoole\ServerFactory`
305 | service. Users should instead reference the `Swoole\Http\Server` service,
306 | which is now registered via the `Zend\Expressive\Swoole\HttpServerFactory`
307 | factory, detailed in the "Added" section above.
308 |
309 | ### Fixed
310 |
311 | - Nothing.
312 |
313 | ## 1.0.2 - 2018-11-13
314 |
315 | ### Added
316 |
317 | - Nothing.
318 |
319 | ### Changed
320 |
321 | - Nothing.
322 |
323 | ### Deprecated
324 |
325 | - Nothing.
326 |
327 | ### Removed
328 |
329 | - Nothing.
330 |
331 | ### Fixed
332 |
333 | - [#45](https://github.com/zendframework/zend-expressive-swoole/pull/45) provides a patch that ensures that SSL support can be enabled when
334 | creating the `Swoole\Http\Server` instance. SSL support requires not just the
335 | SSL certificate and private key, but also providing a protocol of either
336 | `SWOOLE_SOCK_TCP | SWOOLE_SSL` or `SWOOLE_SOCK_TCP6 | SWOOLE_SSL`.
337 | Previously, the union types would raise an exception during instantiation.
338 |
339 | ## 1.0.1 - 2018-11-08
340 |
341 | ### Added
342 |
343 | - Nothing.
344 |
345 | ### Changed
346 |
347 | - Nothing.
348 |
349 | ### Deprecated
350 |
351 | - Nothing.
352 |
353 | ### Removed
354 |
355 | - Nothing.
356 |
357 | ### Fixed
358 |
359 | - [#41](https://github.com/zendframework/zend-expressive-swoole/pull/41) fixes an issue that occurs when the HTTP request body is empty.
360 | `Swoole\Http\Request::rawcontent()` returns `false` in such situations, when a
361 | string is expected. `Zend\Expressive\Swoole\SwooleStream` now detects this and
362 | casts to an empty string.
363 |
364 | ## 1.0.0 - 2018-10-02
365 |
366 | ### Added
367 |
368 | - [#38](https://github.com/zendframework/zend-expressive-swoole/pull/38) adds documentation covering potential issues when using a long-running
369 | server such as Swoole, as well as how to avoid them.
370 |
371 | - [#38](https://github.com/zendframework/zend-expressive-swoole/pull/38) adds documentation covering how to use Monolog as a PSR-3 logger for the
372 | Swoole server.
373 |
374 | - [#38](https://github.com/zendframework/zend-expressive-swoole/pull/38) adds a default value of 1024 for the `max_conn` Swoole HTTP server option.
375 | By default, Swoole uses the value of `ulimit -n` on the system; however, in
376 | containers and virtualized environments, this value often reports far higher
377 | than the host system can allow, which can lead to resource problems and
378 | termination of the server. Setting a default ensures the component can work
379 | out-of-the-box for most situations. Users should consult their host machine
380 | specifications and set an appropriate value in production.
381 |
382 | ### Changed
383 |
384 | - [#38](https://github.com/zendframework/zend-expressive-swoole/pull/38) versions the documentation, moving all URLS below the `/v1/` subpath.
385 | Redirects from the original pages to the new ones were also added.
386 |
387 | ### Deprecated
388 |
389 | - Nothing.
390 |
391 | ### Removed
392 |
393 | - Nothing.
394 |
395 | ### Fixed
396 |
397 | - Nothing.
398 |
399 | ## 0.2.4 - 2018-10-02
400 |
401 | ### Added
402 |
403 | - [#37](https://github.com/zendframework/zend-expressive-swoole/pull/37) adds support for zendframework/zend-diactoros 2.0.0. You may use either
404 | a 1.Y or 2.Y version of that library with Expressive applications.
405 |
406 | ### Changed
407 |
408 | - Nothing.
409 |
410 | ### Deprecated
411 |
412 | - Nothing.
413 |
414 | ### Removed
415 |
416 | - Nothing.
417 |
418 | ### Fixed
419 |
420 | - [#36](https://github.com/zendframework/zend-expressive-swoole/pull/36) fixes the call to `emitMarshalServerRequestException()` to ensure the
421 | request is passed to it.
422 |
423 | ## 0.2.3 - 2018-09-27
424 |
425 | ### Added
426 |
427 | - Nothing.
428 |
429 | ### Changed
430 |
431 | - Nothing.
432 |
433 | ### Deprecated
434 |
435 | - Nothing.
436 |
437 | ### Removed
438 |
439 | - Nothing.
440 |
441 | ### Fixed
442 |
443 | - [#35](https://github.com/zendframework/zend-expressive-swoole/pull/35) fixes logging when unable to marshal a server request.
444 |
445 | ## 0.2.2 - 2018-09-05
446 |
447 | ### Added
448 |
449 | - [#28](https://github.com/zendframework/zend-expressive-swoole/pull/28) adds a new option, `zend-expressive-swoole.swoole-http-server.options.enable_coroutine`.
450 | The option is only relevant for Swoole 4.1 and up. When enabled, this option
451 | will turn on coroutine support, which essentially wraps most blocking I/O
452 | operations (including PDO, Mysqli, Redis, SOAP, `stream_socket_client`,
453 | `fsockopen`, and `file_get_contents` with URIs) into coroutines, allowing
454 | workers to handle additional requests while waiting for the operations to
455 | complete.
456 |
457 | ### Changed
458 |
459 | - Nothing.
460 |
461 | ### Deprecated
462 |
463 | - Nothing.
464 |
465 | ### Removed
466 |
467 | - Nothing.
468 |
469 | ### Fixed
470 |
471 | - Nothing.
472 |
473 | ## 0.2.1 - 2018-09-04
474 |
475 | ### Added
476 |
477 | - Nothing.
478 |
479 | ### Changed
480 |
481 | - Nothing.
482 |
483 | ### Deprecated
484 |
485 | - Nothing.
486 |
487 | ### Removed
488 |
489 | - Nothing.
490 |
491 | ### Fixed
492 |
493 | - [#30](https://github.com/zendframework/zend-expressive-swoole/pull/30) fixes how the `Content-Length` header is passed to the Swoole response, ensuring we cast the value to a string.
494 |
495 | ## 0.2.0 - 2018-08-30
496 |
497 | ### Added
498 |
499 | - [#26](https://github.com/zendframework/zend-expressive-swoole/pull/26) adds comprehensive access logging capabilities via a new subnamespace,
500 | `Zend\Expressive\Swoole\Log`. Capabilities include support (most) of the
501 | Apache log format placeholders (as well as the standard formats used by Apache
502 | and Debian), and the ability to provide your own formatting mechanisms. Please
503 | see the [logging documentation](https://docs.zendframework.com/zend-expressive-swoole/logging/)
504 | for more information.
505 |
506 | - [#20](https://github.com/zendframework/zend-expressive-swoole/pull/20) adds a new interface, `Zend\Expressive\Swoole\StaticResourceHandlerInterface`,
507 | and default implementation `Zend\Expressive\Swoole\StaticResourceHandler`,
508 | used to determine if a request is for a static file, and then to serve it; the
509 | `SwooleRequestHandlerRunner` composes an instance now for providing static
510 | resource serving capabilities.
511 |
512 | The default implementation uses custom middleware to allow providing common
513 | features such as HTTP client-side caching headers, handling `OPTIONS`
514 | requests, etc. Full capabilities include:
515 |
516 | - Filtering by allowed extensions.
517 | - Emitting `405` statuses for unsupported HTTP methods.
518 | - Handling `OPTIONS` requests.
519 | - Handling `HEAD` requests.
520 | - Providing gzip/deflate compression of response content.
521 | - Selectively emitting `Cache-Control` headers.
522 | - Selectively emitting `Last-Modified` headers.
523 | - Selectively emitting `ETag` headers.
524 |
525 | Please see the [static resource documentation](https://docs.zendframework.com/zend-expressive-swoole/static-resources/)
526 | for more information.
527 |
528 | - [#11](https://github.com/zendframework/zend-expressive-swoole/pull/11), [#18](https://github.com/zendframework/zend-expressive-swoole/pull/18), and [#22](https://github.com/zendframework/zend-expressive-swoole/pull/22) add the following console actions and options to
529 | interact with the server via `public/index.php`:
530 | - `start` will start the server; it may be omitted, as this is the default action.
531 | - `--dameonize|-d` tells the server to daemonize itself when `start` is called.
532 | - `--num_workers|w` tells the server how many workers to spawn when starting (defaults to 4).
533 | - `stop` will stop the server.
534 | - `reload` reloads all worker processes, but only when the zend-expressive-swoole.swoole-http-server.mode
535 | configuration value is set to `SWOOLE_PROCESS`.
536 |
537 | ### Changed
538 |
539 | - [#21](https://github.com/zendframework/zend-expressive-swoole/pull/21) renames `RequestHandlerSwooleRunner` (and its related factory) to `SwooleRequestHandlerRunner`.
540 |
541 | - [#20](https://github.com/zendframework/zend-expressive-swoole/pull/20) and [#26](https://github.com/zendframework/zend-expressive-swoole/pull/26) modify the collaborators and thus constructor arguments
542 | expected by the `SwooleRequestHandlerRunner`. The constructor now has the
543 | following signature:
544 |
545 | ```php
546 | public function __construct(
547 | Psr\Http\Server\RequestHandlerInterface $handler,
548 | callable $serverRequestFactory,
549 | callable $serverRequestErrorResponseGenerator,
550 | Zend\Expressive\Swoole\PidManager $pidManager,
551 | Zend\Expressive\Swoole\ServerFactory $serverFactory,
552 | Zend\Expressive\Swoole\StaticResourceHandlerInterface $staticResourceHandler = null,
553 | Zend\Expressive\Swoole\Log\AccessLogInterface $logger = null
554 | ) {
555 | ```
556 |
557 | If you were manually creating an instance, or had provided your own factory,
558 | you will need to update your code.
559 |
560 | ### Deprecated
561 |
562 | - Nothing.
563 |
564 | ### Removed
565 |
566 | - Nothing.
567 |
568 | ### Fixed
569 |
570 | - Nothing.
571 |
572 | ## 0.1.1 - 2018-08-14
573 |
574 | ### Added
575 |
576 | - [#5](https://github.com/zendframework/zend-expressive-swoole/pull/5) adds the ability to serve static file resources from your
577 | configured document root. For information on the default capabilities, as well
578 | as how to configure the functionality, please see
579 | https://docs.zendframework.com/zend-expressive-swoole/intro/#serving-static-files.
580 |
581 | ### Changed
582 |
583 | - [#9](https://github.com/zendframework/zend-expressive-swoole/pull/9) modifies how the `RequestHandlerSwooleRunner` provides logging
584 | output. Previously, it used `printf()` directly. Now it uses a [PSR-3
585 | logger](https://www.php-fig.org/psr/psr-3/) instance, defaulting to an
586 | internal implementation that writes to STDOUT. The logger may be provided
587 | during instantiation, or via the `Psr\Log\LoggerInterface` service.
588 |
589 | ### Deprecated
590 |
591 | - Nothing.
592 |
593 | ### Removed
594 |
595 | - Nothing.
596 |
597 | ### Fixed
598 |
599 | - [#7](https://github.com/zendframework/zend-expressive-swoole/pull/7) fixes how cookies are emitted by the Swoole HTTP server. We now
600 | use the server `cookie()` method to set cookies, ensuring that multiple
601 | cookies are not squashed into a single `Set-Cookie` header.
602 |
603 | ## 0.1.0 - 2018-07-10
604 |
605 | ### Added
606 |
607 | - Everything.
608 |
609 | ### Changed
610 |
611 | - Nothing.
612 |
613 | ### Deprecated
614 |
615 | - Nothing.
616 |
617 | ### Removed
618 |
619 | - Nothing.
620 |
621 | ### Fixed
622 |
623 | - Nothing.
624 |
--------------------------------------------------------------------------------