├── 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 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-expressive-swoole.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-expressive-swoole) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-expressive-swoole/badge.svg?branch=master)](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 | --------------------------------------------------------------------------------